thingsboard/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts
2025-09-24 11:34:03 +03:00

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);
}
}