File

projects/netgrif-components-core/src/lib/view/tree-case-view/tree-component/case-tree.service.ts

Index

Properties
Methods
Accessors

Constructor

constructor(_caseResourceService: CaseResourceService, _treeCaseViewService: TreeCaseViewService, _taskResourceService: TaskResourceService, _logger: LoggerService, _processService: ProcessService, _sideMenuService: SideMenuService, _translateService: TranslateService, _optionSelectorComponent: any, _treeConfiguration: TreeCaseViewConfiguration)
Parameters :
Name Type Optional
_caseResourceService CaseResourceService No
_treeCaseViewService TreeCaseViewService No
_taskResourceService TaskResourceService No
_logger LoggerService No
_processService ProcessService No
_sideMenuService SideMenuService No
_translateService TranslateService No
_optionSelectorComponent any No
_treeConfiguration TreeCaseViewConfiguration No

Methods

Public addChildNode
addChildNode(clickedNode: CaseTreeNode)

Adds a new child node to the given node based on the properties of the node's case

Parameters :
Name Type Optional
clickedNode CaseTreeNode No
Returns : Observable<boolean>

emits true if the child was successfully added, false if not

Public addRootChildNode
addRootChildNode()

Adds a child to the root node.

Useful if you are using the layout where the root node is hidden.

Returns : Observable<boolean>

emits true if the child was successfully added, false if not

Public changeActiveNode
changeActiveNode(node: CaseTreeNode | undefined)

Notifies the parent TreeCaseView that a case was clicked in the tree and it's Task should be displayed

Parameters :
Name Type Optional
node CaseTreeNode | undefined No
Returns : void
Protected convertPathToExpansionTree
convertPathToExpansionTree(paths: CaseTreePath)

Transforms a CaseTreePath object into an ExpansionTree object. The result has all the common paths merged into single branches of the resulting tree structure.

Parameters :
Name Type Optional Description
paths CaseTreePath No

nodes that should be expanded along with their path from the root node

Returns : ExpansionTree

an {

Protected createAndAddChildCase
createAndAddChildCase(processIdentifier: string, clickedNode: CaseTreeNode, operationResult: Subject)

Creates a new case and adds it to the children of the specified node

Parameters :
Name Type Optional Description
processIdentifier string No

identifier of the process that should be created

clickedNode CaseTreeNode No

the node that is the parent of the new case

operationResult Subject<boolean> No

the result of the operation will be emitted into this stream when the operation completes

Returns : void
Protected createChildRequestBody
createChildRequestBody(node: CaseTreeNode)

Returns undefined if the provided node doesn't contain enough information to create the request body.

Parameters :
Name Type Optional Description
node CaseTreeNode No

the {

Returns : CaseGetRequestBody

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.

Protected deleteRemovedNodes
deleteRemovedNodes(parentNode: CaseTreeNode, newCaseRefValue: Array)

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

Parameters :
Name Type Optional Description
parentNode CaseTreeNode No

an inner node of the tree that had some of it's children removed

newCaseRefValue Array<string> No

the new value of the parent node's case ref

Returns : void
Protected deselectNodeIfDescendantOf
deselectNodeIfDescendantOf(node: CaseTreeNode)

Deselects the currently selected node if it is a descendant of the provided node

Parameters :
Name Type Optional Description
node CaseTreeNode No

the node who's descendants should be deselected

Returns : void
Protected expandLevel
expandLevel(levelNodes: Array<CaseTreeNode>, targets: ExpansionTree)

Recursively expands all nodes from the provided array of nodes, that appear in the top level of the ExpansionTree object.

The top level nodes are expanded first, from the provided levelNodes.

Parameters :
Name Type Optional Description
levelNodes Array<CaseTreeNode> No

nodes from which the expansion should start

targets ExpansionTree No

a tree structure representing the nodes that are to be expanded recursively. The top level nodes are expanded first, from the provided levelNodes.

Returns : void
Protected expandNode
expandNode(node: CaseTreeNode)

Expands the target node in the tree and reloads it's children if they are marked as dirty (e.g. eager loaded tree) then, the Observable emits after all the subtree expansions complete.

Parameters :
Name Type Optional Description
node CaseTreeNode No

the {

Returns : Observable<boolean>

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.

Public expandPath
expandPath(path: CaseTreePath)

Expands all nodes in the tree dictated by the argument.

Parameters :
Name Type Optional Description
path CaseTreePath No

nodes that should be expanded along with their path from the root node

Returns : void
Public initializeTree
initializeTree(showRoot: boolean)

Adds the loaded tree root to the display based on the setting. If the root is not displayed it's children will be displayed on the first level.

Parameters :
Name Type Optional Description
showRoot boolean No

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 : Observable<void>

an Observable that emits when the tree finishes initialization.

Protected loadTreeRoot
loadTreeRoot()

Loads and populates the topmost level of the tree.

The displayed cases are determined by this object's rootFilter.

Cases are loaded one page at a time and the tree is refreshed after each page. finishedLoadingFirstLevel$ will emit true once the last page loads successfully. false will be emitted if any of the requests fail.

Returns : void
ngOnDestroy
ngOnDestroy()
Returns : void
Protected processChildNodeAdd
processChildNodeAdd(affectedNode: CaseTreeNode, caseRefField: ImmediateData, newCaseRefValue: Array)

Adds a new child node to the affectedNode by adding the last Case from the newCaseRefValue

Parameters :
Name Type Optional Description
affectedNode CaseTreeNode No

the node in the tree that had a child added - the parent node

caseRefField ImmediateData No

the case ref field of the affected node

newCaseRefValue Array<string> No

the new value of the case ref field in the node

Returns : Observable<ResultWithAfterActions<boolean>>

an Observable that emits true if a node was successfully added, false otherwise.

Protected processChildNodeRemove
processChildNodeRemove(affectedNode: CaseTreeNode, caseRefField: ImmediateData, newCaseRefValues: Set)

Removes the deleted node from the children of the affectedNode

Parameters :
Name Type Optional Description
affectedNode CaseTreeNode No

the node in the tree that had it's child removed

caseRefField ImmediateData No

the case ref field of the affected node

newCaseRefValues Set<string> No

the new value of the case ref field in the node

Returns : Observable<ResultWithAfterActions<boolean>>

an Observable that emits true when the remove operation completes.

Protected pushChildToTree
pushChildToTree(parentNode: CaseTreeNode, childCase: Case)

Adds a new child node to the target parent node.

Parameters :
Name Type Optional Description
parentNode CaseTreeNode No

the nodes whose child should be added

childCase Case No

the child case

Returns : CaseTreeNode

the newly added node

Protected reloadCurrentCase
reloadCurrentCase()

Reloads the currently selected case node. The Case object held in the 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.

Returns : void
Public removeNode
removeNode(node: CaseTreeNode)

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.

Parameters :
Name Type Optional Description
node CaseTreeNode No

the node that should be removed from the tree

Returns : void
Public toggleNode
toggleNode(node: CaseTreeNode)

Toggles the expansion state of a node

Parameters :
Name Type Optional Description
node CaseTreeNode No

the {

Returns : void
Protected updateCurrentChildrenWithNewPage
updateCurrentChildrenWithNewPage(node: CaseTreeNode, page: Page)

Updates the children of the given CaseTreeNode with Cases from the provided Page.

Parameters :
Name Type Optional Description
node CaseTreeNode No

the {

page Page<Case> No

the {

Returns : void
Protected updateNodeChildren
updateNodeChildren(node: CaseTreeNode)

Checks whether dirty children need to be reloaded and reloads them if needed.

Parameters :
Name Type Optional Description
node CaseTreeNode No

the {

Returns : Observable<void>

emits when loading finishes

Protected updatePageOfChildren
updatePageOfChildren(node: CaseTreeNode, pageNumber: number)

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.

Parameters :
Name Type Optional Description
node CaseTreeNode No

the {

pageNumber number No

the number of the first page that should be loaded. All following pages are loaded as well

Returns : Observable<void>

next is emitted when loading of all pages completes (regardless of the outcome)

Protected updateTreeAfterChildAdd
updateTreeAfterChildAdd(clickedNode: CaseTreeNode, newCaseRefValue: Array, operationResult: Subject)

Updates the tree after adding a new child

Parameters :
Name Type Optional Description
clickedNode CaseTreeNode No

the parent node

newCaseRefValue Array<string> No

the new value of the parent node's case ref

operationResult Subject<boolean> No

the result of the operation will be emitted into this stream when the operation completes

Returns : void

Properties

Protected _currentNode
Type : CaseTreeNode
Static Readonly DEFAULT_PAGE_SIZE
Type : number
Default value : 50

Accessors

rootFilter
setrootFilter(filter: Filter)
Parameters :
Name Type Optional
filter Filter No
Returns : void
dataSource
getdataSource()
treeControl
gettreeControl()
currentNode
getcurrentNode()
treeRootLoaded$
gettreeRootLoaded$()

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.

Returns : Observable<boolean>
_currentCase
get_currentCase()
rootNodeLoading$
getrootNodeLoading$()
Returns : Observable | undefined
rootNodeAddingChild$
getrootNodeAddingChild$()
Returns : Observable | undefined
isEagerLoaded
getisEagerLoaded()

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.

Returns : boolean
setisEagerLoaded(eager: boolean)

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.

Parameters :
Name Type Optional Description
eager boolean No

the new setting for eager loading

Returns : void
import {Inject, Injectable, OnDestroy, Optional} from '@angular/core';
import {Filter} from '../../../filter/models/filter';
import {HttpParams} from '@angular/common/http';
import {CaseResourceService} from '../../../resources/engine-endpoint/case-resource.service';
import {CaseTreeNode} from './model/case-tree-node';
import {TreeCaseViewService} from '../tree-case-view.service';
import {TaskResourceService} from '../../../resources/engine-endpoint/task-resource.service';
import {LoggerService} from '../../../logger/services/logger.service';
import {ImmediateData} from '../../../resources/interface/immediate-data';
import {Case} from '../../../resources/interface/case';
import {ProcessService} from '../../../process/process.service';
import {SideMenuService} from '../../../side-menu/services/side-menu.service';
import {SideMenuSize} from '../../../side-menu/models/side-menu-size';
import {forkJoin, Observable, of, ReplaySubject, Subject, Subscription, throwError} from 'rxjs';
import {Page} from '../../../resources/interface/page';
import {TreePetriflowIdentifiers} from '../model/tree-petriflow-identifiers';
import {tap} from 'rxjs/operators';
import {TranslateService} from '@ngx-translate/core';
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 {CaseGetRequestBody} from '../../../resources/interface/case-get-request-body';
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 {CaseTreePath} from './model/case-tree-path';
import {ExpansionTree} from './model/expansion-tree';
import {ResultWithAfterActions} from '../../../utility/result-with-after-actions';
import {EventOutcomeMessageResource} from '../../../resources/interface/message-resource';
import {SetDataEventOutcome} from '../../../event/model/event-outcomes/data-outcomes/set-data-event-outcome';
import {CreateCaseEventOutcome} from '../../../event/model/event-outcomes/case-outcomes/create-case-event-outcome';
import {refreshTree} from '../../../utility/refresh-tree';
import {NAE_TREE_CASE_VIEW_CONFIGURATION} from './model/tree-configuration-injection-token';
import {TreeCaseViewConfiguration} from './model/tree-case-view-configuration';
import {PaginationParams} from '../../../utility/pagination/pagination-params';
import {createSortParam, PaginationSort} from '../../../utility/pagination/pagination-sort';

/**
 * An internal helper object, that is used to return two values from a function.
 */
interface CaseUpdateResult {
    /**
     * Whether the attributes that are visible on the tree changed
     */
    visibleTreePropertiesChanged: boolean;
    /**
     * Whether the nodes children changed
     */
    childrenChanged: boolean;
}

@Injectable()
export class CaseTreeService implements OnDestroy {

    public static readonly DEFAULT_PAGE_SIZE = 50;

    protected _currentNode: CaseTreeNode;
    private _rootNodesFilter: Filter;
    private readonly _treeDataSource: MatTreeNestedDataSource<CaseTreeNode>;
    private readonly _treeControl: NestedTreeControl<CaseTreeNode>;
    private _treeRootLoaded$: ReplaySubject<boolean>;
    private _rootNode: CaseTreeNode;
    private _showRoot: boolean;
    /**
     * 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.
     */
    private _isEagerLoaded = false;
    /**
     * string id of the case, that is currently being reloaded, `undefined` if no case is currently being reloaded
     */
    private _reloadedCaseId: string;

    constructor(protected _caseResourceService: CaseResourceService,
                protected _treeCaseViewService: TreeCaseViewService,
                protected _taskResourceService: TaskResourceService,
                protected _logger: LoggerService,
                protected _processService: ProcessService,
                protected _sideMenuService: SideMenuService,
                protected _translateService: TranslateService,
                @Optional() @Inject(NAE_OPTION_SELECTOR_COMPONENT) protected _optionSelectorComponent: any,
                @Optional() @Inject(NAE_TREE_CASE_VIEW_CONFIGURATION) protected _treeConfiguration: TreeCaseViewConfiguration) {
        if (!this._treeConfiguration) {
            this._treeConfiguration = {
                pageSize: CaseTreeService.DEFAULT_PAGE_SIZE
            };
        }

        this._treeDataSource = new MatTreeNestedDataSource<CaseTreeNode>();
        this._treeControl = new NestedTreeControl<CaseTreeNode>(node => node.children);
        _treeCaseViewService.reloadCase$.asObservable().subscribe(() => {
            this.reloadCurrentCase();
        });
        this._treeRootLoaded$ = new ReplaySubject<boolean>(1);
    }

    ngOnDestroy(): void {
        this._treeRootLoaded$.complete();
    }

    public set rootFilter(filter: Filter) {
        this._rootNodesFilter = filter;
        this.dataSource.data = [];
        this.loadTreeRoot();
    }

    public get dataSource(): MatTreeNestedDataSource<CaseTreeNode> {
        return this._treeDataSource;
    }

    public get treeControl(): NestedTreeControl<CaseTreeNode> {
        return this._treeControl;
    }

    public get currentNode(): CaseTreeNode {
        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.
     */
    public get treeRootLoaded$(): Observable<boolean> {
        return this._treeRootLoaded$.asObservable();
    }

    protected get _currentCase(): Case | undefined {
        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.
     */
    public get rootNodeLoading$(): Observable<boolean> | undefined {
        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.
     */
    public get rootNodeAddingChild$(): Observable<boolean> | undefined {
        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.
     */
    public get isEagerLoaded(): boolean {
        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
     */
    public set isEagerLoaded(eager: boolean) {
        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.
     */
    protected 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.
     */
    public initializeTree(showRoot: boolean): Observable<void> {
        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: Observable<void>;
        if (!showRoot || this._isEagerLoaded) {
            const result = new ReplaySubject<void>(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
     */
    public changeActiveNode(node: CaseTreeNode | undefined): void {
        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
     */
    public toggleNode(node: CaseTreeNode): void {
        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.
     */
    protected expandNode(node: CaseTreeNode): Observable<boolean> {
        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<boolean>(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
     */
    protected updateNodeChildren(node: CaseTreeNode): Observable<void> {
        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<string>();
            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)
     */
    protected updatePageOfChildren(node: CaseTreeNode, pageNumber: number): Observable<void> {
        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<void>(1);

        let params: HttpParams = 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
     */
    protected updateCurrentChildrenWithNewPage(node: CaseTreeNode, page: Page<Case>): void {
        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.
     */
    protected createChildRequestBody(node: CaseTreeNode): CaseGetRequestBody {
        const childCaseRef = getImmediateData(node.case, TreePetriflowIdentifiers.CHILDREN_CASE_REF);
        if (!childCaseRef) {
            return undefined;
        }

        return {stringId: (childCaseRef.value as Array<string>)};
    }

    /**
     * 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
     */
    public addRootChildNode(): Observable<boolean> {
        const ret = new ReplaySubject<boolean>(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
     */
    public addChildNode(clickedNode: CaseTreeNode): Observable<boolean> {
        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<boolean>(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: Subscription;
                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
     */
    protected createAndAddChildCase(processIdentifier: string,
                                    clickedNode: CaseTreeNode,
                                    operationResult: Subject<boolean>) {
        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: EventOutcomeMessageResource) => {
                const caseRefField = getImmediateData(clickedNode.case, TreePetriflowIdentifiers.CHILDREN_CASE_REF);
                const setCaseRefValue = [...caseRefField.value, (outcomeResource.outcome as CreateCaseEventOutcome).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
     */
    protected updateTreeAfterChildAdd(clickedNode: CaseTreeNode, newCaseRefValue: Array<string>, operationResult: Subject<boolean>): void {
        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
     */
    public removeNode(node: CaseTreeNode): void {
        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 as Array<string>).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
     */
    public expandPath(path: CaseTreePath): void {
        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}
     */
    protected convertPathToExpansionTree(paths: CaseTreePath): ExpansionTree {
        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`.
     */
    protected expandLevel(levelNodes: Array<CaseTreeNode>, targets: ExpansionTree): void {
        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
     */
    protected deleteRemovedNodes(parentNode: CaseTreeNode, newCaseRefValue: Array<string>): void {
        const removedChildren = new Set<string>();
        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
     */
    protected deselectNodeIfDescendantOf(node: CaseTreeNode): void {
        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
     */
    private performCaseRefCall(caseId: string, newCaseRefValue: Array<string>): Observable<Array<string> | undefined> {
        const result$ = new ReplaySubject<Array<string>>(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: EventOutcomeMessageResource) => {
                    const changedFields = (outcomeResource.outcome as SetDataEventOutcome).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.
     */
    private updateNodeChildrenFromChangedFields(affectedNode: CaseTreeNode,
                                                newCaseRefValue: Array<string>): Observable<ResultWithAfterActions<boolean>> {
        const caseRefField = getImmediateData(affectedNode.case, TreePetriflowIdentifiers.CHILDREN_CASE_REF);
        const newChildren = new Set<string>();
        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.
     */
    protected processChildNodeAdd(affectedNode: CaseTreeNode,
                                  caseRefField: ImmediateData,
                                  newCaseRefValue: Array<string>): Observable<ResultWithAfterActions<boolean>> {
        const result$ = new ReplaySubject<ResultWithAfterActions<boolean>>(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
     */
    protected pushChildToTree(parentNode: CaseTreeNode, childCase: Case): CaseTreeNode {
        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.
     */
    protected processChildNodeRemove(affectedNode: CaseTreeNode,
                                     caseRefField: ImmediateData,
                                     newCaseRefValues: Set<string>): Observable<ResultWithAfterActions<boolean>> {
        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
     */
    private refreshTree(): void {
        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.
     */
    protected reloadCurrentCase(): void {
        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
     */
    private determineCaseUpdate(oldCase: Case, newCase: Case): CaseUpdateResult {
        const visibleAttributes = [
            TreePetriflowIdentifiers.CAN_ADD_CHILDREN,
            TreePetriflowIdentifiers.CAN_REMOVE_NODE,
            TreePetriflowIdentifiers.BEFORE_TEXT_ICON,
            TreePetriflowIdentifiers.TREE_ADD_ICON
        ];

        const result: CaseUpdateResult = {
            visibleTreePropertiesChanged: true, // for short-circuiting the evaluation, if nodes children changed
            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;
    }
}

result-matching ""

    No results matching ""