UI: Improvement multiple attribute widget

This commit is contained in:
Vladyslav_Prykhodko 2021-12-21 11:50:05 +02:00
parent 783552f6c3
commit 49a9a3136f
5 changed files with 141 additions and 14 deletions

File diff suppressed because one or more lines are too long

View File

@ -114,6 +114,10 @@ export function isNumeric(value: any): boolean {
return (value - parseFloat(value) + 1) >= 0; return (value - parseFloat(value) + 1) >= 0;
} }
export function isBoolean(value: any): boolean {
return typeof value === 'boolean';
}
export function isString(value: any): boolean { export function isString(value: any): boolean {
return typeof value === 'string'; return typeof value === 'string';
} }

View File

@ -37,7 +37,12 @@
type="text" type="text"
(focus)="key.isFocused = true; focusInputElement($event)" (focus)="key.isFocused = true; focusInputElement($event)"
(blur)="key.isFocused = false; inputChanged(source, key)"> (blur)="key.isFocused = false; inputChanged(source, key)">
<mat-icon *ngIf="key.settings.icon" matPrefix>{{key.settings.icon}}</mat-icon> <ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matPrefix>
<mat-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</mat-icon>
<ng-template #customToggleIcon>
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon">
</ng-template>
</ng-container>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')"> <mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
{{ getErrorMessageText(key.settings, 'required') }} {{ getErrorMessageText(key.settings, 'required') }}
</mat-error> </mat-error>
@ -57,7 +62,12 @@
max="{{key.settings.maxValue}}" max="{{key.settings.maxValue}}"
(focus)="key.isFocused = true; focusInputElement($event)" (focus)="key.isFocused = true; focusInputElement($event)"
(blur)="key.isFocused = false; inputChanged(source, key)"> (blur)="key.isFocused = false; inputChanged(source, key)">
<mat-icon *ngIf="key.settings.icon" matPrefix>{{key.settings.icon}}</mat-icon> <ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matPrefix>
<mat-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</mat-icon>
<ng-template #customToggleIcon>
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon">
</ng-template>
</ng-container>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')"> <mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
{{ getErrorMessageText(key.settings,'required') }} {{ getErrorMessageText(key.settings,'required') }}
</mat-error> </mat-error>
@ -77,7 +87,14 @@
</div> </div>
<div class="input-field mat-block" *ngIf="key.settings.dataKeyValueType === 'booleanSwitch'"> <div class="input-field mat-block" *ngIf="key.settings.dataKeyValueType === 'booleanSwitch'">
<mat-slide-toggle formControlName="{{key.formId}}" <mat-slide-toggle formControlName="{{key.formId}}"
[labelPosition]="key.settings.slideToggleLabelPosition"
(change)="inputChanged(source, key)"> (change)="inputChanged(source, key)">
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon">
<mat-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</mat-icon>
<ng-template #customToggleIcon>
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon">
</ng-template>
</ng-container>
<span class="label-wrapper">{{key.label}}</span> <span class="label-wrapper">{{key.label}}</span>
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>

View File

@ -64,3 +64,17 @@
} }
} }
} }
:host ::ng-deep {
.tb-multiple-input {
.mat-slide-toggle-content {
width: 100%;
display: flex;
align-items: center;
.mat-icon {
margin-right: 8px;
}
}
}
}

View File

@ -24,7 +24,14 @@ import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DataKey, Datasource, DatasourceData, DatasourceType, WidgetConfig } from '@shared/models/widget.models'; import { DataKey, Datasource, DatasourceData, DatasourceType, WidgetConfig } from '@shared/models/widget.models';
import { IWidgetSubscription } from '@core/api/widget-api.models'; import { IWidgetSubscription } from '@core/api/widget-api.models';
import { createLabelFromDatasource, isDefinedAndNotNull, isEqual, isNotEmptyStr, isUndefined } from '@core/utils'; import {
createLabelFromDatasource,
isBoolean, isDefined,
isDefinedAndNotNull,
isEqual,
isNotEmptyStr,
isUndefined
} from '@core/utils';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import * as _moment from 'moment'; import * as _moment from 'moment';
import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms';
@ -35,6 +42,7 @@ import { forkJoin, Observable, Subject } from 'rxjs';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
type FieldAlignment = 'row' | 'column'; type FieldAlignment = 'row' | 'column';
@ -44,6 +52,9 @@ type MultipleInputWidgetDataKeyValueType = 'string' | 'double' | 'integer' |
'dateTime' | 'date' | 'time' | 'select'; 'dateTime' | 'date' | 'time' | 'select';
type MultipleInputWidgetDataKeyEditableType = 'editable' | 'disabled' | 'readonly'; type MultipleInputWidgetDataKeyEditableType = 'editable' | 'disabled' | 'readonly';
type ConvertGetValueFunction = (value: any, ctx: WidgetContext) => any;
type ConvertSetValueFunction = (value: any, originValue: any, ctx: WidgetContext) => any;
interface MultipleInputWidgetSettings { interface MultipleInputWidgetSettings {
widgetTitle: string; widgetTitle: string;
showActionButtons: boolean; showActionButtons: boolean;
@ -66,6 +77,7 @@ interface MultipleInputWidgetSelectOption {
interface MultipleInputWidgetDataKeySettings { interface MultipleInputWidgetDataKeySettings {
dataKeyType: MultipleInputWidgetDataKeyType; dataKeyType: MultipleInputWidgetDataKeyType;
dataKeyValueType: MultipleInputWidgetDataKeyValueType; dataKeyValueType: MultipleInputWidgetDataKeyValueType;
slideToggleLabelPosition?: 'after' | 'before';
selectOptions: MultipleInputWidgetSelectOption[]; selectOptions: MultipleInputWidgetSelectOption[];
required: boolean; required: boolean;
isEditable: MultipleInputWidgetDataKeyEditableType; isEditable: MultipleInputWidgetDataKeyEditableType;
@ -78,10 +90,19 @@ interface MultipleInputWidgetDataKeySettings {
invalidDateErrorMessage?: string; invalidDateErrorMessage?: string;
minValueErrorMessage?: string; minValueErrorMessage?: string;
maxValueErrorMessage?: string; maxValueErrorMessage?: string;
useCustomIcon: boolean;
icon: string; icon: string;
customIcon: string ;
safeCustomIcon?: SafeUrl;
inputTypeNumber?: boolean; inputTypeNumber?: boolean;
readOnly?: boolean; readOnly?: boolean;
disabledOnCondition?: boolean; disabledOnCondition?: boolean;
useGetValueFunction?: boolean;
getValueFunctionBody?: string;
getValueFunction?: ConvertGetValueFunction;
useSetValueFunction?: boolean;
setValueFunctionBody?: string;
setValueFunction?: ConvertSetValueFunction;
} }
interface MultipleInputWidgetDataKey extends DataKey { interface MultipleInputWidgetDataKey extends DataKey {
@ -139,7 +160,8 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
private utils: UtilsService, private utils: UtilsService,
private fb: FormBuilder, private fb: FormBuilder,
private attributeService: AttributeService, private attributeService: AttributeService,
private translate: TranslateService) { private translate: TranslateService,
private sanitizer: DomSanitizer) {
super(store); super(store);
} }
@ -265,6 +287,34 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
}); });
} }
if (dataKey.settings.dataKeyValueType === 'booleanSwitch' && isUndefined(dataKey.settings.slideToggleLabelPosition)) {
dataKey.settings.slideToggleLabelPosition = 'after';
}
if (dataKey.settings.useCustomIcon && isDefinedAndNotNull(dataKey.settings.customIcon)) {
dataKey.settings.safeCustomIcon = this.sanitizer.bypassSecurityTrustUrl(dataKey.settings.customIcon);
}
if (dataKey.settings.useGetValueFunction && dataKey.settings.getValueFunctionBody.length) {
try {
dataKey.settings.getValueFunction =
new Function('value, ctx', dataKey.settings.getValueFunctionBody) as ConvertGetValueFunction;
} catch (e) {
console.warn(`Parse getValue function in key ${dataKey.label}`, e);
dataKey.settings.getValueFunction = null;
}
}
if (dataKey.settings.useSetValueFunction && dataKey.settings.setValueFunctionBody.length) {
try {
dataKey.settings.setValueFunction =
new Function('value, originValue, ctx', dataKey.settings.setValueFunctionBody) as ConvertSetValueFunction;
} catch (e) {
console.warn(`Parse setValue function in key ${dataKey.label}`, e);
dataKey.settings.setValueFunction = null;
}
}
source.keys.push(dataKey); source.keys.push(dataKey);
}); });
} else { } else {
@ -342,28 +392,41 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
source.keys.forEach((key) => { source.keys.forEach((key) => {
const keyData = data[dataIndex].data; const keyData = data[dataIndex].data;
if (keyData && keyData.length) { if (keyData && keyData.length) {
let value; let value = keyData[0][1];
const keyValue = this.getKeyValue(value, key.settings);
switch (key.settings.dataKeyValueType) { switch (key.settings.dataKeyValueType) {
case 'dateTime': case 'dateTime':
case 'date': case 'date':
if (isDefinedAndNotNull(keyData[0][1]) && keyData[0][1] !== '') { if (isDefinedAndNotNull(keyValue) && keyValue !== '') {
value = _moment(keyData[0][1]).toDate(); if (keyValue instanceof Date) {
value = keyValue;
} else {
value = _moment(keyValue).toDate();
}
} else { } else {
value = null; value = null;
} }
break; break;
case 'time': case 'time':
value = _moment().startOf('day').add(keyData[0][1], 'ms').toDate(); if (keyValue instanceof Date) {
value = keyValue;
} else {
value = _moment().startOf('day').add(keyValue, 'ms').toDate();
}
break; break;
case 'booleanCheckbox': case 'booleanCheckbox':
case 'booleanSwitch': case 'booleanSwitch':
value = (keyData[0][1] === 'true'); if (isBoolean(keyValue)) {
value = keyValue;
} else {
value = (keyValue === 'true');
}
break; break;
case 'select': case 'select':
value = keyData[0][1].toString(); value = keyValue.toString();
break; break;
default: default:
value = keyData[0][1]; value = keyValue;
} }
key.value = value; key.value = value;
} }
@ -399,6 +462,18 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
}); });
} }
private getKeyValue(data: any, keySetting: MultipleInputWidgetDataKeySettings) {
if (isDefined(keySetting.getValueFunction)) {
try {
return keySetting.getValueFunction(data, this.ctx);
} catch (e) {
console.warn(`Call function getValue`, e);
return data;
}
}
return data;
}
private updateWidgetDisplaying() { private updateWidgetDisplaying() {
this.changeAlignment = (this.ctx.$container && this.ctx.$container[0].offsetWidth < 620); this.changeAlignment = (this.ctx.$container && this.ctx.$container[0].offsetWidth < 620);
} }
@ -493,7 +568,8 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
if (!this.settings.showActionButtons && !this.isSavingInProgress) { if (!this.settings.showActionButtons && !this.isSavingInProgress) {
this.isSavingInProgress = true; this.isSavingInProgress = true;
const currentValue = this.multipleInputFormGroup.get(key.formId).value; const currentValue = this.multipleInputFormGroup.get(key.formId).value;
if (!key.settings.required || (key.settings.required && isDefinedAndNotNull(currentValue) && isNotEmptyStr(currentValue.toString()))) { if (!key.settings.required ||
(key.settings.required && isDefinedAndNotNull(currentValue) && isNotEmptyStr(currentValue.toString()))) {
const dataToSave: MultipleInputWidgetSource = { const dataToSave: MultipleInputWidgetSource = {
datasource: source.datasource, datasource: source.datasource,
keys: [key] keys: [key]
@ -523,8 +599,9 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
const sharedAttributes: AttributeData[] = []; const sharedAttributes: AttributeData[] = [];
const telemetry: AttributeData[] = []; const telemetry: AttributeData[] = [];
for (const key of toSave.keys) { for (const key of toSave.keys) {
const currentValue = key.settings.dataKeyHidden ? key.value : this.multipleInputFormGroup.get(key.formId).value; let currentValue = key.settings.dataKeyHidden ? key.value : this.multipleInputFormGroup.get(key.formId).value;
if (!isEqual(currentValue, key.value) || this.settings.updateAllValues) { if (!isEqual(currentValue, key.value) || this.settings.updateAllValues) {
currentValue = this.setKeyValue(currentValue, key, toSave);
const attribute: AttributeData = { const attribute: AttributeData = {
key: key.name, key: key.name,
value: null value: null
@ -615,6 +692,21 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
} }
} }
private setKeyValue(value: any, key: MultipleInputWidgetDataKey, source: MultipleInputWidgetSource) {
if (isDefined(key.settings.setValueFunction)) {
const currentDatasourceData = this.subscription.data
.find(dsData => dsData.datasource === source.datasource && dsData.dataKey.name === key.name);
const originValue = currentDatasourceData.data[0][1];
try {
return key.settings.setValueFunction(value, originValue, this.ctx);
} catch (e) {
console.warn(`Call function setValue`, e);
return value;
}
}
return value;
}
public discardAll() { public discardAll() {
this.multipleInputFormGroup.reset(undefined, {emitEvent: false}); this.multipleInputFormGroup.reset(undefined, {emitEvent: false});
this.sources.forEach((source) => { this.sources.forEach((source) => {