UNPKG

@netgrif/components-core

Version:

Netgrif Application engine frontend core Angular library

870 lines 132 kB
import { Inject, Injectable, Optional } from '@angular/core'; import { HttpParams } from '@angular/common/http'; import { CaseTreeNode } from './model/case-tree-node'; import { SideMenuSize } from '../../../side-menu/models/side-menu-size'; import { forkJoin, of, ReplaySubject, throwError } from 'rxjs'; import { TreePetriflowIdentifiers } from '../model/tree-petriflow-identifiers'; import { tap } from 'rxjs/operators'; import { hasContent } from '../../../utility/pagination/page-has-content'; import { NestedTreeControl } from '@angular/cdk/tree'; import { MatTreeNestedDataSource } from '@angular/material/tree'; import { ofVoid } from '../../../utility/of-void'; import { getImmediateData } from '../../../utility/get-immediate-data'; import { NAE_OPTION_SELECTOR_COMPONENT } from '../../../side-menu/content-components/injection-tokens'; import { SimpleFilter } from '../../../filter/models/simple-filter'; import { ResultWithAfterActions } from '../../../utility/result-with-after-actions'; import { refreshTree } from '../../../utility/refresh-tree'; import { NAE_TREE_CASE_VIEW_CONFIGURATION } from './model/tree-configuration-injection-token'; import { PaginationParams } from '../../../utility/pagination/pagination-params'; import { createSortParam, PaginationSort } from '../../../utility/pagination/pagination-sort'; import * as i0 from "@angular/core"; import * as i1 from "../../../resources/engine-endpoint/case-resource.service"; import * as i2 from "../tree-case-view.service"; import * as i3 from "../../../resources/engine-endpoint/task-resource.service"; import * as i4 from "../../../logger/services/logger.service"; import * as i5 from "../../../process/process.service"; import * as i6 from "../../../side-menu/services/side-menu.service"; import * as i7 from "@ngx-translate/core"; export class CaseTreeService { _caseResourceService; _treeCaseViewService; _taskResourceService; _logger; _processService; _sideMenuService; _translateService; _optionSelectorComponent; _treeConfiguration; static DEFAULT_PAGE_SIZE = 50; _currentNode; _rootNodesFilter; _treeDataSource; _treeControl; _treeRootLoaded$; _rootNode; _showRoot; /** * Weather the tree is eager loaded or not. * * Defaults to `false`. * * It is not recommended to eager load large trees as each node sends a separate backend request to load its data. */ _isEagerLoaded = false; /** * string id of the case, that is currently being reloaded, `undefined` if no case is currently being reloaded */ _reloadedCaseId; constructor(_caseResourceService, _treeCaseViewService, _taskResourceService, _logger, _processService, _sideMenuService, _translateService, _optionSelectorComponent, _treeConfiguration) { this._caseResourceService = _caseResourceService; this._treeCaseViewService = _treeCaseViewService; this._taskResourceService = _taskResourceService; this._logger = _logger; this._processService = _processService; this._sideMenuService = _sideMenuService; this._translateService = _translateService; this._optionSelectorComponent = _optionSelectorComponent; this._treeConfiguration = _treeConfiguration; if (!this._treeConfiguration) { this._treeConfiguration = { pageSize: CaseTreeService.DEFAULT_PAGE_SIZE }; } this._treeDataSource = new MatTreeNestedDataSource(); this._treeControl = new NestedTreeControl(node => node.children); _treeCaseViewService.reloadCase$.asObservable().subscribe(() => { this.reloadCurrentCase(); }); this._treeRootLoaded$ = new ReplaySubject(1); } ngOnDestroy() { this._treeRootLoaded$.complete(); } set rootFilter(filter) { this._rootNodesFilter = filter; this.dataSource.data = []; this.loadTreeRoot(); } get dataSource() { return this._treeDataSource; } get treeControl() { return this._treeControl; } get currentNode() { return this._currentNode; } /** * Emits a value whenever a new root node Filter is set. * * `true` is emitted if the root node was successfully loaded. `false` otherwise. * * On subscription emits the last emitted value (if any) to the subscriber. */ get treeRootLoaded$() { return this._treeRootLoaded$.asObservable(); } get _currentCase() { return this._currentNode ? this._currentNode.case : undefined; } /** * @returns an `Observable` of the {@link LoadingEmitter} representing the loading state of the root node. * Returns `undefined` if the tree has not yet been initialized. * * Wait for an emission on the [treeRootLoaded$]{@link CaseTreeService#treeRootLoaded$} stream before getting this Observable. * * The first value emitted by the Observable is `false`, when the tree finishes initializing. */ get rootNodeLoading$() { return !!this._rootNode ? this._rootNode.loadingChildren.asObservable() : undefined; } /** * @returns an `Observable` of the {@link LoadingEmitter} representing whether the root node is currently * in the process of adding a new child node or not. * Returns `undefined` if the tree has not yet been initialized. * * Wait for an emission on the [treeRootLoaded$]{@link CaseTreeService#treeRootLoaded$} stream before getting this Observable. * * The first value emitted by the Observable is `false`, when the tree finishes initializing. */ get rootNodeAddingChild$() { return !!this._rootNode ? this._rootNode.addingNode.asObservable() : undefined; } /** * Weather the tree is eager loaded or not. * * Defaults to `false`. * * It is not recommended to eager load large trees as each node sends a separate backend request to load its data. */ get isEagerLoaded() { return this._isEagerLoaded; } /** * Weather the tree is eager loaded or not. * * Defaults to `false`. * * It is not recommended to eager load large trees as each node sends a separate backend request to load its data. * * @param eager the new setting for eager loading */ set isEagerLoaded(eager) { this._isEagerLoaded = eager; } /** * Loads and populates the topmost level of the tree. * * The displayed cases are determined by this object's [rootFilter]{@link CaseTreeService#rootFilter}. * * Cases are loaded one page at a time and the tree is refreshed after each page. * [finishedLoadingFirstLevel$]{@link CaseTreeService#treeRootLoaded$} * will emit `true` once the last page loads successfully. * `false` will be emitted if any of the requests fail. */ loadTreeRoot() { if (this._rootNodesFilter) { this._caseResourceService.searchCases(this._rootNodesFilter).subscribe(page => { if (hasContent(page)) { this._rootNode = new CaseTreeNode(page.content[0], undefined); if (page.content.length !== 1) { this._logger.warn('Filter for tree root returned more than one case. Using the first value as tree root...'); } this._treeRootLoaded$.next(true); } else { this._logger.error('Tree root cannot be generated from the provided filter', page); } }, error => { this._logger.error('Root node of the case tree could not be loaded', error); this._treeRootLoaded$.next(false); }); } } /** * Adds the loaded tree root to the display based on the setting. * @param showRoot whether the root of the tree should be displayed in the tree or not. * If the root is not displayed it's children will be displayed on the first level. * @returns an Observable that emits when the tree finishes initialization. */ initializeTree(showRoot) { if (!this._rootNode) { this._logger.error('Set a Filter before initializing the case tree'); return throwError(new Error('Set a Filter before initializing the case tree')); } this._showRoot = showRoot; if (showRoot) { this.dataSource.data = [this._rootNode]; } else { this.dataSource.data = this._rootNode.children; } let ret; if (!showRoot || this._isEagerLoaded) { const result = new ReplaySubject(1); ret = result.asObservable(); this.expandNode(this._rootNode).subscribe(() => { this.refreshTree(); result.next(); result.complete(); }); } else { this.refreshTree(); ret = ofVoid(); } return ret; } /** * Notifies the parent TreeCaseView that a case was clicked in the tree and it's Task should be displayed */ changeActiveNode(node) { this._currentNode = node; this._treeCaseViewService.loadTask$.next(node ? node.case : undefined); } /** * Toggles the expansion state of a node * @param node the {@link CaseTreeNode} who's content should be toggled */ toggleNode(node) { if (this._treeControl.isExpanded(node)) { this._treeControl.collapse(node); } else { this.expandNode(node); } } /** * Expands the target node in the tree and reloads it's children if they are marked as dirty * @param node the {@link CaseTreeNode} that should be expanded * @returns emits `true` if the node is expanded and `false` if not. If the expansion causes more node expansions * (e.g. eager loaded tree) then, the Observable emits after all the subtree expansions complete. */ expandNode(node) { this._logger.debug('Requesting expansion of tree node', node.toLoggableForm()); if (node.loadingChildren.isActive) { this._logger.debug('Node requested for expansion is loading. Expansion canceled.'); return of(false); } if (!node.dirtyChildren) { this._logger.debug('Node requested for expansion has clean children. Simple expansion.'); this.treeControl.expand(node); return of(true); } const ret = new ReplaySubject(1); this.updateNodeChildren(node).subscribe(() => { this._logger.debug('Node requested for expansion with dirty children had its children reloaded.'); if (node.children.length > 0) { this._logger.debug('Node expanded.', node.toLoggableForm()); this.treeControl.expand(node); if (this._isEagerLoaded) { this._logger.debug(`Eager loading children of tree node with case id '${node.case.stringId}'`); const innerObservables = node.children.map(childNode => this.expandNode(childNode)); // forkJoin doesn't emit with 0 input observables innerObservables.push(of(true)); forkJoin(innerObservables).subscribe(() => { ret.next(true); ret.complete(); }); } else { ret.next(true); ret.complete(); } } else { ret.next(false); ret.complete(); } }); return ret.asObservable(); } /** * Checks whether dirty children need to be reloaded and reloads them if needed. * @param node the {@link CaseTreeNode} who's children are updated * @returns emits when loading finishes */ updateNodeChildren(node) { node.loadingChildren.on(); const childrenCaseRef = getImmediateData(node.case, TreePetriflowIdentifiers.CHILDREN_CASE_REF); if (!childrenCaseRef || childrenCaseRef.value.length === 0) { node.children = []; this.refreshTree(); node.dirtyChildren = false; node.loadingChildren.off(); return ofVoid(); } if (node.children.length === childrenCaseRef.value.length) { const existingChildren = new Set(); node.children.forEach(childNode => existingChildren.add(childNode.case.stringId)); if (childrenCaseRef.value.every(caseId => existingChildren.has(caseId))) { node.dirtyChildren = false; node.loadingChildren.off(); return ofVoid(); } } return this.updatePageOfChildren(node, 0).pipe(tap(() => { this.refreshTree(); node.dirtyChildren = false; node.loadingChildren.off(); })); } /** * Loads every page of children from the given number and updates the existing children. * * Missing nodes are removed. Existing nodes are marked as dirty. New nodes are added. * * Nodes are returned in their insertion order. * @param node the {@link CaseTreeNode} who's children are updated * @param pageNumber the number of the first page that should be loaded. All following pages are loaded as well * @returns next is emitted when loading of all pages completes (regardless of the outcome) */ updatePageOfChildren(node, pageNumber) { const requestBody = this.createChildRequestBody(node); if (!requestBody) { this._logger.error('Cannot create filter to find children of the given node', node.case); return throwError(new Error('Cannot create filter to find children of the given node')); } const done = new ReplaySubject(1); let params = new HttpParams(); params = params.set(PaginationParams.PAGE_SIZE, `${this._treeConfiguration.pageSize}`) .set(PaginationParams.PAGE_NUMBER, `${pageNumber}`) .set(PaginationParams.PAGE_SORT, createSortParam('creationDate', PaginationSort.ASCENDING)); this._caseResourceService.getCases(requestBody, params).subscribe(page => { if (!hasContent(page)) { this._logger.error('Child cases invalid page content', page); done.next(); done.complete(); return; } this.updateCurrentChildrenWithNewPage(node, page); if (pageNumber + 1 < page.pagination.totalPages) { this.updatePageOfChildren(node, pageNumber + 1).subscribe(() => { done.next(); done.complete(); }); } else { done.next(); done.complete(); } }, error => { this._logger.error('Child cases could not be loaded', error); done.next(); done.complete(); }); return done.asObservable(); } /** * Updates the children of the given {@link CaseTreeNode} with [Cases]{@link Case} from the provided {@link Page}. * @param node the {@link CaseTreeNode} who's children are updated * @param page the {@link Page} that contains the updated children */ updateCurrentChildrenWithNewPage(node, page) { page.content.forEach((newCase, index) => { const position = page.pagination.size * page.pagination.number + index; while (position < node.children.length && node.children[position].case.stringId !== newCase.stringId) { node.children.splice(position, 1); } if (node.children.length === position) { node.children.push(new CaseTreeNode(newCase, node)); } else { node.children[position].case = newCase; node.dirtyChildren = true; this.treeControl.collapseDescendants(node.children[position]); } }); } /** * @param node the {@link CaseTreeNode} who's children the {@link Filter} should return * @returns a request body that finds all child cases of the given `node`. * Returns `undefined` if the provided `node` doesn't contain enough information to create the request body. */ createChildRequestBody(node) { const childCaseRef = getImmediateData(node.case, TreePetriflowIdentifiers.CHILDREN_CASE_REF); if (!childCaseRef) { return undefined; } return { stringId: childCaseRef.value }; } /** * Adds a child to the root node. * * Useful if you are using the layout where the root node is hidden. * @returns emits `true` if the child was successfully added, `false` if not */ addRootChildNode() { const ret = new ReplaySubject(1); this.addChildNode(this._rootNode).subscribe(added => { if (added) { if (!this._showRoot && this._treeDataSource.data.length === 0) { this._treeDataSource.data = this._rootNode.children; this.refreshTree(); } } ret.next(added); ret.complete(); }); return ret; } /** * Adds a new child node to the given node based on the properties of the node's case * @returns emits `true` if the child was successfully added, `false` if not */ addChildNode(clickedNode) { clickedNode.addingNode.on(); const caseRefField = getImmediateData(clickedNode.case, TreePetriflowIdentifiers.CHILDREN_CASE_REF); if (caseRefField.allowedNets.length === 0) { this._logger.error(`Case ${clickedNode.case.stringId} can add new tree nodes but has no allowed nets`); clickedNode.addingNode.off(); return of(false); } const ret = new ReplaySubject(1); if (caseRefField.allowedNets.length === 1) { this.createAndAddChildCase(caseRefField.allowedNets[0], clickedNode, ret); } else { this._processService.getNets(caseRefField.allowedNets).subscribe(nets => { const sideMeuRef = this._sideMenuService.open(this._optionSelectorComponent, SideMenuSize.MEDIUM, { title: clickedNode.case.title, options: nets.map(net => ({ text: net.title, value: net.identifier })) }); let sideMenuSubscription; sideMenuSubscription = sideMeuRef.onClose.subscribe(event => { if (!!event.data) { this.createAndAddChildCase(event.data.value, clickedNode, ret); sideMenuSubscription.unsubscribe(); } else { clickedNode.addingNode.off(); ret.next(false); ret.complete(); } }); }); } return ret.asObservable(); } /** * Creates a new case and adds it to the children of the specified node * @param processIdentifier identifier of the process that should be created * @param clickedNode the node that is the parent of the new case * @param operationResult the result of the operation will be emitted into this stream when the operation completes */ createAndAddChildCase(processIdentifier, clickedNode, operationResult) { this._processService.getNet(processIdentifier).subscribe(net => { const childTitleImmediate = getImmediateData(clickedNode.case, TreePetriflowIdentifiers.CHILD_NODE_TITLE); let childTitle = this._translateService.instant('caseTree.newNodeDefaultName'); if (!!childTitleImmediate) { childTitle = childTitleImmediate.value; } else if (!!net.defaultCaseName) { childTitle = net.defaultCaseName; } this._caseResourceService.createCase({ title: childTitle, netId: net.stringId }).subscribe((outcomeResource) => { const caseRefField = getImmediateData(clickedNode.case, TreePetriflowIdentifiers.CHILDREN_CASE_REF); const setCaseRefValue = [...caseRefField.value, outcomeResource.outcome.aCase.stringId]; this.performCaseRefCall(clickedNode.case.stringId, setCaseRefValue).subscribe(valueChange => this.updateTreeAfterChildAdd(clickedNode, valueChange ? valueChange : setCaseRefValue, operationResult)); }); }); } /** * Updates the tree after adding a new child * @param clickedNode the parent node * @param newCaseRefValue the new value of the parent node's case ref * @param operationResult the result of the operation will be emitted into this stream when the operation completes */ updateTreeAfterChildAdd(clickedNode, newCaseRefValue, operationResult) { this.updateNodeChildrenFromChangedFields(clickedNode, newCaseRefValue).subscribe(result => { clickedNode.addingNode.off(); this.expandNode(clickedNode).subscribe(expandSuccess => { if (expandSuccess) { this.changeActiveNode(clickedNode.children[clickedNode.children.length - 1]); result.executeAfterActions(); } operationResult.next(true); operationResult.complete(); }); }); } /** * removes the provided non-root node if the underlying case allows it. * * The underlying case is removed from the case ref of it's parent element with the help from the `remove` * operation provided by case ref itself. * @param node the node that should be removed from the tree */ removeNode(node) { if (!node.parent) { this._logger.error('Case tree doesn\'t support removal of the root node, as it has no parent case ref.'); return; } node.removingNode.on(); const caseRefImmediate = getImmediateData(node.parent.case, TreePetriflowIdentifiers.CHILDREN_CASE_REF); const setCaseRefValue = caseRefImmediate.value.filter(id => id !== node.case.stringId); this.performCaseRefCall(node.parent.case.stringId, setCaseRefValue).subscribe(caseRefChange => { const newCaseRefValue = caseRefChange ? caseRefChange : setCaseRefValue; this.deleteRemovedNodes(node.parent, newCaseRefValue); this.updateNodeChildrenFromChangedFields(node.parent, newCaseRefValue); node.removingNode.off(); }); this.deselectNodeIfDescendantOf(node); } /** * Expands all nodes in the tree dictated by the argument. * * @param path nodes that should be expanded along with their path from the root node */ expandPath(path) { if (this.dataSource.data.length === 0) { return; } let rootNode = this.dataSource.data[0]; while (rootNode.parent !== undefined) { rootNode = rootNode.parent; } this.expandLevel([rootNode], this.convertPathToExpansionTree(path)); } /** * Transforms a {@Link CaseTreePath} object into an {@link ExpansionTree} object. * The result has all the common paths merged into single branches of the resulting tree structure. * * @param paths nodes that should be expanded along with their path from the root node * @returns an {@link ExpansionTree} equivalent to the provided {@link CaseTreePath} */ convertPathToExpansionTree(paths) { const result = {}; Object.values(paths).forEach(path => { let currentNode = result; path.forEach(nodeId => { if (currentNode[nodeId] === undefined) { currentNode[nodeId] = {}; } currentNode = currentNode[nodeId]; }); }); return result; } /** * Recursively expands all nodes from the provided array of nodes, that appear in the top level of the {@link ExpansionTree} object. * * @param levelNodes nodes from which the expansion should start * @param targets a tree structure representing the nodes that are to be expanded recursively. * The top level nodes are expanded first, from the provided `levelNodes`. */ expandLevel(levelNodes, targets) { const desiredIds = new Set(Object.keys(targets)); if (desiredIds.size === 0) { return; } levelNodes.forEach(n => { const node = n; if (!desiredIds.has(node.case.stringId)) { return; // continue } this.expandNode(node).subscribe(success => { if (!success) { this._logger.debug('Could not expand tree node with ID: ' + node.case.stringId); return; } this.expandLevel(node.children, targets[node.case.stringId]); }); }); } /** * Deletes the subtrees rooted at the nodes that are present in the parent node's child case ref values, * but are no longer present in the new value * @param parentNode an inner node of the tree that had some of it's children removed * @param newCaseRefValue the new value of the parent node's case ref */ deleteRemovedNodes(parentNode, newCaseRefValue) { const removedChildren = new Set(); getImmediateData(parentNode.case, TreePetriflowIdentifiers.CHILDREN_CASE_REF).value.forEach(id => removedChildren.add(id)); newCaseRefValue.forEach(id => removedChildren.delete(id)); removedChildren.forEach(removedId => this._caseResourceService.deleteCase(removedId, true) .subscribe(responseMessage => { if (responseMessage.error) { this._logger.error('Removal of child case unsuccessful', responseMessage.error); } })); } /** * Deselects the currently selected node if it is a descendant of the provided node * @param node the node who's descendants should be deselected */ deselectNodeIfDescendantOf(node) { let bubblingNode = this.currentNode; while (bubblingNode && bubblingNode !== this._rootNode) { if (bubblingNode === node) { this.changeActiveNode(undefined); break; } bubblingNode = bubblingNode.parent; } } /** * Performs a backend call on the given case, and sets the value of the case ref field in the transition defined by * [CASE_REF_TRANSITION]{@link TreePetriflowIdentifiers#CASE_REF_TRANSITION}. * @param caseId string ID of the case that should have it's tree case ref set * @param newCaseRefValue the new value of the case ref field */ performCaseRefCall(caseId, newCaseRefValue) { const result$ = new ReplaySubject(1); this._taskResourceService.getTasks(SimpleFilter.fromTaskQuery({ case: { id: caseId }, transitionId: TreePetriflowIdentifiers.CASE_REF_TRANSITION })).subscribe(page => { if (!hasContent(page)) { this._logger.error('Case ref accessor task doesn\'t exist!'); result$.complete(); return; } const task = page.content[0]; this._taskResourceService.assignTask(task.stringId).subscribe(assignResponse => { if (!assignResponse.success) { this._logger.error('Case ref accessor task could not be assigned', assignResponse.error); } const body = {}; body[task.stringId] = { [TreePetriflowIdentifiers.CHILDREN_CASE_REF]: { type: 'caseRef', value: newCaseRefValue } }; this._taskResourceService.setData(task.stringId, body).subscribe((outcomeResource) => { const changedFields = outcomeResource.outcome.changedFields.changedFields; const caseRefChanges = changedFields[TreePetriflowIdentifiers.CHILDREN_CASE_REF]; result$.next(caseRefChanges ? caseRefChanges.value : undefined); result$.complete(); this._taskResourceService.finishTask(task.stringId).subscribe(finishResponse => { if (finishResponse.success) { this._logger.debug('Case ref accessor task finished', finishResponse.success); } else { this._logger.error('Case ref accessor task finish failed', finishResponse.error); } }); }, error => { this._logger.error('Could not set data to tree case ref', error); result$.complete(); }); }, error => { this._logger.error('Case ref accessor task could not be assigned', error); result$.complete(); }); }, error => { this._logger.error('Case ref accessor task could not be found', error); result$.complete(); }); return result$.asObservable(); } /** * Performs an update after adding or removing a node from the tree. * * If only one node was added adds it into the tree * * If only one node was removed removes it from the tree * * Otherwise collapses the affected node and marks it's children as dirty * * @param affectedNode node that had it's children changed * @param newCaseRefValue new value of the caseRef field returned by backend * @returns an `Observable` that emits an object with the [result]{@link ResultWithAfterActions#result} attribute set to `true` if * the update completes successfully and `false` otherwise. */ updateNodeChildrenFromChangedFields(affectedNode, newCaseRefValue) { const caseRefField = getImmediateData(affectedNode.case, TreePetriflowIdentifiers.CHILDREN_CASE_REF); const newChildren = new Set(); newCaseRefValue.forEach(id => newChildren.add(id)); let numberOfMissingChildren = 0; for (let i = 0; i < caseRefField.value.length && numberOfMissingChildren < 2; i++) { if (!newChildren.has(caseRefField.value[i])) { numberOfMissingChildren++; } } const exactlyOneChildAdded = caseRefField.value.length + 1 === newCaseRefValue.length && caseRefField.value.every(it => newChildren.has(it)); const exactlyOneChildRemoved = caseRefField.value.length - 1 === newCaseRefValue.length && numberOfMissingChildren === 1; if (!exactlyOneChildAdded && !exactlyOneChildRemoved) { caseRefField.value = newCaseRefValue; this._treeControl.collapseDescendants(affectedNode); affectedNode.dirtyChildren = true; return of(new ResultWithAfterActions(true)); } if (exactlyOneChildAdded) { return this.processChildNodeAdd(affectedNode, caseRefField, newCaseRefValue); } else { return this.processChildNodeRemove(affectedNode, caseRefField, newChildren); } } /** * Adds a new child node to the `affectedNode` by adding the last Case from the `newCaseRefValue` * @param affectedNode the node in the tree that had a child added - the parent node * @param caseRefField the case ref field of the affected node * @param newCaseRefValue the new value of the case ref field in the node * @returns an `Observable` that emits `true` if a node was successfully added, `false` otherwise. */ processChildNodeAdd(affectedNode, caseRefField, newCaseRefValue) { const result$ = new ReplaySubject(1); caseRefField.value = newCaseRefValue; this._caseResourceService.getOneCase(newCaseRefValue[newCaseRefValue.length - 1]).subscribe(childCase => { if (childCase) { this._logger.debug('Pushing child node to tree', { childCase, affectedNode: affectedNode.toLoggableForm() }); const childNode = this.pushChildToTree(affectedNode, childCase); result$.next(new ResultWithAfterActions(true, [() => { if (this._isEagerLoaded) { this._logger.debug('Eagerly expanding a newly added node.', childNode.toLoggableForm()); this.expandNode(childNode); } }])); } else { this._logger.error('New child case was not found, illegal state', childCase); result$.next(new ResultWithAfterActions(false)); } result$.complete(); }, error => { this._logger.error('New child node case could not be found', error); result$.next(new ResultWithAfterActions(false)); result$.complete(); }); return result$.asObservable(); } /** * Adds a new child node to the target parent node. * @param parentNode the nodes whose child should be added * @param childCase the child case * @returns the newly added node */ pushChildToTree(parentNode, childCase) { const childNode = new CaseTreeNode(childCase, parentNode); parentNode.children.push(childNode); this.refreshTree(); return childNode; } /** * Removes the deleted node from the children of the `affectedNode` * @param affectedNode the node in the tree that had it's child removed * @param caseRefField the case ref field of the affected node * @param newCaseRefValues the new value of the case ref field in the node * @returns an `Observable` that emits `true` when the remove operation completes. */ processChildNodeRemove(affectedNode, caseRefField, newCaseRefValues) { const index = caseRefField.value.findIndex(it => !newCaseRefValues.has(it)); caseRefField.value.splice(index, 1); affectedNode.children.splice(index, 1); this.refreshTree(); return of(new ResultWithAfterActions(true)); } /** * @ignore * Forces a rerender of the tree content */ refreshTree() { refreshTree(this._treeDataSource); } /** * Reloads the currently selected case node. The {@link Case} object held in the {@link CaseTreeNode} instance is not replaced, * but the new Case is `Object.assign`-ed into it. This means that the reference to the Case instance is unchanged but references * to all it's non-primitive attributes are changed. * * If a reload of the current node is initiated before the previous one completed, the new one is ignored. * * If the currently selected case changed before a response from backend was received the response is ignored. * * Note that the parent node, nor the child nodes are reloaded. */ reloadCurrentCase() { if (!this._currentNode) { this._logger.debug('No Tree Case Node selected, nothing to reload'); return; } if (this._reloadedCaseId && this._currentNode.case.stringId !== this._reloadedCaseId) { this._logger.debug('Reload of the current case already in progress'); return; } this._reloadedCaseId = this._currentNode.case.stringId; this._caseResourceService.getOneCase(this._currentCase.stringId).subscribe(reloadedCurrentCase => { if (!reloadedCurrentCase) { this._logger.error('Current Case Tree Node could not be reloaded. Invalid server response', reloadedCurrentCase); return; } if (this._currentNode && reloadedCurrentCase.stringId === this._currentNode.case.stringId) { this._reloadedCaseId = undefined; const change = this.determineCaseUpdate(this._currentCase, reloadedCurrentCase); Object.assign(this._currentCase, reloadedCurrentCase); this._treeCaseViewService.loadTask$.next(this._currentCase); if (change.visibleTreePropertiesChanged) { this.refreshTree(); } if (change.childrenChanged) { this._currentNode.dirtyChildren = true; this.expandNode(this._currentNode); } this._logger.debug('Current Case Tree Node reloaded'); } else { this._logger.debug('Discarding case reload response, since the current node has changed before its case was received'); } }, error => { this._logger.error('Current Case Tree Node reload request failed', error); }); } /** * Determines if anny of the case attributes that are visible on the tree changed. * @param oldCase the previous version of the Case object, that is currently displayed on the tree * @param newCase the new version of the Case object, that should replace the old one */ determineCaseUpdate(oldCase, newCase) { const visibleAttributes = [ TreePetriflowIdentifiers.CAN_ADD_CHILDREN, TreePetriflowIdentifiers.CAN_REMOVE_NODE, TreePetriflowIdentifiers.BEFORE_TEXT_ICON, TreePetriflowIdentifiers.TREE_ADD_ICON ]; const result = { visibleTreePropertiesChanged: true, childrenChanged: false }; const oldChildCaseRef = getImmediateData(oldCase, TreePetriflowIdentifiers.CHILDREN_CASE_REF); if (oldChildCaseRef !== undefined) { const oldChildren = new Set(oldChildCaseRef.value); const newChildren = new Set(getImmediateData(newCase, TreePetriflowIdentifiers.CHILDREN_CASE_REF).value); result.childrenChanged = oldChildren.size !== newChildren.size; if (!result.childrenChanged) { result.childrenChanged = Array.from(oldChildren).some(childId => !newChildren.has(childId)); } // short-circuit if (result.childrenChanged) { return result; } } result.visibleTreePropertiesChanged = visibleAttributes.some(attribute => { return getImmediateData(oldCase, attribute) && getImmediateData(oldCase, attribute).value !== getImmediateData(newCase, attribute).value; }); return result; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CaseTreeService, deps: [{ token: i1.CaseResourceService }, { token: i2.TreeCaseViewService }, { token: i3.TaskResourceService }, { token: i4.LoggerService }, { token: i5.ProcessService }, { token: i6.SideMenuService }, { token: i7.TranslateService }, { token: NAE_OPTION_SELECTOR_COMPONENT, optional: true }, { token: NAE_TREE_CASE_VIEW_CONFIGURATION, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CaseTreeService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CaseTreeService, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: i1.CaseResourceService }, { type: i2.TreeCaseViewService }, { type: i3.TaskResourceService }, { type: i4.LoggerService }, { type: i5.ProcessService }, { type: i6.SideMenuService }, { type: i7.TranslateService }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [NAE_OPTION_SELECTOR_COMPONENT] }] }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [NAE_TREE_CASE_VIEW_CONFIGURATION] }] }] }); //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2FzZS10cmVlLnNlcnZpY2UuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi9wcm9qZWN0cy9uZXRncmlmLWNvbXBvbmVudHMtY29yZS9zcmMvbGliL3ZpZXcvdHJlZS1jYXNlLXZpZXcvdHJlZS1jb21wb25lbnQvY2FzZS10cmVlLnNlcnZpY2UudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFDLE1BQU0sRUFBRSxVQUFVLEVBQWEsUUFBUSxFQUFDLE1BQU0sZUFBZSxDQUFDO0FBRXRFLE9BQU8sRUFBQyxVQUFVLEVBQUMsTUFBTSxzQkFBc0IsQ0FBQztBQUVoRCxPQUFPLEVBQUMsWUFBWSxFQUFDLE1BQU0sd0JBQXdCLENBQUM7QUFRcEQsT0FBTyxFQUFDLFlBQVksRUFBQyxNQUFNLDBDQUEwQyxDQUFDO0FBQ3RFLE9BQU8sRUFBQyxRQUFRLEVBQWMsRUFBRSxFQUFFLGFBQWEsRUFBeUIsVUFBVSxFQUFDLE1BQU0sTUFBTSxDQUFDO0FBRWhHLE9BQU8sRUFBQyx3QkFBd0IsRUFBQyxNQUFNLHFDQUFxQyxDQUFDO0FBQzdFLE9BQU8sRUFBQyxHQUFHLEVBQUMsTUFBTSxnQkFBZ0IsQ0FBQztBQUVuQyxPQUFPLEVBQUMsVUFBVSxFQUFDLE1BQU0sOENBQThDLENBQUM7QUFDeEUsT0FBTyxFQUFDLGlCQUFpQixFQUFDLE1BQU0sbUJBQW1CLENBQUM7QUFDcEQsT0FBTyxFQUFDLHVCQUF1QixFQUFDLE1BQU0sd0JBQXdCLENBQUM7QUFDL0QsT0FBTyxFQUFDLE1BQU0sRUFBQyxNQUFNLDBCQUEwQixDQUFDO0FBRWhELE9BQU8sRUFBQyxnQkFBZ0IsRUFBQyxNQUFNLHFDQUFxQyxDQUFDO0FBQ3JFLE9BQU8sRUFBQyw2QkFBNkIsRUFBQyxNQUFNLHdEQUF3RCxDQUFDO0FBQ3JHLE9BQU8sRUFBQyxZQUFZLEVBQUMsTUFBTSxzQ0FBc0MsQ0FBQztBQUdsRSxPQUFPLEVBQUMsc0JBQXNCLEVBQUMs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