# 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 tofalse
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 totrue
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 classmap
. - 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'sparent_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 |