# Implement a Multi Module Tree

To implement a multi-module tree in a layout, you need to add layout multi_module_tree to the target layout. Layout multi_module_tree is available after the installation of this 4App and contains most of the configuration you need to display objects of multiple Core Engine modules in a hierarchical view.

In the following sample configuration, we will configure a layout to display the objects of the modules folder and file together in one tree.

# 1. Import Layout

Layout multi_module_tree can be used in any other layout via the import_layout tag.

<import_layout layout_id="multi_module_tree"/>

To change a layout, you need to store a layout change in your custom folder (Layouts documentation (opens new window)).

# 2. Define Root Elements and Structure

To define which modules to display, you need to consider three layout parameters:

  • rootModules: This parameter defines all modules that should be potential root elements.
  • moduleStructure: This parameter describes the relations between all the modules that should appear in the tree.
  • addTreeRootFilter: This parameter controls whether the loaded root elements should have a parent object or not (default: false). For related trees (e.g. in a detail view), the TreeRootFilter should be set to false to display all relevant elements. For unrelated or unfiltered trees (e.g. a filter in a main view), it is essential that the TreeRootFilter is set to true in order to see only root elements.

The way of configuring these parameters depends on the kind of relation between the corresponding modules. They can be related either via a CEId field or via a child loader (which is required if there is no actual relation between these modules). Our example with file and folder requires an additional child loader.

# Relation Based on a CEId Field

In the following configuration, we want to display a tree of all roles and the users they contain. The following specifications are required:

<import_layout layout_id="multi_module_tree">
  <parameter>
    <entry key="rootModules" class="array">
      <value>ce_role</value>
    </entry>
    <entry key="moduleStructure" class="map">
      <entry key="ce_role" class="array">
        <value class="map">
          <entry key="moduleName">ce_role</entry>
          <entry key="relationField">pid</entry>
        </value>
        <value class="map">
          <entry key="moduleName">user</entry>
          <entry key="relationField">role</entry>
        </value>
      </entry>
    </entry>
  </parameter>
</import_layout>
  • Parameter moduleStructure has to be of class map.
  • Each module whose objects should show up on the first level of the tree (root elements) should get an own entry in the moduleStructure map.
  • Each of these entries need to be an array containing a number of maps describing what child objects are allowed in the tree.

In the example above, the folder module is allowed to have children in the folder module (parameter moduleName of the inner map is set to folder).
Parameter relationField defines that field pid has to be used to retrieve all children of a folder.

Please note: The relationField has to be part of the child module.

# Relation Based on a Child Loader

Sometimes the children of a module cannot be loaded by using a relation field from the child module. For example, if we want to display a tree with all folders, all subfolders, and all files in them. In this case, the objects of the file module have only a loose relation to the folder module, and we need to define a child loader:

<import_layout layout_id="multi_module_tree">
  <parameter>
    <entry key="moduleStructure" class="map">
      <entry key="folder" class="array">
        <!-- relation folder <-> folder -->
        <value class="map">
          <entry key="moduleName">folder</entry>
          <entry key="relationField">pid</entry>
        </value>
        <!-- relation folder <-> file -->
        <value class="map">
          <entry key="moduleName">file</entry>
          <entry key="childLoader" class="actor">folderFileChildLoader</entry> <!-- MyNamespace-FolderFileChildLoader is NOT a part of this 4App! -->
        </value>
      </entry>
    </entry>
  </parameter>
</import_layout>

In the example above, the relationField is removed and a new parameter named childLoader is inserted. That parameter is of type actor. The actor itself has to be defined separately in the actors element:

<actors>
  <actor id="folderFileChildLoader" type="MyNamespace-FolderFileChildLoader"/>
</actors>

Custom Child Loader

Please note: A child loader differs from use case to use case. The implementation of the exemplary MyNamespace-FolderFileChildLoader is thus NOT part of this 4App.

# Custom Child Loader

A custom child loader has to be implemented in a frontend project. It has to extend the abstract class MultiModuleTreeChildLoader.

In our example with file and folder, the exemplary MyNamespace-FolderFileChildLoader needs to get all files in a folder. To achieve this, it has to check the following:

  • all files whose own parent_path equals the folder's parent_path + / + name (of the folder)
  • the mount of file and folder needs to be the same.

The ChildLoader may look like this:

import {MultiModuleTreeChildLoader} from 'cmweb-multi-module-tree/src/com/cm4ap/ce/actor/MultiModuleTreeChildLoader';
import type {BaseBean} from 'cmweb-core/src/vos/vo/bean/BaseBean';
import {ExternalLayoutActor} from 'cmweb-core/src/main/mvc/Actor';
import {SearchPropertyFilter} from 'cmweb-core/src/vos/vo/request/search/SearchPropertyFilter';
import {ObjectsApiService} from 'cmweb-core/src/services/remoteService/ObjectsApiService';
import {GlobalConstants} from 'cmweb-core/src/modules/global/GlobalConstants';
import {TreeComponentError} from 'cmweb-core/src/components/tree/TreeComponentError';
import {BeanCache} from 'cmweb-core/src/cache/BeanCache';
import {Comparator, LogicalOperator} from 'cmweb-core/src/com/cm4ap/ce/view/objectrenderer/RendererConstants';
import {FileConstants} from 'cmweb-dam/src/modules/file/FileConstants';
import {SortOrder} from 'cmweb-core-openapi/src/models/SortOrder';

@ExternalLayoutActor('MyNamespace', 'FolderFileChildLoader')
export class FolderFileChildLoader extends MultiModuleTreeChildLoader {

    public async loadChildren(folder: BaseBean): Promise<BaseBean[]> {
        if(folder.module_name !== 'folder') {
            throw new Error(
                    'The actor MyNamespace-FolderFileChildLoader is intended for use with module folder as parent');
        }

        const cacheResult = await BeanCache.getByBeans([folder],
                [FileConstants.FIELD_MOUNT, FileConstants.FIELD_PARENT_PATH, FileConstants.FIELD_NAME]);

        folder = cacheResult.cachedBeans[0];
        const mount = folder?.getPropertyString(FileConstants.FIELD_MOUNT);
        if(!mount) {
            return [];
        }
        const parentPath = folder.getPropertyString(FileConstants.FIELD_PARENT_PATH) ?? '';
        const name = folder.getPropertyString(FileConstants.FIELD_NAME);
        if(folder.type !== 'mount' && !name) {
            return [];
        }

        const childPath = parentPath === '' ? name : parentPath + '/' + name;

        const mountFilter = new SearchPropertyFilter({
            operator: LogicalOperator.AND,
            field: FileConstants.FIELD_MOUNT,
            comparator: Comparator.EQUAL,
            value: mount,
        });

        const parentPathFilter = new SearchPropertyFilter({
            operator: LogicalOperator.AND,
            field: FileConstants.FIELD_PARENT_PATH,
            comparator: Comparator.EQUAL,
            value: childPath,
        });

        const searchResult = await ObjectsApiService.getObjectsLegacy({
            module: FileConstants.MODULE_NAME,
            filter: [mountFilter, parentPathFilter],
            sort: [GlobalConstants.FIELD_FRIENDLYNAME + ':' + SortOrder.ASC],
        });

        // assert that a valid result has been passed
        if(!searchResult.result) {
            throw new TreeComponentError(TreeComponentError.REMOTE_ERROR, -1,
                    'L-GLOBAL-ERROR_LOADING_CHILDREN-NO_VALID_RESULT');
        }

        return searchResult.result;
    }

}

# Custom Root Loader

If a tree should not show all root elements of the chosen root modules, but only a selection of the objects as "entry points" for the tree, you can implement a custom root loader.

For example, if a folder/file tree in the detail view of module ce_role should only show all folders and files that belong to a corresponding role, e.g. for Role "Sales", only from folder "Sales":

* Mount: data
    * Departments
        * Sales
            * Subfolder 1
                * File 1
                * File 2
                * File 3
            * Subfolder 2
                * File 4
        * Project Management
            ...
        ...
    ...

The root loader to achieve this needs to extend the class MultiModuleTreeRootLoader and has to be passed as actor to the layout:

<import_layout layout_id="multi_module_tree" module="ce_role">
  <parameter>
    <entry key="moduleStructure" class="map">
      <entry key="folder" class="array">
        <!-- relation folder <-> folder -->
        <value class="map">
          <entry key="moduleName">folder</entry>
          <entry key="relationField">pid</entry>
        </value>
        <!-- relation folder <-> file -->
        <value class="map">
          <entry key="moduleName">file</entry>
          <entry key="childLoader" class="actor">folderFileChildLoader</entry> <!-- MyNamespace-FolderFileChildLoader is NOT a part of this 4App! -->
        </value>
      </entry>
    </entry>
    <entry key="rootLoader" class="actor">rootLoader</entry>
  </parameter>
</import_layout>

A root loader can now be added to the actors section of the layout:

<actors>
  <actor id="rootLoader" type="MyNamespace-CustomRootLoader"/>
</actors>

In this example, the MyNamespace-CustomRootLoader may look like this:

import {BaseBean} from 'cmweb-core/src/vos/vo/bean/BaseBean';
import {MultiModuleTreeRootLoader} from 'cmweb-multi-module-tree/src/com/cm4ap/ce/actor/MultiModuleTreeRootLoader';
import {BeanCache} from 'cmweb-core/src/cache/BeanCache';
import {SearchPropertyFilter} from 'cmweb-core/src/vos/vo/request/search/SearchPropertyFilter';
import {TreeComponentError} from 'cmweb-core/src/components/tree/TreeComponentError';
import {ExternalLayoutActor} from 'cmweb-core/src/main/mvc/Actor';
import {Comparator, LogicalOperator} from 'cmweb-core/src/com/cm4ap/ce/view/objectrenderer/RendererConstants';
import {GlobalConstants} from 'cmweb-core/src/modules/global/GlobalConstants';
import {ObjectsApiService} from 'cmweb-core/src/services/remoteService/ObjectsApiService';
import {SortOrder} from 'cmweb-core-openapi/src/models/SortOrder';

@ExternalLayoutActor('MyNamespace', 'CustomRootLoader')
export class CustomRootLoader extends MultiModuleTreeRootLoader {

    public async loadRootElements(moduleName: string): Promise<BaseBean[]> {
        const friendlyname = (await BeanCache.getByIds(this.context.module, this.context.record,
                [GlobalConstants.FIELD_FRIENDLYNAME])).cachedBeans[0]?.getPropertyString(
                GlobalConstants.FIELD_FRIENDLYNAME);

        const pidFilter = new SearchPropertyFilter({
            operator: LogicalOperator.AND,
            field: GlobalConstants.FIELD_FRIENDLYNAME,
            comparator: Comparator.EQUAL,
            value: friendlyname,
        });

        const searchResult = await ObjectsApiService.getObjectsLegacy({
            module: moduleName,
            filter: [pidFilter],
            sort: [GlobalConstants.FIELD_FRIENDLYNAME + ':' + SortOrder.ASC],
        });

        // assert that a valid result has been passed
        if(!searchResult.result) {
            throw new TreeComponentError(TreeComponentError.REMOTE_ERROR, -1,
                    'L-GLOBAL-ERROR_LOADING_CHILDREN-NO_VALID_RESULT');
        }

        return searchResult.result;
    }

}

# ContextMultiModuleTreeRootLoader

The actor ContextMultiModuleTreeRootLoader is a special root loader implementation that loads root elements based on a relation of the object. E.g., a project module may have a field called project_folder that contains the ID of a folder object. To achieve that in a multi-module tree only the folders and files within this special project folder are displayed, the ContextMultiModuleTreeRootLoader can be used:

<actor id="rootLoader" type="ContextMultiModuleTreeRootLoader">
  <parameter>
    <!-- BeanActor that represents the project -->
    <entry key="beanActor" class="actor">beanActor</entry>
    <!-- Relation to the project module from the view of the folder module -->
    <entry key="relationFieldName">project_project_folder</entry>
  </parameter>
</actor>

# Available Layout Parameters

For the layout change, the following parameters are supported:

Layout Parameter Description Default Value
treeTitle The title of the panel
panelType Defines whether the panel is foldable or not.
Allowed values: foldable, flat
foldable
panelDefaultOpen Defines whether the panel is opened per default. true
showRoot Defines whether the root elements should be shown in the tree. true
addTreeRootFilter Defines whether only pid root elements should be loaded on tree root level. In every non-related MultiModuleTree, it is useful to set it to true. false
showRootPreset When the preset key is set and the preset exists, the value of the preset will be used to determine whether the root elements should be shown.
view The current view for all the objects that will be shown in the tree. When using the default item renderer (MultiModuleTreeItemRenderer), the view will be used to get the conditional operations. main
defaultOpenLevel The number of levels that are open at start 1
defaultOpenLevelPreset When the preset key is set and the preset exists, the value of the preset will be used to determine whether the number of levels that are open at start.
showCOButton Defines whether the conditional operation button should be shown or not. true
itemRenderer The tag name of the item renderer element. cm4ap-multi-module-tree-item-renderer
showItemRendererObjectImageMultiModule A map of booleans. Defines for what objects the object image should be shown.
itemRendererIconNameMultiModule A map of strings. Defines for what objects a specific icon should be shown in the tree.
sortField Defines the field that is used for sorting the objects in one level below on node of the tree.
Allowed values: id, module_name, mod_time, type, created_time, friendlyname, created_by, mod_by, mod_time_img
friendlyname
sortOrder Defines whether the sorting is ascending or descending.
Allowed values: asc, desc
asc
Request missing documentation