Add a new editor to CMS Cockpit

Date
September 29, 2017
Hot topics 🔥
Tech Insights
Contributor
David Roman
Add a new editor to CMS Cockpit

By Sergey Gernyak, Back-end Engineer at WeAreBrain.

 


The CMS Cockpit forms a separate part of the Hybris Platform which allows you to construct pages via visual editors and the drag&drop function. In general, there is a great deal of available functionality OOTB that covers almost all business requirements. However, we had one requirement that is unique and there are no built-in solutions as yet. What we needed, was the ability to select an item from a 3rd party API and save it inside Hybris DB.

ZK framework in the heart

During a code reading session, I’ve found that CMS Cockpit is based completely on the ZK Framework. With this knowledge, we set out to write glue code that instructs CMS Cockpit to work with a new editor using the ZK framework as a renderer. For this solution, I chose to use a combobox component which has autocomplete functionality — exactly what we needed.

A little modeling

To start, we decided to model all we needed to implement to use the editor we wanted. You’ll see what I mean in the diagram below:

All the functionality can be divided into the following parts:

  • A combobox component from the ZK framework, which plays the renderer role
  • A data source gateway which holds the requests for the 3-rd party API
  • A model, which keeps the current editor’s state
  • The editor itself

What else is needed?

Just coding the editor won’t help you to see it in the CMS Cockpit. In most cases the list of steps to get it done include:

  • Code the editor implementation with business requirements
  • Register the editor in the global cockpits editor’s pool via the Spring configuration — at this point the editor will be named using a unique identifier
  • Use the editor’s unique identifier — It could be something like content area, editor area or wizard area.

Code examples

Here is the most interesting part — the example source code:

package com.mycompany.cms2components.data;

public class ItemOption {
    private Integer id;
    private String title;
    private String description;
    private String thumbUrl;

    public ItemOption()
    {
    }

    public void setId(final Integer id)
    {
        this.id = id;
    }

    public Integer getId()
    {
        return id;
    }

    public void setTitle(final String title)
    {
        this.title = title;
    }

    public String getTitle()
    {
        return title;
    }

    @Override
    public String toString() {
        return this.getTitle();
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getThumbUrl() {
        return thumbUrl;
    }

    public void setThumbUrl(String thumbUrl) {
        this.thumbUrl = thumbUrl;
    }
}
package com.mycompany.cms2components.service;

import com.mycompany.cms2components.data.ItemOption;

import java.util.List;

public interface ItemSelectDataSource {
    List<ItemOption> doSearch(String term, String locale);
    List<ItemOption> getByIds(List<String> ids);
}
package com.mycompany.cms2components.editor;

import com.mycompany.cms2components.data.ItemOption;
import com.mycompany.cms2components.editor.model.ItemSelectUIEditorModel;
import com.mycompany.cms2components.service.ItemSelectDataSource;
import de.hybris.platform.cockpit.model.editor.EditorListener;
import de.hybris.platform.cockpit.model.editor.ListUIEditor;
import de.hybris.platform.cockpit.model.editor.impl.AbstractUIEditor;
import de.hybris.platform.cockpit.services.values.ObjectValueContainer;
import de.hybris.platform.core.Registry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.ReflectionUtils;
import org.zkoss.zk.ui.HtmlBasedComponent;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.event.InputEvent;
import org.zkoss.zul.Combobox;
import org.zkoss.zul.Comboitem;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

public class ItemSelectUIEditor extends AbstractUIEditor implements ListUIEditor {
    private static final Logger LOG = LoggerFactory.getLogger(ItemSelectUIEditor.class);

    private Combobox editorView;
    private ItemSelectDataSource dataSource;
    private ItemSelectUIEditorModel model;

    public ItemSelectUIEditor() {
        LOG.info("ItemSelectUIEditor initialization started!");
        this.dataSource = this.getDataSourceBean();
        this.model = this.getModelBean();
        this.editorView = new Combobox();
        this.setupEditorViewDefaultBehaviour();
    }


    @Override
    public HtmlBasedComponent createViewComponent(Object initialValue, Map<String, ? extends Object> parameters, EditorListener listener) {
        this.setInitialSelection(initialValue);
        this.setupEditorViewEvents(listener);
        return this.getEditorView();
    }

    private void setInitialSelection(Object initialValue) {
        if (initialValue == null) { return; }
        List<ItemOption> options = dataSource.getByIds(Collections.singletonList(initialValue.toString()));
        this.model.setAutocompleteResult(options);
        this.updateAutoCompleteItemList();
        this.getEditorView().setSelectedIndex(0);
    }

    private void setupEditorViewDefaultBehaviour() {
        this.getEditorView().setAutocomplete(true);
        this.getEditorView().setAutodrop(true);
        this.getEditorView().setButtonVisible(false);
    }

    private void setupEditorViewEvents(EditorListener listener) {
        this.getEditorView().addEventListener(Events.ON_CHANGING, event -> {
            String term = ((InputEvent) event).getValue();
            LOG.info("onChanging triggered with value '" + term + "'!");
            Field valueHolderFields = ReflectionUtils.findField(listener.getClass(), "val$valueHolder");
            ReflectionUtils.makeAccessible(valueHolderFields);
            ObjectValueContainer.ObjectValueHolder valueHolder = (ObjectValueContainer.ObjectValueHolder) ReflectionUtils.getField(valueHolderFields, listener);
            String locale = valueHolder.getLanguageIso();
            ItemSelectUIEditor.this.updateAutocomplete(term, locale);
        });

        this.getEditorView().addEventListener(Events.ON_CHANGE, event -> {
            LOG.info("onChange triggered!");
            this.onSavingEvent(listener);
        });
    }

    private void updateAutocomplete(String term, String locale) {
        if (term.length() < 3) {
            this.model.setAutocompleteResult(new ArrayList<>());
        }else{
            List<ItemOption> options = dataSource.doSearch(term, locale);
            this.model.setAutocompleteResult(options);
        }
        this.updateAutoCompleteItemList();
    }

    private void updateAutoCompleteItemList() {
        this.getEditorView().getItems().clear();
        for (Comboitem comboitem : this.model.getAutocompleteComboitems()) {
            this.getEditorView().appendChild(comboitem);
        }
    }

    private void onSavingEvent(EditorListener listener) {
        LOG.info("onSavingEvent called!");
        if(this.getEditorView().getSelectedItem() == null) { return; }
        Comboitem selectedOption = this.getEditorView().getSelectedItem();
        this.setValue(selectedOption.getValue());
        listener.valueChanged(this.getValue());
    }


    @Override
    public boolean isInline() {
        return true;
    }

    @Override
    public String getEditorType() {
        return "INTEGER";
    }

    @Override
    public void setAvailableValues(List<? extends Object> list) {
    }

    @Override
    public List<ItemOption> getAvailableValues() {
        return null;
    }

    public void setFocus(HtmlBasedComponent rootEditorComponent, boolean selectAll) {
        Combobox element = (Combobox)((CancelButtonContainer)rootEditorComponent).getContent();
        element.setFocus(true);
        if(this.initialInputString != null) {
            element.setText(this.initialInputString);
        }

    }

    protected Combobox getEditorView() {
        return editorView;
    }

    private ItemSelectDataSource getDataSourceBean() {
        return Registry.getApplicationContext().getBean("defaultItemSelectDataSource", ItemSelectDataSource.class);
    }

    private ItemSelectUIEditorModel getModelBean() {
        return Registry.getApplicationContext().getBean("defaultItemSelectUIEditorModel", ItemSelectUIEditorModel.class);
    }
}
package com.mycompany.cms2components.editor.model;

import com.mycompany.cms2components.data.ItemOption;
import org.zkoss.zul.Comboitem;

import java.util.List;

public interface ItemSelectUIEditorModel {
    List<ItemOption> getAutocompleteResult();
    void setAutocompleteResult(List<ItemOption> options);
    List<Comboitem> getAutocompleteComboitems();
}
<?xml version="1.0" encoding="UTF-8"?>
<wizard-config showPrefilledValues="false" selectMode="true" createMode="true" displaySubtypes="true">
    <displayed-properties>
        <group qualifier="General" visible="true" initially-opened="true">
            <label key="cockpit.config.label.General" />
            <property qualifier="MyComponent.name"/>
        </group>
        <group qualifier="Properties" visible="true" initially-opened="true">
            <label key="config.wizardConfig.properties" />
            <property qualifier="contentId" editorCode="contentItemSelectAutocomplete"/>
        </group>
    </displayed-properties>
</wizard-config>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd"
       default-autowire="byName">

    <!-- Allows Additional PropertyEditorDescriptor to be added for all cockpits -->
    <bean id="LookupEditorFactory" class="de.hybris.platform.cockpit.model.meta.LookupEditorFactory"  init-method="init" lazy-init="false">
        <property name="editorFactory" ref="EditorFactory"></property>
    </bean>

    <bean id="myCompanyIntegerEditors" class="de.hybris.platform.cockpit.model.meta.DefaultPropertyEditorDescriptor">
        <property name="editorType" value="INTEGER"/>
        <property name="defaultEditor" value="de.hybris.platform.cockpit.model.editor.impl.DefaultIntegerUIEditor" />
        <property name="defaultMode" value="single"/>
        <property name="editors">
            <map>
                <entry key="contentItemSelectAutocomplete" value="com.mycompany.cms2components.editor.ItemSelectUIEditor"/>
            </map>
        </property>
        <property name="label" value="myCompanyIntegerEditors"/>
    </bean>
</beans>

NOTE: all these files have different contexts, so do not pay a lot of attention to the classes/variables/ naming etc.

NOTE 2: I am newbie in the Hybris Platform development area, so if there’s something I missed or doesn’t make sense, holla in the comments section below.

NOTE 3: As github gist does not allow slashes inside file names I’ve used two underscore symbols in place of the slash .

Happy coding!

David Roman

David is one of our marketing gurus. He loves working with content but has a good eye for marketing analytics as well. Creativity is what drives him, photography being one of his passions.

Working Machines

An executive’s guide to AI and Intelligent Automation. Working Machines takes a look at how the renewed vigour for the development of Artificial Intelligence and Intelligent Automation technology has begun to change how businesses operate.