Merge pull request #12150 from vvlladd28/feature/edit-alias/datasource-widgets

Add edit alias from dasource widgets
This commit is contained in:
Igor Kulikov 2024-11-29 18:06:58 +02:00 committed by GitHub
commit 404ab062a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 97 additions and 59 deletions

View File

@ -31,6 +31,14 @@
(click)="clear()"> (click)="clear()">
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
<button *ngIf="selectEntityAliasFormGroup.get('entityAlias').value?.id && !disabled"
type="button"
matSuffix mat-icon-button aria-label="Edit"
matTooltip="{{ 'device-profile.edit' | translate }}"
matTooltipPosition="above"
(click)="editEntityAlias($event)">
<mat-icon class="material-icons">edit</mat-icon>
</button>
<button *ngIf="!selectEntityAliasFormGroup.get('entityAlias').value && !disabled" <button *ngIf="!selectEntityAliasFormGroup.get('entityAlias').value && !disabled"
style="margin-right: 8px;" style="margin-right: 8px;"
type="button" type="button"

View File

@ -20,4 +20,5 @@ import { EntityAlias } from '@shared/models/alias.models';
export interface EntityAliasSelectCallbacks { export interface EntityAliasSelectCallbacks {
createEntityAlias: (alias: string, allowedEntityTypes: Array<EntityType>) => Observable<EntityAlias>; createEntityAlias: (alias: string, allowedEntityTypes: Array<EntityType>) => Observable<EntityAlias>;
editEntityAlias: (alias: EntityAlias, allowedEntityTypes: Array<EntityType>) => Observable<EntityAlias>;
} }

View File

@ -14,24 +14,22 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, SkipSelf, ViewChild } from '@angular/core'; import { Component, ElementRef, forwardRef, Input, OnInit, SkipSelf, ViewChild } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
UntypedFormBuilder, FormBuilder,
UntypedFormControl, FormControl,
UntypedFormGroup, FormGroup,
FormGroupDirective, FormGroupDirective,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
NgForm NgForm,
} from '@angular/forms'; } from '@angular/forms';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { map, mergeMap, share, tap } from 'rxjs/operators'; import { map, mergeMap, share, tap } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { EntityAlias } from '@shared/models/alias.models'; import { EntityAlias } from '@shared/models/alias.models';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { TruncatePipe } from '@shared/pipe/truncate.pipe'; import { TruncatePipe } from '@shared/pipe/truncate.pipe';
@ -54,9 +52,9 @@ import { ErrorStateMatcher } from '@angular/material/core';
useExisting: EntityAliasSelectComponent useExisting: EntityAliasSelectComponent
}*/] }*/]
}) })
export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, ErrorStateMatcher { export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, ErrorStateMatcher {
selectEntityAliasFormGroup: UntypedFormGroup; selectEntityAliasFormGroup: FormGroup;
modelValue: string | null; modelValue: string | null;
@ -75,15 +73,9 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
@ViewChild('entityAliasAutocomplete') entityAliasAutocomplete: MatAutocomplete; @ViewChild('entityAliasAutocomplete') entityAliasAutocomplete: MatAutocomplete;
@ViewChild('autocomplete', { read: MatAutocompleteTrigger }) autoCompleteTrigger: MatAutocompleteTrigger; @ViewChild('autocomplete', { read: MatAutocompleteTrigger }) autoCompleteTrigger: MatAutocompleteTrigger;
private requiredValue: boolean;
get tbRequired(): boolean {
return this.requiredValue;
}
@Input() @Input()
set tbRequired(value: boolean) { @coerceBoolean()
this.requiredValue = coerceBooleanProperty(value); tbRequired: boolean;
}
@Input() @Input()
disabled: boolean; disabled: boolean;
@ -98,16 +90,13 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
private dirty = false; private dirty = false;
private creatingEntityAlias = false; private propagateChange = (_v: any) => { };
private propagateChange = (v: any) => { }; constructor(@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
constructor(private store: Store<AppState>,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
private entityService: EntityService, private entityService: EntityService,
public translate: TranslateService, public translate: TranslateService,
public truncate: TruncatePipe, public truncate: TruncatePipe,
private fb: UntypedFormBuilder) { private fb: FormBuilder) {
this.selectEntityAliasFormGroup = this.fb.group({ this.selectEntityAliasFormGroup = this.fb.group({
entityAlias: [null] entityAlias: [null]
}); });
@ -117,7 +106,7 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
this.propagateChange = fn; this.propagateChange = fn;
} }
registerOnTouched(fn: any): void { registerOnTouched(_fn: any): void {
} }
ngOnInit() { ngOnInit() {
@ -134,7 +123,7 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
this.filteredEntityAliases = this.selectEntityAliasFormGroup.get('entityAlias').valueChanges this.filteredEntityAliases = this.selectEntityAliasFormGroup.get('entityAlias').valueChanges
.pipe( .pipe(
tap(value => { tap(value => {
let modelValue; let modelValue: EntityAlias;
if (typeof value === 'string' || !value) { if (typeof value === 'string' || !value) {
modelValue = null; modelValue = null;
} else { } else {
@ -151,14 +140,12 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
); );
} }
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = this.tbRequired && !this.modelValue; const customErrorState = this.tbRequired && !this.modelValue;
return originalErrorState || customErrorState; return originalErrorState || customErrorState;
} }
ngAfterViewInit(): void {}
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled; this.disabled = isDisabled;
if (this.disabled) { if (this.disabled) {
@ -225,7 +212,7 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
} }
textIsNotEmpty(text: string): boolean { textIsNotEmpty(text: string): boolean {
return (text && text != null && text.length > 0) ? true : false; return text?.length > 0;
} }
entityAliasEnter($event: KeyboardEvent) { entityAliasEnter($event: KeyboardEvent) {
@ -237,10 +224,23 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
} }
} }
editEntityAlias($event: Event) {
$event.preventDefault();
$event.stopPropagation();
if (this.callbacks && this.callbacks.editEntityAlias) {
this.callbacks.editEntityAlias(this.selectEntityAliasFormGroup.get('entityAlias').value,
this.allowedEntityTypes).subscribe((alias) => {
if (alias) {
this.modelValue = alias.id;
this.selectEntityAliasFormGroup.get('entityAlias').patchValue(alias, {emitEvent: true});
}
});
}
}
createEntityAlias($event: Event, alias: string, focusOnCancel = true) { createEntityAlias($event: Event, alias: string, focusOnCancel = true) {
$event.preventDefault(); $event.preventDefault();
$event.stopPropagation(); $event.stopPropagation();
this.creatingEntityAlias = true;
if (this.callbacks && this.callbacks.createEntityAlias) { if (this.callbacks && this.callbacks.createEntityAlias) {
this.callbacks.createEntityAlias(alias, this.allowedEntityTypes).subscribe((newAlias) => { this.callbacks.createEntityAlias(alias, this.allowedEntityTypes).subscribe((newAlias) => {
if (!newAlias) { if (!newAlias) {

View File

@ -136,8 +136,7 @@
</div> </div>
<ng-template #searchNotEmpty> <ng-template #searchNotEmpty>
<span> <span>
{{ translate.get('entity.no-key-matching', {{ 'entity.no-key-matching' | translate : {key: searchText | truncate: true: 6: &apos;...&apos;} }}
{key: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }}
</span> </span>
<span *ngIf="!isEntityDatasource; else createEntityKey"> <span *ngIf="!isEntityDatasource; else createEntityKey">
<a translate (click)="createKey(searchText)">entity.create-new-key</a> <a translate (click)="createKey(searchText)">entity.create-new-key</a>

View File

@ -16,8 +16,8 @@
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { import {
ChangeDetectorRef,
Component, Component,
DestroyRef,
ElementRef, ElementRef,
forwardRef, forwardRef,
Input, Input,
@ -33,20 +33,18 @@ import {
import { import {
AbstractControl, AbstractControl,
ControlValueAccessor, ControlValueAccessor,
FormBuilder,
FormControl,
FormGroup,
FormGroupDirective, FormGroupDirective,
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
NgForm, NgForm,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
ValidationErrors, ValidationErrors,
Validator Validator
} from '@angular/forms'; } from '@angular/forms';
import { Observable, of } from 'rxjs'; import { Observable, of, ReplaySubject } from 'rxjs';
import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators'; import { filter, map, mergeMap, share, tap } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete'; import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete';
import { MatChipGrid, MatChipInputEvent, MatChipRow } from '@angular/material/chips'; import { MatChipGrid, MatChipInputEvent, MatChipRow } from '@angular/material/chips';
@ -58,7 +56,6 @@ import { DataKeySettingsFunction } from './data-keys.component.models';
import { alarmFields } from '@shared/models/alarm.models'; import { alarmFields } from '@shared/models/alarm.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { ErrorStateMatcher } from '@angular/material/core'; import { ErrorStateMatcher } from '@angular/material/core';
import { TruncatePipe } from '@shared/pipe/truncate.pipe';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { import {
DataKeyConfigDialogComponent, DataKeyConfigDialogComponent,
@ -74,6 +71,7 @@ import { DatasourceComponent } from '@home/components/widget/config/datasource.c
import { ColorPickerPanelComponent } from '@shared/components/color-picker/color-picker-panel.component'; import { ColorPickerPanelComponent } from '@shared/components/color-picker/color-picker-panel.component';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({ @Component({
selector: 'tb-data-keys', selector: 'tb-data-keys',
@ -115,11 +113,10 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
return this.datasourceComponent.hideDataKeyDecimals; return this.datasourceComponent.hideDataKeyDecimals;
} }
datasourceTypes = DatasourceType;
widgetTypes = widgetType; widgetTypes = widgetType;
dataKeyTypes = DataKeyType; dataKeyTypes = DataKeyType;
keysListFormGroup: UntypedFormGroup; keysListFormGroup: FormGroup;
modelValue: Array<DataKey> | null; modelValue: Array<DataKey> | null;
@ -218,23 +215,21 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
private dirty = false; private dirty = false;
private propagateChange = (v: any) => { }; private propagateChange = (_v: any) => { };
private keysRequired = this._keysRequired.bind(this); private keysRequired = this._keysRequired.bind(this);
private keysValidator = this._keysValidator.bind(this); private keysValidator = this._keysValidator.bind(this);
constructor(private store: Store<AppState>, constructor(@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
private datasourceComponent: DatasourceComponent, private datasourceComponent: DatasourceComponent,
public translate: TranslateService, private translate: TranslateService,
private utils: UtilsService, private utils: UtilsService,
private dialog: MatDialog, private dialog: MatDialog,
private fb: UntypedFormBuilder, private fb: FormBuilder,
private cd: ChangeDetectorRef,
private popoverService: TbPopoverService, private popoverService: TbPopoverService,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private renderer: Renderer2, private renderer: Renderer2,
public truncate: TruncatePipe) { private destroyRef: DestroyRef) {
} }
updateValidators() { updateValidators() {
@ -246,7 +241,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
this.keysListFormGroup.get('keys').updateValueAndValidity(); this.keysListFormGroup.get('keys').updateValueAndValidity();
} }
private _keysRequired(control: AbstractControl): ValidationErrors | null { private _keysRequired(_control: AbstractControl): ValidationErrors | null {
const value = this.modelValue; const value = this.modelValue;
if (value && Array.isArray(value) && value.length) { if (value && Array.isArray(value) && value.length) {
return null; return null;
@ -255,7 +250,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
} }
} }
private _keysValidator(control: AbstractControl): ValidationErrors | null { private _keysValidator(_control: AbstractControl): ValidationErrors | null {
const value = this.modelValue; const value = this.modelValue;
if (value && Array.isArray(value)) { if (value && Array.isArray(value)) {
if (value.some(v => isObject(v) && (!v.type || !v.name))) { if (value.some(v => isObject(v) && (!v.type || !v.name))) {
@ -274,7 +269,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
} }
} }
registerOnTouched(fn: any): void { registerOnTouched(_fn: any): void {
} }
ngOnInit() { ngOnInit() {
@ -314,6 +309,15 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
mergeMap(name => this.fetchKeys(name) ), mergeMap(name => this.fetchKeys(name) ),
share() share()
); );
this.aliasController.entityAliasesChanged.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(aliasIds => {
if (aliasIds.includes(this.entityAliasId)) {
this.clearSearchCache();
this.dirty = true;
}
})
} }
public maxDataKeysText(): string { public maxDataKeysText(): string {
@ -413,7 +417,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
} }
} }
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = this.keysListFormGroup.get('keys').hasError('dataKey'); const customErrorState = this.keysListFormGroup.get('keys').hasError('dataKey');
return originalErrorState || customErrorState; return originalErrorState || customErrorState;
@ -442,7 +446,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
this.dirty = true; this.dirty = true;
} }
validate(c: UntypedFormControl) { validate(_c: FormControl) {
return (this.keysListFormGroup.get('keys').hasError('dataKey')) ? { return (this.keysListFormGroup.get('keys').hasError('dataKey')) ? {
dataKeys: { dataKeys: {
valid: false, valid: false,
@ -637,8 +641,12 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
fetchObservable = of([]); fetchObservable = of([]);
} }
this.fetchObservable$ = fetchObservable.pipe( this.fetchObservable$ = fetchObservable.pipe(
publishReplay(1), share({
refCount() connector: () => new ReplaySubject(1),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: false
})
); );
} }
return this.fetchObservable$; return this.fetchObservable$;
@ -650,7 +658,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
} }
textIsNotEmpty(text: string): boolean { textIsNotEmpty(text: string): boolean {
return text && text.length > 0; return text?.length > 0;
} }
clear(value: string = '', focus = true) { clear(value: string = '', focus = true) {

View File

@ -165,6 +165,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
widgetConfigCallbacks: WidgetConfigCallbacks = { widgetConfigCallbacks: WidgetConfigCallbacks = {
createEntityAlias: this.createEntityAlias.bind(this), createEntityAlias: this.createEntityAlias.bind(this),
editEntityAlias: this.editEntityAlias.bind(this),
createFilter: this.createFilter.bind(this), createFilter: this.createFilter.bind(this),
generateDataKey: this.generateDataKey.bind(this), generateDataKey: this.generateDataKey.bind(this),
fetchEntityKeysForDevice: this.fetchEntityKeysForDevice.bind(this), fetchEntityKeysForDevice: this.fetchEntityKeysForDevice.bind(this),
@ -841,6 +842,27 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
); );
} }
private editEntityAlias(alias: EntityAlias, allowedEntityTypes: Array<EntityType>): Observable<EntityAlias> {
return this.dialog.open<EntityAliasDialogComponent, EntityAliasDialogData,
EntityAlias>(EntityAliasDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
isAdd: false,
allowedEntityTypes,
entityAliases: this.dashboard.configuration.entityAliases,
alias: deepClone(alias)
}
}).afterClosed().pipe(
tap((entityAlias) => {
if (entityAlias) {
this.dashboard.configuration.entityAliases[entityAlias.id] = entityAlias;
this.aliasController.updateEntityAliases(this.dashboard.configuration.entityAliases);
}
})
);
}
private createFilter(filter: string): Observable<Filter> { private createFilter(filter: string): Observable<Filter> {
const singleFilter: Filter = {id: null, filter, keyFilters: [], editable: true}; const singleFilter: Filter = {id: null, filter, keyFilters: [], editable: true};
return this.dialog.open<FilterDialogComponent, FilterDialogData, return this.dialog.open<FilterDialogComponent, FilterDialogData,