Skip to content

Tree

Available since v3.0.1

Tree widget presents hierarchical records as an expandable tree, so users can browse parent and child rows in one table.

Info

The following features are not available for the Tree widget in this release:

  • fully or partially expanded initial load (the tree always opens collapsed)

tree.png

Basics

Live Sample · GitHub

For the widget to work correctly, the following requirements must be met:

  • A parent-child relationship must be defined between the records. Both standard fields and custom fields can be used, with the standard logic overridden if necessary.
  • A flag must be provided to determine whether the tree can be expanded further. It indicates whether the current record is a leaf node or has child records.

The minimal data set of a tree record is:

  • id — the identifier of the record.
  • parentId — the identifier of the parent record. For a root record the value is null. The backend must always return this field, and the field must be filterable, because the tree is built with the requests parentId.specified=false (root records) and parentId.equals=<id> (child records). see more Lazy load
  • a name (title) field — the value shown in the tree column.
  • isLeaf — a computed boolean flag. The default value is false. The value true means that the record has no child records, so the expand arrow is not displayed for it.

Info

The first field of the fields array is rendered as the tree column: the expand arrow. All the other fields are rendered as ordinary columns.

options.tree

All tree specific settings are placed in options.tree of .widget.json.

Example
Property Type Default Description
parentIdFieldKey String parentId Name of the field that holds the identifier of the parent record.
isLeafFieldKey String isLeaf Name of the field that holds the flag "the record has no child records".
searchModes Array of collapse, hide ["collapse", "hide"] Modes of displaying the filtration result. The first item of the array is the active one. see Search modes
onFilterApplyNestLevel Number 0 How many levels of the navigation path are shown above a found record in collapse mode. see Search modes
insertPosition start, end — Where a newly created row is inserted inside its node. see Actions

Lazy load

The tree is always loaded lazily and always opens collapsed: only the root records are requested when the widget is opened, and the child records of a node are requested when the user expands it.

Info

The operation specified is used only to select the records with an empty parent. For all the other requests the operation equals is used. This is why the field that holds the parent identifier must be filterable on the backend.

Every node keeps its own pagination state, so the records of one node are loaded page by page independently of the neighbouring nodes. see more Pagination

Example
  • Root records are requested with the filter by an empty parent:
?parentId.specified=false&_page=1&_limit=5
  • Child records of a node are requested when the node is expanded:
?parentId.equals=<id>&_page=1&_limit=5

How does it look?

tree.png

How to add?

Example

You have the option to utilized default field names for standard properties such as parentId, isLeaf. When doing so, you'll not need to establish mappings for these fields to standard criteria

Step1 Create field parentId, isLeaf to corresponding DataResponseDTO.

  • parentId — identifies the parent record of the current record and defines the parent-child relationship in the tree. If the record is a root node, parentId must be empty.
  • isLeaf — indicates whether the record can be expanded. The value true means that the record cannot be expanded, while false means that the record can be expanded to display child records.
@Getter
@Setter
@NoArgsConstructor
public class MyExampleDTO extends DataResponseDTO {

    @SearchParameter(name = "customField")
    private String customField;
    @SearchParameter(name = "parentId", provider = LongValueProvider.class)
    private Long parentId;
    private Boolean isLeaf;

    public MyExampleDTO(Myexample entity) {
        this.id = entity.getId().toString();
        this.customField = entity.getCustomField();

        this.parentId = entity.getParentId();
        this.isLeaf = entity.getChildren().isEmpty();
    }
}

Step2 Create file .widget.json with type = "Tree"

Add existing field to a tree widget. see more Fields

For the tree to work correctly, the widget must contain the parentId and isLeaf fields. These fields should be configured as hidden.

{
  "name": "MyExampleTree",
  "title": "Default fields",
  "type": "Tree",
  "bc": "myExampleBc",
  "fields": [
    {
      "title": "Custom Field",
      "key": "customField",
      "type": "input"
    },
    {
      "title": "Is Leaf",
      "key": "isLeaf",
      "type": "hidden"
    },
    {
      "title": "Parent Id",
      "key": "parentId",
      "type": "hidden"
    }
  ],
  "options": {
    "edit": {
      "style": "none"
    },
    "actionGroups": {
      "include": [
      ]
    }
  }
}
Live Sample · GitHub

You have the option to utilize custom field names for standard properties. When doing so, you'll need to establish mappings for these fields to standard criteria

Step1 Create field customFieldParentId, customFieldIsLeaf to corresponding DataResponseDTO.

  • customFieldParentId — identifies the parent record of the current record and defines the parent-child relationship in the tree. If the record is a root node, parentId must be empty.
  • customFieldIsLeaf — indicates whether the record can be expanded. The value true means that the record cannot be expanded, while false means that the record can be expanded to display child records.
@Getter
@Setter
@NoArgsConstructor
public class MyExampleDTO extends DataResponseDTO {

    @SearchParameter(name = "customField")
    private String customField;
    @SearchParameter(name = "parentId", provider = LongValueProvider.class)
    private Long customFieldParentId;
    private Boolean customFieldIsLeaf;

    public MyExampleDTO(Myexample entity) {
        this.id = entity.getId().toString();
        this.customField = entity.getCustomField();

        this.customFieldParentId = entity.getParentId();
        this.customFieldIsLeaf = entity.getChildren().isEmpty();
    }
}

Step2 Create file .widget.json with type = "Tree"

Add existing field to a tree widget. see more Fields

For the tree to work correctly, the widget must contain the customFieldParentId and customFieldIsLeaf fields. These fields should be configured as hidden.

{
  "name": "MyExampleTree",
  "title": "Custom Fields",
  "type": "Tree",
  "bc": "myExampleBc",
  "fields": [
    {
      "title": "Custom Field",
      "key": "customField",
      "type": "input"
    },
    {
      "title": "Is Leaf",
      "key": "customFieldIsLeaf",
      "type": "hidden"
    },
    {
      "title": "Parent Id",
      "key": "customFieldParentId",
      "type": "hidden"
    }
  ],
  "options": {
    "tree": {
      "isLeafFieldKey": "customFieldIsLeaf",
      "parentIdFieldKey": "customFieldParentId"
    },
    "edit": {
      "style": "none"
    },
    "actionGroups": {
      "include": [
      ]
    }
  }
}
Live Sample · GitHub

Title

Live Sample · GitHub

Title Basic

Title for widget (optional)

treewidgetinf.png

There are types of:

  • constant title: shows constant text.
  • constant title empty: if you want to visually connect widgets by them to be placed one under another

How does it look?

consttitle.png

empytitle.png

How to add?

Example

Step1 Add name for title to .widget.json.

{
  "name": "MyExampleTree",
  "title": "Constant Title",
  "type": "Tree",
  "bc": "myExampleBc",
  "fields": [
    {
      "title": "Custom Field",
      "key": "customField",
      "type": "input"
    },
    {
      "title": "Is Leaf",
      "key": "isLeaf",
      "type": "hidden"
    },
    {
      "title": "Parent Id",
      "key": "parentId",
      "type": "hidden"
    }
  ],
  "options": {
    "tree": {
      "searchModes": [
        "collapse",
        "hide"
      ]
    },
    "edit": {
      "style": "none"
    },
    "actionGroups": {
      "include": [
      ]
    }
  }
}
Live Sample · GitHub

Step1 Delete parameter title to .widget.json.

{
  "name": "MyExampleEmptyTitle",
  "type": "Tree",
  "bc": "myExampleBc",
  "fields": [
    {
      "title": "Custom Field widget: Empty Title",
      "key": "customField",
      "type": "input"
    },
    {
      "title": "Is Leaf",
      "key": "isLeaf",
      "type": "hidden"
    },
    {
      "title": "Parent Id",
      "key": "parentId",
      "type": "hidden"
    }
  ],
  "options": {
    "tree": {
      "searchModes": [
        "collapse",
        "hide"
      ]
    },
    "edit": {
      "style": "none"
    },
    "actionGroups": {
      "include": [
      ]
    }
  }
}

Live Sample · GitHub

Title Color

Title Color allows you to specify a color for a title. It can be constant or calculated.

Constant color

Live Sample · GitHub

Constant color is a fixed color that doesn't change. It remains the same regardless of any factors in the application.

Calculated color

Live Sample · GitHub

Calculated color can be used to change a title color dynamically. It changes depending on business logic or data in the application.

Info

Title colorization is applicable to the following fields: date, dateTime, dateTimeWithSeconds, number, money, percent, time, input, text, dictionary, radio, checkbox, pickList, inlinePickList, multivalue, multivalueHover.

How does it look?

colorwidget.png

How to add?
Example

Step 1 Add custom field for color to corresponding DataResponseDTO. The field can contain a HEX color or be null.

    public MyExampleDTO(Myexample entity) {
        this.id = entity.getId().toString();
        this.customField = entity.getCustomField();
        this.customFieldText = entity.getCustomFieldText();
        this.customFieldColor = "#edaa";
        this.customFieldTextColor = "#aeda";

Step 2 Add "bgColorKey" : custom field for color and to .widget.json.

Add in title field with ${customField}

{
  "name": "MyExample",
  "title": "customField: ${customField},  customFieldText: ${customFieldText}",
  "type": "Tree",
  "bc": "myexample",
  "fields": [
    {
      "title": "Custom Field Color",
      "key": "customField",
      "type": "input",
      "bgColorKey": "customFieldColor"
    },
    {
      "title": "Custom Field Text",
      "key": "customFieldText",
      "type": "input",
      "bgColorKey": "customFieldTextColor"
    },
    {
      "title": "Is Leaf",
      "key": "isLeaf",
      "type": "hidden"
    },
    {
      "title": "Parent Id",
      "key": "parentId",
      "type": "hidden"
    }
  ]
}

Live Sample · GitHub

Add "bgColor" : HEX color to .widget.json.

Add in title field with ${customField}

{
  "name": "MyExampleColorConst",
  "title": "customField: ${customField},  customFieldText: ${customFieldText}",
  "type": "Tree",
  "bc": "myexample",
  "fields": [
    {
      "title": "Custom Field Color",
      "key": "customField",
      "type": "input",
      "bgColor": "#edaa"
    },
    {
      "title": "Custom Field Text",
      "key": "customFieldText",
      "type": "input",
      "bgColor": "#aeda"
    },
    {
      "title": "Is Leaf",
      "key": "isLeaf",
      "type": "hidden"
    },
    {
      "title": "Parent Id",
      "key": "parentId",
      "type": "hidden"
    }
  ]
}

Live Sample · GitHub

Business component

This specifies the business component (BC) to which this form belongs. A business component represents a specific part of a system that handles a particular business logic or data.

see more Business component

Show condition

Live Sample · GitHub

  • no show condition - recommended: widget always visible

Live Sample · GitHub

  • show condition by current entity: condition can include boolean expression depending on current entity fields. Field updates will trigger condition recalculation only on save or if field is force active

Tips

It is recommended not to use Show condition when possible, because wide usage of this feature makes application hard to support.

How does it look?

tree.png

How to add?

Example

see Basic

Step1 Add showCondition to .widget.json. see more showCondition

{
  "name": "MyExample",
  "title": "Show condition widget",
  "type": "Tree",
  "bc": "myExampleBc",
  "showCondition": {
    "bcName": "myExampleBc",
    "params": {
      "fieldKey": "customFieldShowCond",
      "value": "true"
    }
  },
  "fields": [
    {
      "title": "Custom Field Input",
      "key": "customFieldInput",
      "type": "input"
    },
    {
      "title": "Custom Field, bc - myExampleBc",
      "key": "customField",
      "type": "number"
    },
    {
      "title": "Is Leaf",
      "key": "isLeaf",
      "type": "hidden"
    },
    {
      "title": "Parent Id",
      "key": "parentId",
      "type": "hidden"
    }
  ],
  "options": {
    "tree": {
      "searchModes": [
        "collapse",
        "hide"
      ]
    },
    "edit": {
      "style": "none"
    },
    "actionGroups": {
      "include": [
      ]
    }
  }
}

Fields

Fields Configuration. The fields array defines the individual fields present within the form.

{
    "title": "Custom Field",
    "key": "customField",
    "type": "input"
}
  • "title"

Description: Field Title.

Type: String(optional).

  • "key"

    Description: Name field to corresponding DataResponseDTO.

    Type: String(required).

  • "type"

Description: Field types

Type: String(required).

How to add?

Example

Step 1 Download plugin download Intellij Plugin

Step 2 Add existing field to an existing form widget addfield.gif

Add field to .widget.json.

   {
     "name": "MyExampleTree",
     "title": "Constant Title",
     "type": "Tree",
     "bc": "myExampleBc",
     "fields": [
       {
         "title": "Custom Field",
         "key": "customField",
         "type": "input"
       },
       {
         "title": "Is Leaf",
         "key": "isLeaf",
         "type": "hidden"
       },
       {
         "title": "Parent Id",
         "key": "parentId",
         "type": "hidden"
       }
     ],
     "options": {
       "tree": {
         "searchModes": [
           "collapse",
           "hide"
         ]
       },
       "edit": {
         "style": "none"
       },
       "actionGroups": {
         "include": [
         ]
       }
     }
   }

Options layout

options.layout - no use in this type.

Actions

Actions show available actions as separate buttons see more Actions.

As for Tree widget, there are several actions. They are configured exactly like the actions of a List widget: the same create, edit, save, cancel-create, delete actions and the same three ways of creating and editing a record — inline, inline-form and with view.

Create

Create button enables you to create a new value by clicking the Add button. This action can be performed in three different ways, feel free to choose any, depending on your logic of application:

There are three methods to create a record:

  • Inline: You can add a line directly.

Info

Pagination won't function until the page is refreshed after adding records.

  • Inline-form: You can add data using a form widget without leaving your current view.

  • With view: You can create a record by navigating to a view.

Inline

Live Sample · GitHub

With Line Addition, a new empty row is immediately added to the top of the tree widget when the "Add" button is clicked. This is a quick way to add rows without needing to input data beforehand.

The position of the newly created line within the node can be determined as follows:

  • places it before the already loaded rows of the node
  • places it after them.
How does it look?

create_inline.png

How to add?
Example

Step1 Add button create to corresponding VersionAwareResponseService.

    @Override
    public Actions<MyExampleDTO> getActions() {
        return Actions.<MyExampleDTO>builder()
                .create(crt -> crt.text("Add"))
                .save(sv -> sv.text("Save"))
                .cancelCreate(ccr -> ccr.text("Cancel").available(bc -> true))
                .delete(dlt -> dlt.text("Delete"))
                .action(act -> act
                        .action("customSave", "custom Save")
                        .scope(ActionScope.RECORD)
                        .invoker(this::customSaveInvoker)
                )
                .action(act -> act
                        .action("customSaveWithRefresh", "custom Save With Refresh")
                        .scope(ActionScope.RECORD)
                        .invoker(this::customSaveInvokerWithRefresh)
                )
                .action(act -> act
                        .action("customDeleteConfirm", "custom Delete Confirm")
                        .withPreAction(PreAction.confirm(cf -> cf
                                .text("You want to delete the value?")
                        ))
                        .invoker((bc, dto) ->
                        {
                            this.deleteEntity(bc);
                            return new ActionResultDTO<MyExampleDTO>();
                        })
                )
                .action(act -> act
                        .action("customDelete", "custom Delete")
                        .invoker((bc, dto) ->
                        {
                            this.deleteEntity(bc);
                            return new ActionResultDTO<MyExampleDTO>();
                        })
                )
                .action(act -> act
                        .action("saveWaitUntil", "save Wait Until")
                        .scope(ActionScope.RECORD)
                        .invoker((bc, dto) -> {
                            MyEntity myEntity = repository.findById(bc.getIdAsLong()).orElseThrow();
                            myEntity.setCustomFieldDictionary(CustomFieldDictionaryEnum.HIGH);
                            repository.save(myEntity);
                            return new ActionResultDTO<MyExampleDTO>().setAction(
                                    PostAction.waitUntil(
                                                    MyExampleDTO_.customFieldDictionary,
                                                    CustomFieldDictionaryEnum.HIGH)
                                            .timeoutMaxRequests(6).timeout(Duration.ofSeconds(12)).build());
                        }))
                .build();
    }
Step2 Add button create to corresponding .widget.json.

The position of a newly created row inside its node is defined by

options.tree.insertPosition start places it before the already loaded rows of the node, end places it after them

{
  "name": "MyExampleTree",
  "title": "Tree",
  "type": "Tree",
  "bc": "myexample",
  "fields": [
    {
      "title": "Custom Field",
      "key": "customField",
      "type": "input"
    },
    {
      "title": "customFieldMoney",
      "key": "customFieldMoney",
      "digits": 2,
      "type": "money"
    },
    {
      "title": "Is Leaf",
      "key": "isLeaf",
      "type": "hidden"
    },
    {
      "title": "Parent Id",
      "key": "parentId",
      "type": "number"
    },
    {
      "title": "Id",
      "key": "id",
      "type": "number"
    },
    {
      "title": "custom Field Dictionary",
      "key": "customFieldDictionary",
      "type": "dictionary"
    }
  ],
  "options": {
    "tree": {
      "insertPosition": "start"
    },
    "actionGroups": {
      "include": [
        "create",
        "delete",
        "save",
        "customSave",
        "customSaveWithRefresh",
        "customDelete",
        "customDeleteConfirm",
        "saveWaitUntil",
        "cancel-create"
      ]
    }
  }
}
Step3 Add fields.setEnabled to corresponding FieldMetaBuilder.
    public void buildRowDependentMeta(RowDependentFieldsMeta<MyExampleDTO> fields, InnerBcDescription bcDescription,
                                      Long id, Long parentId) {
        fields.setEnumValues(MyExampleDTO_.customFieldDictionary, CustomFieldDictionaryEnum.values());
        fields.setEnabled(MyExampleDTO_.customFieldDictionary);
        fields.setEnabled(MyExampleDTO_.customFieldMoney);
        fields.setEnabled(MyExampleDTO_.parentId);
        fields.setEnabled(MyExampleDTO_.isLeaf);
        fields.setEnabled(MyExampleDTO_.customFieldText);
        fields.setEnabled(MyExampleDTO_.customField);
    }
Live Sample · GitHub

Inline-form

Live Sample · GitHub

Create with widget opens an additional widget when the "Add" button is clicked. The form will appear on the same screen, allowing you to view both the tree of entities and the form for adding a new row. After filling the information in and clicking "Save", the new row is added to the Tree.

The position of a newly created row inside its node is defined :

  • places it before the already loaded rows of the node
  • places it after them.
How does it look?

create_inline_form.png

How to add?
Example

Step1 Add button create to corresponding VersionAwareResponseService.

    @Override
    public Actions<MyExampleDTO> getActions() {
        return Actions.<MyExampleDTO>builder()
                .create(crt -> crt.text("Add"))
                .save(sv -> sv.text("Save"))
                .cancelCreate(ccr -> ccr.text("Cancel").available(bc -> true))
                .delete(dlt -> dlt.text("Delete"))
                .action(act -> act
                        .action("customSave", "custom Save")
                        .scope(ActionScope.RECORD)
                        .invoker(this::customSaveInvoker)
                )
                .build();
    }
Step2 Add fields.setEnabled to corresponding FieldMetaBuilder.
    public void buildRowDependentMeta(RowDependentFieldsMeta<MyExampleDTO> fields, InnerBcDescription bcDescription,
                                      Long id, Long parentId) {
        fields.setEnabled(MyExampleDTO_.customFieldMoney);
        fields.setEnabled(MyExampleDTO_.parentId);
        fields.setEnabled(MyExampleDTO_.isLeaf);
        fields.setEnabled(MyExampleDTO_.customFieldText);
        fields.setEnabled(MyExampleDTO_.customField);
    }

Step3 Create widget.json with type Form that appears when you click a button

{
  "title": "",
  "name": "myEntityCreateForm",
  "type": "Form",
  "bc": "myexample",
  "fields": [
    {
      "label": "parentId",
      "key": "parentId",
      "type": "number"
    },
    {
      "label": "Custom Field",
      "key": "customField",
      "type": "input"
    },
    {
      "label": "customFieldMoney",
      "key": "customFieldMoney",
      "digits": 2,
      "type": "money"
    }
  ],
  "options": {
    "layout": {
      "rows": [
        {
          "cols": [
            {
              "fieldKey": "parentId",
              "span": 12
            }
          ]
        },
        {
          "cols": [
            {
              "fieldKey": "customField",
              "span": 12
            }
          ]
        },
        {
          "cols": [
            {
              "fieldKey": "customFieldMoney",
              "span": 12
            }
          ]
        }
      ]
    },
    "actionGroups": {
      "include": [
        "save",
        "cancel-create"
      ]
    }
  }
}

Step4 Add widget.json with type Form to corresponding .view.json.

{
  "name": "myexampletree",
  "title": "Create inline-form",
  "template": "DashboardView",
  "url": "/screen/myexample/view/myexampletree",
  "widgets": [
    {
      "widgetName": "SecondLevelMenu",
      "position": 0,
      "gridWidth": 24
    },
    {
      "widgetName": "ThirdLevelMenu",
      "position": 10,
      "gridWidth": 24
    },
    {
      "widgetName": "myEntityCreateForm",
      "position": 20,
      "gridWidth": 12
    },
    {
      "widgetName": "MyExampleTree",
      "position": 30,
      "gridWidth": 24
    }
  ],
  "rolesAllowed": [
    "CXBOX_USER"
  ]
}

Step5 Add button create and widget with type Form to corresponding .widget.json.

options.create: Name widget that appears when you click a button

The position of a newly created row inside its node is defined by

options.tree.insertPosition start places it before the already loaded rows of the node, end places it after them

{
  "name": "MyExampleTree",
  "title": "Tree widget action create inline-form",
  "type": "Tree",
  "bc": "myexample",
  "fields": [
    {
      "title": "customField",
      "key": "customField",
      "type": "input"
    },
    {
      "title": "customFieldMoney",
      "key": "customFieldMoney",
      "digits": 2,
      "type": "money"
    },
    {
      "title": "Is Leaf",
      "key": "isLeaf",
      "type": "hidden"
    },
    {
      "title": "Parent Id",
      "key": "parentId",
      "type": "hidden"
    },
    {
      "title": "Id",
      "key": "id",
      "type": "number"
    }
  ],
  "options": {
    "tree": {
      "insertPosition": "start"
    },
    "create": {
      "widget": "myEntityCreateForm"
    },
    "edit": {
      "widget": "myEntityCreateForm"
    },
    "actionGroups": {
      "include": [
        "create",
        "delete"
      ]
    }
  }
}

Live Sample · GitHub

With view

Live Sample · GitHub

With Create with view, clicking the "Add" button opens a separate view that displays only the data entry form. After completing the form and saving, the system returns to the tree of entities with the new row added.

How does it look?

create_with_view.png

How to add?
Example

Step1 Add button create to corresponding VersionAwareResponseService.

    @Override
    public Actions<MyExampleDTO> getActions() {
        return Actions.<MyExampleDTO>builder()
                .create(crt -> crt.text("Add"))
                .action(act -> act
                        .action("finish", "Save and Close")
                        .invoker((bc, dto) -> {
                            MyEntity myEntity = repository.getReferenceById(bc.getIdAsLong());
                            repository.save(myEntity);
                            return new ActionResultDTO<MyExampleDTO>().setAction(
                                    PostAction.drillDown(
                                            DrillDownType.INNER,
                                            "/screen/myexample/view/myexampletree"
                                    ));
                        })
                )
                .action(act -> act
                        .action("customSave", "custom Save")
                        .scope(ActionScope.RECORD)
                        .invoker(this::customSaveInvoker)
                )
                .action(act -> act
                        .action("customDelete", "custom Delete")
                        .withPreAction(PreAction.confirm(cf -> cf
                                .text("You want to delete the value?")
                        ))
                        .invoker((bc, dto) ->
                        {
                            this.deleteEntity(bc);
                            return new ActionResultDTO<MyExampleDTO>();
                        })
                )
                .build();
    }
Step2 Add PostAction.drillDown to method doCreateEntity to corresponding VersionAwareResponseService.
    @Override
    protected CreateResult<MyExampleDTO> doCreateEntity(MyEntity entity, BusinessComponent bc) {
        repository.save(entity);
        return new CreateResult<>(entityToDto(bc, entity))
                .setAction(PostAction.drillDown(
                        DrillDownType.INNER,
                        "/screen/myexample/view/myexampleform/"
                                + CxboxMyExampleController.myexample + "/"
                                + entity.getId()));
    }
Step3 Add button create to corresponding .widget.json.

{
  "name": "MyExampleTree",
  "title": "Tree widget action create with view",
  "type": "Tree",
  "bc": "myexample",
  "fields": [
    {
      "title": "customField",
      "key": "customFieldRO",
      "type": "input"
    },
    {
      "title": "customField",
      "key": "customFieldMoneyRO",
      "digits": 2,
      "type": "money"
    },
    {
      "title": "Is Leaf",
      "key": "isLeaf",
      "type": "hidden"
    },
    {
      "title": "Parent Id",
      "key": "parentId",
      "type": "hidden"
    }
  ],
  "options": {
    "actionGroups": {
      "include": [
        "create"
      ]
    }
  }
}

Live Sample · GitHub

Edit

Edit enables you to change the field value. Just like with Create button, there are three ways of implementing this Action.

There are three methods to create a record:

  • Inline edit: You can edit a line directly.

  • Inline-form: You can edit data using a form widget without leaving your current view.

  • With view: You can edit a record by navigating to a view.

Inline edit

Live Sample · GitHub

Edit Inline implies inline-edit. Click twice on the value you want to change.

How does it look?

edit_inline.png

How to add?
Example

Step1 Add fields.setEnabled to corresponding FieldMetaBuilder.

    @Override
    public void buildRowDependentMeta(RowDependentFieldsMeta<MyExampleDTO> fields, InnerBcDescription bcDescription,
                                      Long id, Long parentId) {
        fields.setEnabled(MyExampleDTO_.customFieldMoney);
        fields.setEnabled(MyExampleDTO_.parentId);
        fields.setEnabled(MyExampleDTO_.isLeaf);
        fields.setEnabled(MyExampleDTO_.customFieldText);
        fields.setEnabled(MyExampleDTO_.customField);
    }

Live Sample · GitHub

Inline-form

Live Sample · GitHub

Edit with widget opens an additional widget when clicking the edit icon in the row.

How does it look?

edit_inline_form.png

How to add?
Example

Step1 Add action save to corresponding VersionAwareResponseService: the form saves the record with it.

    @Override
    public Actions<MyExampleDTO> getActions() {
        return Actions.<MyExampleDTO>builder()
                .save(sv -> sv.text("Save"))
                .action(act -> act
                        .action("edit", "Edit"))
                .cancelCreate(ccr -> ccr.text("Cancel").available(bc -> true))
                .build();
    }

Step2 Add fields.setEnabled to corresponding FieldMetaBuilder.

    public void buildRowDependentMeta(RowDependentFieldsMeta<MyExampleDTO> fields, InnerBcDescription bcDescription,
                                      Long id, Long parentId) {
        fields.setEnabled(MyExampleDTO_.customFieldMoney);
        fields.setEnabled(MyExampleDTO_.parentId);
        fields.setEnabled(MyExampleDTO_.isLeaf);
        fields.setEnabled(MyExampleDTO_.customFieldText);
        fields.setEnabled(MyExampleDTO_.customField);
    }

Step3 Create widget.json with type Form that appears when you click a button

{
  "title": "",
  "name": "myEntityEditForm",
  "type": "Form",
  "bc": "myexample",
  "fields": [
    {
      "label": "Custom Field",
      "key": "customField",
      "type": "input"
    },
    {
      "label": "customField",
      "key": "customFieldMoney",
      "digits": 2,
      "type": "money"
    }
  ],
  "options": {
    "layout": {
      "rows": [
        {
          "cols": [
            {
              "fieldKey": "customField",
              "span": 12
            }
          ]
        },
        {
          "cols": [
            {
              "fieldKey": "customFieldMoney",
              "span": 12
            }
          ]
        }
      ]
    },
    "actionGroups": {
      "include": [
        "save",
        "cancel-create"
      ]
    }
  }
}

Step4 Add widget.json with type Form to corresponding .view.json.

{
  "name": "myexampletree",
  "title": "Edit inline-form",
  "template": "DashboardView",
  "url": "/screen/myexample/view/myexampletree",
  "widgets": [
    {
      "widgetName": "SecondLevelMenu",
      "position": 0,
      "gridWidth": 24
    },
    {
      "widgetName": "ThirdLevelMenu",
      "position": 10,
      "gridWidth": 24
    },
    {
      "widgetName": "myEntityEditForm",
      "position": 20,
      "gridWidth": 24
    },
    {
      "widgetName": "MyExampleTree",
      "position": 30,
      "gridWidth": 24
    }
  ],
  "rolesAllowed": [
    "CXBOX_USER"
  ]
}

Step5 Add widget with type Form to options.edit.widget of corresponding .widget.json: the edit icon appears in each row.

options.edit: Name widget that appears when you click a button

{
  "name": "MyExampleTree",
  "title": "Tree widget action edit inline-form",
  "type": "Tree",
  "bc": "myexample",
  "fields": [
    {
      "title": "customField",
      "key": "customField",
      "type": "input"
    },
    {
      "title": "customField",
      "key": "customFieldMoney",
      "digits": 2,
      "type": "money"
    },
    {
      "title": "Is Leaf",
      "key": "isLeaf",
      "type": "hidden"
    },
    {
      "title": "Parent Id",
      "key": "parentId",
      "type": "hidden"
    }
  ],
  "options": {
    "edit": {
      "widget": "myEntityEditForm"
    }
  }
}

Live Sample · GitHub

With view

Live Sample · GitHub

With Edit with view, you can edit the entity from a separate view that displays only the data entry form. Click on the "Edit" option in the three-dot menu.

How does it look?

edit_with_view.png

How to add?
Example

Step1 Add action edit to corresponding VersionAwareResponseService.

Add PostAction.drillDown to method edit

    @Override
    public Actions<MyExampleDTO> getActions() {
        return Actions.<MyExampleDTO>builder()
                .action(act -> act
                        .action("finish", "Save and Close")
                        .invoker((bc, dto) -> {
                            MyEntity myEntity = repository.getReferenceById(bc.getIdAsLong());
                            repository.save(myEntity);
                            return new ActionResultDTO<MyExampleDTO>().setAction(
                                    PostAction.drillDown(
                                            DrillDownType.INNER,
                                            "/screen/myexample/view/myexampletree"
                                    ));
                        })
                )
                .addGroup(
                        "actions",
                        "Actions",
                        0,
                        Actions.<MyExampleDTO>builder()
                                .action(act -> act
                                        .action("edit", "Edit")
                                        .withoutAutoSaveBefore()
                                        .invoker((bc, data) -> {
                                            return new ActionResultDTO<MyExampleDTO>()
                                                    .setAction(PostAction.drillDown(
                                                            DrillDownType.INNER,
                                                            "/screen/myexample/view/myexampleform/"
                                                                    + CxboxMyExampleController.myexample + "/"
                                                                    + bc.getId()
                                                    ));
                                        })).build()
                )
                .build();
    }
Step2 Add button or group button to corresponding .widget.json.

{
  "name": "MyExampleTree",
  "title": "Tree widget action edit with view",
  "type": "Tree",
  "bc": "myexample",
  "fields": [
    {
      "title": "customField",
      "key": "customField",
      "type": "input"
    },
    {
      "title": "customField",
      "key": "customFieldMoney",
      "digits": 2,
      "type": "money"
    },
    {
      "title": "Is Leaf",
      "key": "isLeaf",
      "type": "hidden"
    },
    {
      "title": "Parent Id",
      "key": "parentId",
      "type": "hidden"
    }
  ],
  "options": {
    "actionGroups": {
      "include": [
        "actions"
      ]
    }
  }
}
Live Sample · GitHub

Additional properties

Node refresh

The difference from a List widget is what is refreshed after an action. A List widget refreshes the whole page, a Tree widget refreshes only the node the record belongs to:

  • the row is updated from the response of the action or, if the response does not contain it, re-read with the request ?id.equals=<id>;
  • the row-meta of the row is requested again;
  • if the record is not returned any more, the row is removed from the tree;
  • the node is collapsed and its already loaded child records are forgotten, so they are loaded again on the next expand.

Info

Sibling records and parent records are not refreshed automatically. PostAction.refreshBC` is not supported for a Tree widget in this release: the root page is loaded again, but the expanded nodes and their already loaded child records stay as they are. If an action changes other records, reload the view.

Requests Because only the node is refreshed, a Tree widget performs fewer requests than a List widget.

Action List widget Tree widget
Delete DELETE /data + GET /data + GET /row-meta + GET /count DELETE /data + GET /row-meta
Save PUT /data + GET /data + GET /row-meta + GET /count PUT /data + GET /row-meta
Cancel-create DELETE /data + GET /data + GET /row-meta + GET /count DELETE /data + GET /row-meta

Example: the record action custom Save of the sample below updates the record; the tree refreshes only its node. Live Sample · GitHub

Example

    @Override
    public Actions<MyExampleDTO> getActions() {
        return Actions.<MyExampleDTO>builder()
                .create(crt -> crt.text("Add"))
                .save(sv -> sv.text("Save"))
                .cancelCreate(ccr -> ccr.text("Cancel").available(bc -> true))
                .delete(dlt -> dlt.text("Delete"))
                .action(act -> act
                        .action("customSave", "custom Save")
                        .scope(ActionScope.RECORD)
                        .invoker(this::customSaveInvoker)
                )
                .action(act -> act
                        .action("customSaveWithRefresh", "custom Save With Refresh")
                        .scope(ActionScope.RECORD)
                        .invoker(this::customSaveInvokerWithRefresh)
                )
                .action(act -> act
                        .action("customDeleteConfirm", "custom Delete Confirm")
                        .withPreAction(PreAction.confirm(cf -> cf
                                .text("You want to delete the value?")
                        ))
                        .invoker((bc, dto) ->
                        {
                            this.deleteEntity(bc);
                            return new ActionResultDTO<MyExampleDTO>();
                        })
                )
                .action(act -> act
                        .action("customDelete", "custom Delete")
                        .invoker((bc, dto) ->
                        {
                            this.deleteEntity(bc);
                            return new ActionResultDTO<MyExampleDTO>();
                        })
                )
                .action(act -> act
                        .action("saveWaitUntil", "save Wait Until")
                        .scope(ActionScope.RECORD)
                        .invoker((bc, dto) -> {
                            MyEntity myEntity = repository.findById(bc.getIdAsLong()).orElseThrow();
                            myEntity.setCustomFieldDictionary(CustomFieldDictionaryEnum.HIGH);
                            repository.save(myEntity);
                            return new ActionResultDTO<MyExampleDTO>().setAction(
                                    PostAction.waitUntil(
                                                    MyExampleDTO_.customFieldDictionary,
                                                    CustomFieldDictionaryEnum.HIGH)
                                            .timeoutMaxRequests(6).timeout(Duration.ofSeconds(12)).build());
                        }))
                .build();
    }
Live Sample · GitHub

Customization of displayed columns

Live Sample · GitHub

To customize the columns displayed on a tree widget, you can perform two main actions:

  • Hide columns
  • Swap columns

Info

Currently, table customization data is stored within internal tables, even when microservices are used.

Basic

When customizing columns, records are inserted into the ADDITIONAL_FIELDS table. Table ADDITIONAL_FIELDS for store user-specific settings:

  • user_id: The user ID for which the columns are being customized.
  • view: The name of the view where the columns are customized.
  • widget: The name of the widget where the columns are customized.
  • order_fields: When configuring swap columns, the field sequence will be updated, and a new comma-separated sequence of fields will be saved.
  • added_to_additional_fields: User-hidden fields.
  • removed_from_additional_fields: Contains a list of fields that were initially hidden for the user but were later made visible. When the user opens a widget and chooses to display fields that were previously hidden, those fields are recorded in this column.
How does it look?

columns_menu.png

columns_all.png

columns_hidden.png

How to add?
Example

Add in options parameter additional to corresponding .widget.json.

"additional": {
  "enabled": true
}
{
  "name": "MyExampleTree",
  "title": "Tree widget customization columns",
  "type": "Tree",
  "bc": "myexample",
  "fields": [
    {
      "title": "Custom Field Input",
      "key": "customField",
      "type": "input"
    },
    {
      "title": "Custom Field Percent",
      "key": "customFieldPercent",
      "type": "percent"
    },
    {
      "title": "Custom Field Radio",
      "key": "customFieldRadio",
      "type": "radio"
    },
    {
      "title": "Is Leaf",
      "key": "isLeaf",
      "type": "hidden"
    },
    {
      "title": "Parent Id",
      "key": "parentId",
      "type": "hidden"
    }
  ],
  "options": {
    "additional": {
      "enabled": true
    }
  }
}

Live Sample · GitHub

Can also set columns to be pre-hidden, meaning they will be hidden when the widget opens.

Add in options parameter additional to corresponding .widget.json.

Add in options parameter fields with list of hidden fields to corresponding .widget.json.

   "additional": {
      "fields": ["customFieldPercent", "customFieldRadio"],
      "enabled": true
    }
{
  "name": "MyExampleTreeHiddenFields",
  "title": "Tree widget customization columns hidden fields",
  "type": "Tree",
  "bc": "myexample",
  "fields": [
    {
      "title": "Custom Field Input",
      "key": "customField",
      "type": "input"
    },
    {
      "title": "Custom Field Percent",
      "key": "customFieldPercent",
      "type": "percent"
    },
    {
      "title": "Custom Field Radio",
      "key": "customFieldRadio",
      "type": "radio"
    },
    {
      "title": "Is Leaf",
      "key": "isLeaf",
      "type": "hidden"
    },
    {
      "title": "Parent Id",
      "key": "parentId",
      "type": "hidden"
    }
  ],
  "options": {
    "additional": {
      "fields": [
        "customFieldPercent",
        "customFieldRadio"
      ],
      "enabled": true
    }
  }
}

Live Sample · GitHub

Handling Old Records

Delete fields with widget

When fields stored in the additional settings table are deleted from the widget, the functionality will continue to work correctly by ignoring these old fields.

Filtration

Basic

see more Fields

Filtration of a Tree widget is a standard request. The rows that match the filter are returned by the backend without any hierarchy, and the widget decides how to display them, see Search modes.

The panel above the tree shows:

  • Clear N filter(s) — the number of applied filters and the possibility to reset them;
  • Shown N — how many found records are currently displayed;
  • More M — how many found records are not displayed yet, where M is the number of found records minus the number of shown records.

Info

With the pagination mode nextAndPreviousWithCount the /count request is not performed after filtration. Instead of the number of found records an i icon is displayed with the tooltip "Load more and show count".

FullTextSearch

FullTextSearch - when the user types in the full text search input area, then widget filters the rows that match the search query. see FullTextSearch

The result of a full text search is displayed in the same way as the result of filtration, see Search modes.

Personal filter group

A user-filled filter can be saved for each individual user. see Personal filter group

Live Sample · GitHub

Filter group

Filter group - predefined filters settings that users can use in an application. They allow users to quickly apply specific filtering criteria without having to manually input. see Filter group

Live Sample · GitHub

Search modes

Live Sample · GitHub

The way the result of filtration and full text search is displayed is defined by options.tree.searchModes.

There are two modes:

  • collapse — search results and tree. The tree displays the found records together with the records that are required for navigation: the navigation path above every found record is restored onFilterApplyNestLevel levels up. The user can navigate through the tree, expand and collapse branches, see the neighbouring records and load additional records.
  • hide — search results only. The tree displays only the found records, everything else is hidden. The user works with the search result only and cannot navigate through the other records of the tree.

Both modes can be available at the same time. The first item of the searchModes array is the mode that is active when the filter is applied; the user switches between the available modes in the widget.

Restoring the path upwards

In collapse mode the records whose parents have not been loaded yet are placed under a pseudo node. The >... button loads the missing parents with the request ?id.equals=<parentId> and moves the records into the hierarchy. One click restores up to 2 levels of nesting, so for a deep hierarchy the button has to be pressed several times.

How does it look?

search_collapse.png

search_hide.png

search_modes_menu.png

How to add?
Example

Step1 Add options.tree.searchModes to corresponding .widget.json.

The first item of the array is the mode that is active when the filter is applied.

"tree": {
  "searchModes": ["collapse", "hide"],
  "onFilterApplyNestLevel": 1
}
{
  "name": "MyExampleTree",
  "title": "Custom Fields",
  "type": "Tree",
  "bc": "myExampleBc",
  "fields": [
    {
      "title": "Custom Field",
      "key": "customField",
      "type": "input"
    },
    {
      "title": "Is Leaf",
      "key": "isLeaf",
      "type": "hidden"
    },
    {
      "title": "Parent Id",
      "key": "parentId",
      "type": "hidden"
    }
  ],
  "options": {
    "tree": {
      "searchModes": [
        "collapse",
        "hide"
      ]
    },
    "edit": {
      "style": "none"
    },
    "actionGroups": {
      "include": [
      ]
    }
  }
}

Live Sample · GitHub

Pagination is the process of dividing content into separate, discrete pages, making it easier to navigate and consume large amounts of information. see Pagination

A Tree widget has no navigation arrows. Instead of the "next" arrow the last row of a node is the More button, which loads the next page of the node and appends it to the already loaded records. The limit selector (availableLimitsList) is moved to the gear menu of the widget.

The More button follows the same algorithm as the "next" arrow of the three pagination modes:

Pagination mode When More is displayed Counter
nextAndPreviousWithHasNext hasNext returned by the backend is true not displayed
nextAndPreviousWithCount count is greater than the number of already loaded records how many records are left to load
nextAndPreviousSmart the backend returned more records than the limit of the node not displayed

see more Pagination modes

How does it look?

more_hasnext.png

more_root.png

more_smart.png

How to add?
Example

{
  "name": "MyExampleTree",
  "title": "Widget property Pagination nextAndPreviousWithHasNext AnySource",
  "type": "Tree",
  "bc": "myexample",
  "fields": [
    {
      "title": "customField",
      "key": "customField",
      "type": "input"
    },
    {
      "title": "Parent Id",
      "key": "parentId",
      "type": "hidden"
    },
    {
      "title": "Is Leaf",
      "key": "isLeaf",
      "type": "hidden"
    }
  ],
  "options": {
    "pagination": {
      "type": "nextAndPreviousWithHasNext"
    }
  }
}
Live Sample · GitHub

{
  "name": "MyExampleTree",
  "title": "Tree property Pagination nextAndPreviousWithCount InnerSource",
  "type": "Tree",
  "bc": "myexample",
  "fields": [
    {
      "title": "customField",
      "key": "customField",
      "type": "input"
    },
    {
      "title": "Parent Id",
      "key": "parentId",
      "type": "hidden"
    },
    {
      "title": "Is Leaf",
      "key": "isLeaf",
      "type": "hidden"
    }
  ],
  "options": {
    "pagination": {
      "type": "nextAndPreviousWithCount"
    }
  }
}
Live Sample · GitHub

{
  "name": "MyExampleTree",
  "title": "Widget property Pagination nextAndPreviousSmart AnySource",
  "type": "Tree",
  "bc": "myexample",
  "fields": [
    {
      "title": "customField",
      "key": "customField",
      "type": "input"
    },
    {
      "title": "Parent Id",
      "key": "parentId",
      "type": "hidden"
    },
    {
      "title": "Is Leaf",
      "key": "isLeaf",
      "type": "hidden"
    }
  ],
  "options": {
    "pagination": {
      "type": "nextAndPreviousSmart"
    }
  }
}
Live Sample · GitHub

The list of available limits is displayed in the gear menu of the widget.

{
  "name": "MyExampleTree",
  "title": "Tree",
  "type": "Tree",
  "bc": "myexample",
  "fields": [
    {
      "title": "customField",
      "key": "customField",
      "type": "input"
    },
    {
      "title": "Parent Id",
      "key": "parentId",
      "type": "hidden"
    },
    {
      "title": "Is Leaf",
      "key": "isLeaf",
      "type": "hidden"
    }
  ],
  "options": {
    "pagination": {
      "availableLimitsList": [
        1,
        2,
        3
      ]
    }
  }
}
Live Sample · GitHub

Export to Excel

Info

Export to Excel is not available for the Tree widget in this release

Multi-upload files

not available

Sorting

Sorting allows the user to sort the records by a column. see Sorting

Sorting of a Tree widget works inside a node: the records are sorted among the children of the same parent, the hierarchy itself is not changed. In all other respects sorting is standard.