372 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			372 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
///
 | 
						|
/// Copyright © 2016-2025 The Thingsboard Authors
 | 
						|
///
 | 
						|
/// Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
/// you may not use this file except in compliance with the License.
 | 
						|
/// You may obtain a copy of the License at
 | 
						|
///
 | 
						|
///     http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
///
 | 
						|
/// Unless required by applicable law or agreed to in writing, software
 | 
						|
/// distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
/// See the License for the specific language governing permissions and
 | 
						|
/// limitations under the License.
 | 
						|
///
 | 
						|
 | 
						|
import {
 | 
						|
  AfterViewInit,
 | 
						|
  ChangeDetectorRef,
 | 
						|
  Component,
 | 
						|
  ElementRef,
 | 
						|
  forwardRef,
 | 
						|
  Input,
 | 
						|
  NgZone,
 | 
						|
  OnDestroy,
 | 
						|
  OnInit,
 | 
						|
  SecurityContext,
 | 
						|
  ViewChild
 | 
						|
} from '@angular/core';
 | 
						|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
 | 
						|
import { TranslateService } from '@ngx-translate/core';
 | 
						|
import { PageComponent } from '@shared/components/page.component';
 | 
						|
import { MatDialog } from '@angular/material/dialog';
 | 
						|
import { DialogService } from '@core/services/dialog.service';
 | 
						|
import { PageLink } from '@shared/models/page/page-link';
 | 
						|
import { Direction, SortOrder } from '@shared/models/page/sort-order';
 | 
						|
import { MatPaginator } from '@angular/material/paginator';
 | 
						|
import { MatSort } from '@angular/material/sort';
 | 
						|
import { fromEvent, merge } from 'rxjs';
 | 
						|
import { debounceTime, distinctUntilChanged, first, tap } from 'rxjs/operators';
 | 
						|
import {
 | 
						|
  toWidgetActionDescriptor,
 | 
						|
  WidgetActionCallbacks,
 | 
						|
  WidgetActionDescriptorInfo,
 | 
						|
  WidgetActionsData,
 | 
						|
  WidgetActionsDatasource
 | 
						|
} from '@home/components/widget/action/manage-widget-actions.component.models';
 | 
						|
import { UtilsService } from '@core/services/utils.service';
 | 
						|
import { WidgetActionDescriptor, WidgetActionSource, WidgetActionType, widgetType } from '@shared/models/widget.models';
 | 
						|
import {
 | 
						|
  WidgetActionDialogComponent,
 | 
						|
  WidgetActionDialogData
 | 
						|
} from '@home/components/widget/action/widget-action-dialog.component';
 | 
						|
import { deepClone } from '@core/utils';
 | 
						|
import { hidePageSizePixelValue } from '@shared/models/constants';
 | 
						|
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
 | 
						|
import { DomSanitizer } from '@angular/platform-browser';
 | 
						|
 | 
						|
@Component({
 | 
						|
  selector: 'tb-manage-widget-actions',
 | 
						|
  templateUrl: './manage-widget-actions.component.html',
 | 
						|
  styleUrls: ['./manage-widget-actions.component.scss'],
 | 
						|
  providers: [
 | 
						|
    {
 | 
						|
      provide: NG_VALUE_ACCESSOR,
 | 
						|
      useExisting: forwardRef(() => ManageWidgetActionsComponent),
 | 
						|
      multi: true
 | 
						|
    }
 | 
						|
  ]
 | 
						|
})
 | 
						|
export class ManageWidgetActionsComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy, ControlValueAccessor {
 | 
						|
 | 
						|
  @Input() disabled: boolean;
 | 
						|
 | 
						|
  @Input() widgetType: widgetType;
 | 
						|
 | 
						|
  @Input() defaultIconColor: string;
 | 
						|
 | 
						|
  @Input() callbacks: WidgetActionCallbacks;
 | 
						|
 | 
						|
  @Input() actionSources: {[actionSourceId: string]: WidgetActionSource};
 | 
						|
 | 
						|
  @Input() additionalWidgetActionTypes: WidgetActionType[];
 | 
						|
 | 
						|
  displayedColumns: string[];
 | 
						|
  pageLink: PageLink;
 | 
						|
  textSearchMode = false;
 | 
						|
  hidePageSize = false;
 | 
						|
  dataSource: WidgetActionsDatasource;
 | 
						|
  dragDisabled = true;
 | 
						|
 | 
						|
  private actionsMap: {[actionSourceId: string]: Array<WidgetActionDescriptor>};
 | 
						|
  private viewsInited = false;
 | 
						|
  private dirtyValue = false;
 | 
						|
  private widgetResize$: ResizeObserver;
 | 
						|
  private destroyed = false;
 | 
						|
 | 
						|
  @ViewChild('searchInput') searchInputField: ElementRef;
 | 
						|
 | 
						|
  @ViewChild(MatPaginator) paginator: MatPaginator;
 | 
						|
  @ViewChild(MatSort) sort: MatSort;
 | 
						|
 | 
						|
  private propagateChange = (_: any) => {};
 | 
						|
 | 
						|
  constructor(private translate: TranslateService,
 | 
						|
              private utils: UtilsService,
 | 
						|
              private dialog: MatDialog,
 | 
						|
              private dialogs: DialogService,
 | 
						|
              private cd: ChangeDetectorRef,
 | 
						|
              private elementRef: ElementRef,
 | 
						|
              private zone: NgZone,
 | 
						|
              private sanitizer: DomSanitizer) {
 | 
						|
    super();
 | 
						|
    const sortOrder: SortOrder = { property: 'actionSourceName', direction: Direction.ASC };
 | 
						|
    this.pageLink = new PageLink(10, 0, null, sortOrder);
 | 
						|
    this.dataSource = new WidgetActionsDatasource(this.translate, this.utils);
 | 
						|
    this.displayedColumns = ['actionSourceId', 'actionSourceName', 'name', 'icon', 'typeName', 'actions'];
 | 
						|
  }
 | 
						|
 | 
						|
  ngOnInit(): void {
 | 
						|
    this.widgetResize$ = new ResizeObserver(() => {
 | 
						|
      this.zone.run(() => {
 | 
						|
        const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
 | 
						|
        if (showHidePageSize !== this.hidePageSize) {
 | 
						|
          this.hidePageSize = showHidePageSize;
 | 
						|
          this.cd.markForCheck();
 | 
						|
        }
 | 
						|
      });
 | 
						|
    });
 | 
						|
    this.widgetResize$.observe(this.elementRef.nativeElement);
 | 
						|
  }
 | 
						|
 | 
						|
  ngOnDestroy(): void {
 | 
						|
    this.destroyed = true;
 | 
						|
    if (this.widgetResize$) {
 | 
						|
      this.widgetResize$.disconnect();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  ngAfterViewInit() {
 | 
						|
    fromEvent(this.searchInputField.nativeElement, 'keyup')
 | 
						|
      .pipe(
 | 
						|
        debounceTime(150),
 | 
						|
        distinctUntilChanged(),
 | 
						|
        tap(() => {
 | 
						|
          this.paginator.pageIndex = 0;
 | 
						|
          this.updateData();
 | 
						|
        })
 | 
						|
      )
 | 
						|
      .subscribe();
 | 
						|
 | 
						|
    this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
 | 
						|
 | 
						|
    merge(this.sort.sortChange, this.paginator.page)
 | 
						|
      .pipe(
 | 
						|
        tap(() => this.updateData())
 | 
						|
      )
 | 
						|
      .subscribe();
 | 
						|
 | 
						|
    this.viewsInited = true;
 | 
						|
    if (this.dirtyValue) {
 | 
						|
      this.dirtyValue = false;
 | 
						|
      this.updateData(true);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  private updateData(reload: boolean = false) {
 | 
						|
    this.pageLink.page = this.paginator.pageIndex;
 | 
						|
    this.pageLink.pageSize = this.paginator.pageSize;
 | 
						|
    this.pageLink.sortOrder.property = this.sort.active;
 | 
						|
    this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
 | 
						|
    this.dataSource.loadActions(this.pageLink, reload);
 | 
						|
  }
 | 
						|
 | 
						|
  dropAction(event: CdkDragDrop<WidgetActionsDatasource>) {
 | 
						|
    this.dragDisabled = true;
 | 
						|
    const droppedAction: WidgetActionDescriptorInfo = event.item.data;
 | 
						|
    this.dataSource.pageData$.pipe(
 | 
						|
      first()
 | 
						|
    ).subscribe((actions) => {
 | 
						|
      const action = actions.data;
 | 
						|
      let startActionSourceIndex = action.findIndex(element => element.actionSourceId === droppedAction.actionSourceId);
 | 
						|
      const targetActions = this.getOrCreateTargetActions(droppedAction.actionSourceId);
 | 
						|
      if (startActionSourceIndex === 0) {
 | 
						|
        startActionSourceIndex -= targetActions.findIndex(element => element.id === action[0].id);
 | 
						|
      }
 | 
						|
      moveItemInArray(targetActions, event.previousIndex - startActionSourceIndex, event.currentIndex - startActionSourceIndex);
 | 
						|
      this.onActionsUpdated();
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  addAction($event: Event) {
 | 
						|
    this.openWidgetActionDialog($event);
 | 
						|
  }
 | 
						|
 | 
						|
  editAction($event: Event, action: WidgetActionDescriptorInfo) {
 | 
						|
    this.openWidgetActionDialog($event, action);
 | 
						|
  }
 | 
						|
 | 
						|
  private openWidgetActionDialog($event: Event, action: WidgetActionDescriptorInfo = null) {
 | 
						|
    if ($event) {
 | 
						|
      $event.stopPropagation();
 | 
						|
    }
 | 
						|
    const isAdd = action === null;
 | 
						|
    let prevActionSourceId = null;
 | 
						|
    if (!isAdd) {
 | 
						|
      prevActionSourceId = action.actionSourceId;
 | 
						|
    }
 | 
						|
    const availableActionSources: {[actionSourceId: string]: WidgetActionSource} = {};
 | 
						|
    for (const id of Object.keys(this.actionSources)) {
 | 
						|
      const actionSource = this.actionSources[id];
 | 
						|
      if (actionSource.multiple) {
 | 
						|
        availableActionSources[id] = actionSource;
 | 
						|
      } else {
 | 
						|
        if (!isAdd && action.actionSourceId === id) {
 | 
						|
          availableActionSources[id] = actionSource;
 | 
						|
        } else {
 | 
						|
          const existing = this.actionsMap[id];
 | 
						|
          if (!existing || !existing.length) {
 | 
						|
            availableActionSources[id] = actionSource;
 | 
						|
          }
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    const actionsData: WidgetActionsData = {
 | 
						|
      actionsMap: this.actionsMap,
 | 
						|
      actionSources: availableActionSources
 | 
						|
    };
 | 
						|
 | 
						|
    this.dialog.open<WidgetActionDialogComponent, WidgetActionDialogData,
 | 
						|
      WidgetActionDescriptorInfo>(WidgetActionDialogComponent, {
 | 
						|
      disableClose: true,
 | 
						|
      panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
 | 
						|
      data: {
 | 
						|
        isAdd,
 | 
						|
        callbacks: this.callbacks,
 | 
						|
        actionsData,
 | 
						|
        action: deepClone(action),
 | 
						|
        widgetType: this.widgetType,
 | 
						|
        defaultIconColor: this.defaultIconColor,
 | 
						|
        additionalWidgetActionTypes: this.additionalWidgetActionTypes
 | 
						|
      }
 | 
						|
    }).afterClosed().subscribe(
 | 
						|
      (res) => {
 | 
						|
        if (res) {
 | 
						|
          this.saveAction(res, isAdd, prevActionSourceId);
 | 
						|
        }
 | 
						|
      }
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  private saveAction(actionInfo: WidgetActionDescriptorInfo, isAdd: boolean, prevActionSourceId: string) {
 | 
						|
    const actionSourceId = actionInfo.actionSourceId;
 | 
						|
    const action = toWidgetActionDescriptor(actionInfo);
 | 
						|
    if (isAdd) {
 | 
						|
      const targetActions = this.getOrCreateTargetActions(actionSourceId);
 | 
						|
      targetActions.push(action);
 | 
						|
    } else {
 | 
						|
      if (actionSourceId !== prevActionSourceId) {
 | 
						|
        let targetActions = this.getOrCreateTargetActions(prevActionSourceId);
 | 
						|
        const targetIndex = targetActions.findIndex((targetAction) => targetAction.id === action.id);
 | 
						|
        if (targetIndex > -1) {
 | 
						|
          targetActions.splice(targetIndex, 1);
 | 
						|
        }
 | 
						|
        targetActions = this.getOrCreateTargetActions(actionSourceId);
 | 
						|
        targetActions.push(action);
 | 
						|
      } else {
 | 
						|
        const targetActions = this.getOrCreateTargetActions(actionSourceId);
 | 
						|
        const targetIndex = targetActions.findIndex((targetAction) => targetAction.id === action.id);
 | 
						|
        if (targetIndex > -1) {
 | 
						|
          targetActions[targetIndex] = action;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
    this.onActionsUpdated();
 | 
						|
  }
 | 
						|
 | 
						|
  private getOrCreateTargetActions(actionSourceId: string): Array<WidgetActionDescriptor> {
 | 
						|
    const actionsMap = this.actionsMap;
 | 
						|
    let targetActions = actionsMap[actionSourceId];
 | 
						|
    if (!targetActions) {
 | 
						|
      targetActions = [];
 | 
						|
      actionsMap[actionSourceId] = targetActions;
 | 
						|
    }
 | 
						|
    return targetActions;
 | 
						|
  }
 | 
						|
 | 
						|
  deleteAction($event: Event, action: WidgetActionDescriptorInfo) {
 | 
						|
    if ($event) {
 | 
						|
      $event.stopPropagation();
 | 
						|
    }
 | 
						|
    const title = this.translate.instant('widget-config.delete-action-title');
 | 
						|
    const content = this.translate.instant('widget-config.delete-action-text', {actionName: action.name});
 | 
						|
    const safeContent = this.sanitizer.sanitize(SecurityContext.HTML, content);
 | 
						|
    this.dialogs.confirm(title, safeContent,
 | 
						|
      this.translate.instant('action.no'),
 | 
						|
      this.translate.instant('action.yes'), true).subscribe(
 | 
						|
      (res) => {
 | 
						|
        if (res) {
 | 
						|
          const targetActions = this.getOrCreateTargetActions(action.actionSourceId);
 | 
						|
          const targetIndex = targetActions.findIndex((targetAction) => targetAction.id === action.id);
 | 
						|
          if (targetIndex > -1) {
 | 
						|
            targetActions.splice(targetIndex, 1);
 | 
						|
            this.onActionsUpdated();
 | 
						|
          }
 | 
						|
        }
 | 
						|
      });
 | 
						|
  }
 | 
						|
 | 
						|
  enterFilterMode() {
 | 
						|
    this.textSearchMode = true;
 | 
						|
    this.pageLink.textSearch = '';
 | 
						|
    setTimeout(() => {
 | 
						|
      this.searchInputField.nativeElement.focus();
 | 
						|
      this.searchInputField.nativeElement.setSelectionRange(0, 0);
 | 
						|
    }, 10);
 | 
						|
  }
 | 
						|
 | 
						|
  exitFilterMode() {
 | 
						|
    this.textSearchMode = false;
 | 
						|
    this.pageLink.textSearch = null;
 | 
						|
    this.paginator.pageIndex = 0;
 | 
						|
    this.updateData();
 | 
						|
  }
 | 
						|
 | 
						|
  private resetSortAndFilter(update: boolean = true) {
 | 
						|
    this.pageLink.textSearch = null;
 | 
						|
    this.paginator.pageIndex = 0;
 | 
						|
    const sortable = this.sort.sortables.get('actionSourceName');
 | 
						|
    this.sort.active = sortable.id;
 | 
						|
    this.sort.direction = 'asc';
 | 
						|
    if (update) {
 | 
						|
      this.updateData(true);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  registerOnChange(fn: any): void {
 | 
						|
    this.propagateChange = fn;
 | 
						|
  }
 | 
						|
 | 
						|
  registerOnTouched(_fn: any): void {
 | 
						|
  }
 | 
						|
 | 
						|
  setDisabledState(isDisabled: boolean): void {
 | 
						|
    this.disabled = isDisabled;
 | 
						|
  }
 | 
						|
 | 
						|
  writeValue(actions?: {[actionSourceId: string]: Array<WidgetActionDescriptor>}): void {
 | 
						|
    this.actionsMap = actions ?? {};
 | 
						|
    setTimeout(() => {
 | 
						|
      if (!this.destroyed) {
 | 
						|
        const actionData: WidgetActionsData = {
 | 
						|
          actionsMap: this.actionsMap,
 | 
						|
          actionSources: this.actionSources
 | 
						|
        };
 | 
						|
        this.dataSource.setActions(actionData);
 | 
						|
        if (this.viewsInited) {
 | 
						|
          this.resetSortAndFilter(true);
 | 
						|
        } else {
 | 
						|
          this.dirtyValue = true;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }, 0);
 | 
						|
  }
 | 
						|
 | 
						|
  private onActionsUpdated() {
 | 
						|
    this.updateData(true);
 | 
						|
    this.propagateChange(this.actionsMap);
 | 
						|
  }
 | 
						|
}
 |