import {
WidgetComponent,
WidgetConfiguration,
WidgetConfigure,
WidgetId,
WidgetInput,
WidgetOutput,
} from "../widget.metadata";
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Input,
OnDestroy,
ViewChild,
} from "@angular/core";
import { getOrDefault, WidgetConfig } from "../widget.configuration";
import {
concatAll,
concatMap,
filter,
map,
mergeAll,
mergeMap,
take,
takeUntil,
throttleTime,
} from "rxjs/operators";
import { AppdataStore } from "../../components/appdata/appdata.store";
import { WidgetframeService } from "../widgetframe/widgetframe.service";
import { CustomNotificationService } from "../../components/notification/customnotification.service";
import { HalService } from "../../components/hal/hal.service";
import { MatDialog } from "@angular/material/dialog";
import { MatMenuTrigger } from "@angular/material/menu";
import { CreateFolderEntryDialogComponent } from "./worklist-create-dialog/create-folder-entry-dialog.component";
import {
Subject,
Observable,
ReplaySubject,
from,
of,
combineLatest,
} from "rxjs";
import { NgUnsubscribe } from "../../ng-unsubscribe";
import { AppContext } from "../../components/app-context/app.context";
import { BaseConfiguration } from "../widgetframe/widgetframe.component";
import { Content, Selectors } from "../../components/app-context/api";
import * as uriTemplates_ from "uri-templates";
import { TranslateService } from "@ngx-translate/core";
import { TreeSelectorComponent } from "../../components/tree-selector/tree-selector.component";
import { Action } from "../../components/hal/action";
import { ActionEvent } from "../../components/hal/hal";
import { CurrentLocaleService } from "../../components/i18n/currentLocale.service";
import { NodeLoader } from "./node-loader";
import {
OnDragEvent,
OnDropEvent,
OnDroppedEvent,
} from "../../components/tree-selector/tree-selector.api";
import { HttpResponse } from "@angular/common/http";
import { DialogService } from "../../components/dialog";
const uriTemplates = uriTemplates_;
export interface WorklistSelectConfiguration extends BaseConfiguration {
allowDrag: boolean;
multi: boolean;
emitElementMoved: boolean;
localStorageState: string;
localStorageSearch: string;
listFolderType: string;
elementsDataType: string;
staticFolders: any[];
refreshElementsActions: any[];
addElementConfig: any;
showFolderActions: boolean;
showElementIcon: boolean;
emitOnFolderSelect: boolean;
disableDeselect: boolean;
footer: any;
loadElements: boolean;
loadElementsUrl: string;
showActions: boolean;
isFolderHierarchy: boolean;
loadFoldersUrl: string;
identifierField: string;
descriptionField: string;
localStateRecovery: boolean;
showStaticFoldersOnly: boolean;
height?: string;
dynamicHeight?: boolean;
dynamicTreeHeightInDialog?: boolean;
dynamicHeightAdditionalHeight?: string;
selectors: {
folderMenu?: Selectors;
folderIcons?: Selectors;
nodeMenu?: Selectors;
nodeIcons?: Selectors;
// fallback, used, when the more specific selectors are not defined
menu: Selectors;
icons: Selectors;
};
cellClass?: string;
cellClassModifier?: string;
infoWidth: string;
infoHeight: string;
focusSearchInput?: boolean;
}
const DEFAULT_ADD_ELEMENT_CONFIG = {
showAddElementIcon: true,
labelTooltip: "infotext.create.worklist.or.folder",
availableOptions: [
{
actionsHref: "/api/core/worklists",
description: "worklist",
listFolderType: "WORKLIST",
},
{
actionsHref: "/api/core/listfolders/WORKLIST",
description: "folder",
listFolderType: "WORKLIST",
},
],
};
@WidgetComponent("nm-worklist-select")
@Component({
selector: "nm-worklist-select",
templateUrl: "./worklist-select.component.html",
styleUrls: ["./worklist-select.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class WorklistSelectComponentWidget implements OnDestroy {
private unsubscribe = NgUnsubscribe.create();
@ViewChild(TreeSelectorComponent, { static: true })
private treeSelector: TreeSelectorComponent;
@WidgetInput()
public disableElementAction = new Subject<{
node: string;
action: string;
value: boolean;
}>();
@WidgetId()
public widgetId: string;
@WidgetInput()
public removeNodeSelection = new Subject<boolean>();
@WidgetInput()
public updateElements = new Subject<any>();
@WidgetInput()
public updateFolders = new Subject<any>();
@WidgetInput()
public reloadElements = new Subject<boolean>();
@WidgetInput("disableAddNewElementButton")
public disableAddNewElementButtonChannel = new Subject<boolean>();
@WidgetOutput()
public listSelected = new Subject<number>();
@WidgetOutput("elementSelected")
public elementSelected = new ReplaySubject<any>(1);
@WidgetOutput()
public reset = new Subject();
@WidgetOutput()
public addElementAction = new Subject();
@WidgetOutput()
public changeElementStateAction = new Subject<any>();
@WidgetInput()
public selectItem = new Subject<any>();
@WidgetOutput()
public elementMoved = new Subject<any>();
@WidgetOutput()
public editClicked = new Subject<any>();
/** Add new element configuration */
public addElementConfig: any;
/** Element selector configuration */
public multi: boolean;
public emitElementMoved: boolean;
public selectInitialized = true;
public localStorageState: string;
public localStorageSearch: string;
public listFolderType: string;
public elementsDataType: string;
public height: string;
public staticFolders: any[];
public refreshElementsActions: any[];
public availableActions: any[] = [];
public showFolderActions: boolean;
public showElementIcon: boolean;
public forcedElementSelection;
public emitOnFolderSelect: boolean;
public disableDeselect: boolean;
public loadElements: boolean;
public isFolderHierarchy: boolean;
public identifierField: string;
public descriptionField: string;
public localStateRecovery: boolean;
public showStaticFoldersOnly: boolean;
public dynamicHeight: boolean = false;
public dynamicTreeHeightInDialog: boolean = false;
public dynamicHeightAdditionalHeight: string;
public focusSearchInput: boolean;
public folderMenuSelectors: Selectors;
public folderIconsSelectors: Selectors;
public nodeMenuSelectors: Selectors;
public nodeIconsSelectors: Selectors;
/** Footer configuration */
public footer: any;
public withHeader: boolean;
public showActions: boolean;
public actions: any[];
public nodes: any[];
public disableAddNewElementButton: boolean = false;
private nodeLoader: NodeLoader;
private originalName: String;
constructor(
private dialogService: DialogService,
private halService: HalService,
private notificationService: CustomNotificationService,
private appstore: AppdataStore,
private widgetFrameService: WidgetframeService,
private cd: ChangeDetectorRef,
private appContext: AppContext,
private translateService: TranslateService,
private currentLocaleService: CurrentLocaleService
) {
this.nodeLoader = new NodeLoader(
translateService,
appstore,
widgetFrameService
);
this.addActions();
}
createAction(name, icon, label, labelFromAction, isDisableable) {
return {
name: name,
icon: icon,
label: label,
labelFromAction: labelFromAction,
isDisableable: isDisableable,
};
}
addActions() {
this.actions = [];
this.actions.push(
this.createAction("refresh", "refresh", "label.refresh", false, false)
);
this.actions.push(
this.createAction(
"duplicate",
"content-duplicate",
"label.duplicate",
false,
false
)
);
this.actions.push(
this.createAction(
"checkLogging",
"read",
"label.checkLogging",
false,
false
)
);
this.actions.push(
this.createAction("import", "file-import", null, true, false)
);
this.actions.push(
this.createAction("export", "file-export", null, true, false)
);
this.actions.push(
this.createAction("translate", "translate", null, true, false)
);
this.actions.push(
this.createAction(
"add-to-dashboard",
"view-dashboard-outline",
null,
true,
false
)
);
this.actions.push(
this.createAction(
"remove-from-dashboard",
"view-dashboard",
null,
true,
false
)
);
}
@WidgetConfigure()
widgetConfigure() {
this.multi = getOrDefault(this.configuration.configuration.multi, false);
this.emitElementMoved = getOrDefault(
this.configuration.configuration.emitElementMoved,
false
);
this.localStorageState = getOrDefault(
this.configuration.configuration.localStorageState,
"worklist-state-storage"
);
this.localStorageSearch = getOrDefault(
this.configuration.configuration.localStorageSearch,
"worklist-search-string"
);
this.listFolderType = getOrDefault(
this.configuration.configuration.listFolderType,
"WORKLIST"
);
this.elementsDataType = getOrDefault(
this.configuration.configuration.elementsDataType,
"worklists"
);
this.showFolderActions = getOrDefault(
this.configuration.configuration.showFolderActions,
true
);
this.showElementIcon = getOrDefault(
this.configuration.configuration.showElementIcon,
true
);
this.emitOnFolderSelect = getOrDefault(
this.configuration.configuration.emitOnFolderSelect,
true
);
this.disableDeselect = getOrDefault(
this.configuration.configuration.disableDeselect,
false
);
this.staticFolders = getOrDefault(
this.configuration.configuration.staticFolders,
NodeLoader.defaultStaticFolders()
);
this.refreshElementsActions = getOrDefault(
this.configuration.configuration.refreshElementsActions,
this.getDefaultRefreshActions()
);
this.addElementConfig = Object.assign(
{},
DEFAULT_ADD_ELEMENT_CONFIG,
this.configuration.configuration.addElementConfig || {}
);
this.footer = getOrDefault(this.configuration.configuration.footer, null);
this.showStaticFoldersOnly = getOrDefault(
this.configuration.configuration.showStaticFoldersOnly,
false
);
this.selectItem
.pipe(takeUntil(this.unsubscribe))
.subscribe((data) => (this.forcedElementSelection = data));
this.withHeader = getOrDefault(
this.configuration.configuration.withHeader,
true
);
this.showActions = getOrDefault(
this.configuration.configuration.showActions,
true
);
this.loadElements = getOrDefault(
this.configuration.configuration.loadElements,
false
);
this.isFolderHierarchy = getOrDefault(
this.configuration.configuration.isFolderHierarchy,
false
);
this.identifierField = getOrDefault(
this.configuration.configuration.identifierField,
"identifier"
);
this.descriptionField = getOrDefault(
this.configuration.configuration.descriptionField,
"description"
);
this.localStateRecovery = getOrDefault(
this.configuration.configuration.localStateRecovery,
true
);
this.dynamicHeight = getOrDefault(
this.configuration.configuration.dynamicHeight,
false
);
this.dynamicTreeHeightInDialog = getOrDefault(
this.configuration.configuration.dynamicTreeHeightInDialog,
false
);
this.dynamicHeightAdditionalHeight = getOrDefault(
this.configuration.configuration.dynamicHeightAdditionalHeight,
""
);
this.height = getOrDefault(
this.configuration.configuration.height,
"650px"
);
const selectors: any = this.configuration.configuration.selectors || {};
this.folderMenuSelectors = getOrDefault(
selectors.folderMenu || selectors.menu,
{
target: ["nm-worklist-select", "nm-worklist-select-folder"],
type: "menu",
}
);
this.folderIconsSelectors = getOrDefault(
selectors.folderIcons || selectors.icons,
{
target: ["nm-worklist-select", "nm-worklist-select-folder"],
type: "icons",
}
);
this.nodeMenuSelectors = getOrDefault(
selectors.nodeMenu || selectors.menu,
{
target: ["nm-worklist-select", "nm-worklist-select-node"],
type: "menu",
}
);
this.nodeIconsSelectors = getOrDefault(
selectors.nodeIcons || selectors.icons,
{
target: ["nm-worklist-select", "nm-worklist-select-node"],
type: "icons",
}
);
this.focusSearchInput = getOrDefault(
this.configuration.configuration.focusSearchInput,
false
);
this.removeNodeSelection
.pipe(takeUntil(this.unsubscribe))
.subscribe((event) => this.treeSelector.clearSelection());
this.updateElements.pipe(takeUntil(this.unsubscribe)).subscribe((node) => {
const mapped = this.nodeLoader.mapEntry(
node.value,
this.identifierField,
this.descriptionField
);
this.treeSelector.updateNode(mapped.id, mapped);
});
this.updateFolders.pipe(takeUntil(this.unsubscribe)).subscribe((node) => {
const mapped = this.nodeLoader.mapFolder(node.value);
this.treeSelector.updateNode(mapped.id, mapped);
});
let sources = [
//
of(),
this.currentLocaleService.getCurrentLocale(),
this.reloadElements,
this.halService
.getActionEvents()
.pipe(filter((event) => event.name === "duplicate")),
];
// trigger re-load, if the configured selectors
// are triggered via the userContext
// (used to reload after adding a folder or worklist)
if (
this.refreshElementsActions &&
this.appContext &&
this.appContext.userContext
) {
this.refreshElementsActions.forEach((selector) => {
sources.push(this.appContext.userContext.subscribe(selector));
});
}
from(sources)
.pipe(mergeAll(), takeUntil(this.unsubscribe), throttleTime(500))
.subscribe(() => this.loadNodes());
this.disableAddNewElementButtonChannel
.asObservable()
.pipe(takeUntil(this.unsubscribe))
.subscribe((disabled) => {
this.disableAddNewElementButton = disabled;
this.cd.markForCheck();
});
}
private loadNodes() {
if (this.showStaticFoldersOnly) {
combineLatest([
this.nodeLoader.staticFolders(this.staticFolders),
this.nodeLoader.elements(
this.configuration.configuration.loadElementsUrl,
this.elementsDataType,
this.identifierField,
this.descriptionField
),
])
.pipe(
map(([folders, elements]) => [...folders.nodes, ...elements.nodes])
)
.subscribe((nodes) => {
this.nodes = nodes;
this.cd.markForCheck();
});
return;
}
this.nodeLoader
.nodes(
this.staticFolders,
this.isFolderHierarchy
? this.configuration.configuration.loadFoldersUrl
: null,
this.listFolderType,
this.configuration.configuration.loadElementsUrl,
this.elementsDataType,
this.identifierField,
this.descriptionField
)
.subscribe((nodes) => {
this.nodes = nodes;
this.cd.markForCheck();
});
}
public isNode(index, row: any): boolean {
return row.isNode === true;
}
public isStatic(index, row: any): boolean {
return row.isStatic === true;
}
public isStaticChild(index, row: any): boolean {
return row.isNode === true && row.parent === "dynamic-worklists";
}
public isFolder(index, row: any): boolean {
return row.isFolder === true;
}
public onEdit(data: any) {
// save the original name to prevent executing actions, if nothing changed
this.originalName = data.name;
// use the inline edit mode unless explicitly set in the node
if (data.editMode && data.editMode !== "inline") {
this.editClicked.next(data.id);
} else {
this.treeSelector.edit(data.id);
}
}
public onEdited(data: any) {
let trimmedName = data.name.trim();
if (!trimmedName) {
let errorTitle = this.translateService.instant(
"worklist-select.edit.error.title"
);
let errorMsg = this.translateService.instant(
"worklist-select.edit.error.message"
);
this.notificationService.error(errorTitle, errorMsg);
data.name = this.originalName;
this.reload(data);
return;
}
if (this.originalName === trimmedName) {
return;
}
if (data.isFolder) {
const action = data.actions.update;
action.payload = {
name: trimmedName,
};
this.halService.executeAndShowMessage("rename", action).subscribe();
return;
}
const action = data.actions.update;
action.payload.description = trimmedName;
this.halService.executeAndShowMessage("rename", action).subscribe();
}
public onDrag(event: OnDragEvent) {
event.cancel =
this.isStatic(null, event.dragged) ||
this.isStaticChild(null, event.dragged);
}
public onDrop(event: OnDropEvent) {
if (
this.isStatic(null, event.target) ||
this.isStaticChild(null, event.target)
) {
event.cancel = true;
} else if (event.target.isNode) {
event.dropAsChild = false;
}
}
public onDropped(node, event: OnDroppedEvent) {
if (this.emitElementMoved) {
this.elementMoved.next(event);
return;
}
const action = event.node.actions.update;
if (event.node.isFolder) {
const parent = event.to.parent?.folder ? event.to.parent.id : "-1";
action.payload = {
parent: parent,
};
this.halService.executeAndShowMessage("move", action).subscribe();
} else {
if (event.to.parent?.folder) {
action.payload.listFolder = event.to.parent.id;
event.node.root = false;
} else {
action.payload.listFolder = null;
event.node.root = true;
}
this.halService.executeAndShowMessage("move", action).subscribe();
}
}
onDeleteClick(node, action) {
if (action.confirmed) {
// trigger delete action, _without_ reloading...
this.executeAction("delete", action, node, false).subscribe((event) => {
this.notificationService.fromResponse(event.response);
this.treeSelector.removeNode(node.id);
});
} else {
action.confirmed = true;
setTimeout(() => {
action.confirmed = false;
this.cd.markForCheck();
}, 3000);
}
}
public executeActionAndShowMessage(
name: string,
action: Action,
node: any,
reload: boolean = true
) {
const observable = this.halService.executeAndShowMessage(name, action);
if (reload) {
observable.subscribe((event) => this.reload(node, event.response));
}
return observable;
}
private executeAction(
actionName: string,
action: Action,
node: any,
reload: boolean = true
): Observable<ActionEvent> {
const observable = this.halService.execute(actionName, action);
if (reload) {
observable.subscribe((event) => this.reload(node, event.response));
}
return observable;
}
private reload(node: any, actionResponse?: HttpResponse<any>) {
if (!node) {
return;
}
// if the calling action already contained an updated node object
// no need to reload
const updatedNode = actionResponse?.body?.data;
if (updatedNode && updatedNode[this.identifierField] === node.pimRef) {
const mapped = this.nodeLoader.mapEntry(
updatedNode,
this.identifierField,
this.descriptionField
);
this.treeSelector.updateNode(node.id, mapped);
return;
}
if (node.actions.reload) {
this.halService
.execute("reload", node.actions.reload)
.subscribe((event) => {
if (event.response.body) {
const mapped = this.nodeLoader.mapEntry(
event.response.body,
this.identifierField,
this.descriptionField
);
this.treeSelector.updateNode(node.id, mapped);
}
});
}
}
onClickAction(data, actionName) {
switch (actionName) {
case "add-to-dashboard":
case "remove-from-dashboard":
case "refresh":
case "duplicate":
this.executeActionAndShowMessage(
actionName,
data.actions[actionName],
data
);
break;
default:
this.executeAction(actionName, data.actions[actionName], data);
break;
}
}
filteredActions(data): any[] {
return this.actions.filter((action) => data.actions?.[action.name]);
}
@WidgetConfiguration()
public configuration: WidgetConfig<WorklistSelectConfiguration>;
ngOnDestroy(): void {
this.unsubscribe.destroy();
}
onSelect(data) {
if (data && Array.isArray(data)) {
data = data.length > 0 ? data[0] : null;
}
if (data) {
this.elementSelected.next(data);
if (data.isNode || this.emitOnFolderSelect) {
this.listSelected.next(data);
}
} else {
this.elementSelected.next(null);
this.reset.next();
}
}
public onAddElement() {
this.addElementAction.next();
if (
this.addElementConfig &&
this.addElementConfig.availableOptions &&
this.addElementConfig.availableOptions.length > 0
) {
this.availableActions = this.getAvailableActions();
const dialogRef = this.dialogService.open(
CreateFolderEntryDialogComponent,
{
autoFocus: true,
width: "706px",
height: "889px",
}
);
dialogRef.componentInstance.title = "infotext.create.element";
dialogRef.componentInstance.availableActions = this.availableActions;
dialogRef.componentInstance.listFolderType = this.listFolderType;
}
}
private getAvailableActions(): any[] {
var availableActions = [];
if (this.addElementConfig && this.addElementConfig.availableOptions) {
from(this.addElementConfig.availableOptions)
.pipe(
concatMap((availableOption) => {
let option: any = availableOption as any;
return this.widgetFrameService.getData(option.actionsHref).pipe(
map((response) => {
return {
description: option.description,
actions: response._actions,
listFolderType: option.listFolderType,
};
})
);
})
)
.subscribe((option) => availableActions.push(option));
}
return availableActions;
}
public onClickElementIcon(event, data) {
event.stopPropagation();
event.preventDefault();
this.changeElementStateAction.next(data);
}
public onElementMove(event) {
this.elementMoved.next(event);
}
public onEditButtonClick(event) {
this.editClicked.next(event);
}
private getDefaultRefreshActions(): any[] {
return [
{
target: "worklists",
},
{
target: "listfolders",
action: "create",
},
{
target: "listfolder",
action: "delete",
},
];
}
public getWorkListRowContent(data: any): string {
return data.appendEntriesCount && data.entries !== null
? "[" + data.entries + "] " + data.name
: data.name;
}
}