This is an internal documentation. There is a good chance you’re looking for something else. See Disclaimer.

Legacy Actions

Warning

No longer create new legacy actions if anyhow possible. Many use cases can be represented by Simple Actions now.

Actions may be used to add custom functionality to list or detail pages. Actions are created in Java-Script by extending the AbstractEntityExplorerAction. Most legacy actions communicate with the backend using DWR Java Services.

Actions are usually part of the “Actions” menu of the respective form.

../../../_images/actions.png

The following components are commonly used in ExtJs actions.

  • JavaScript that will be executed when the action is executed

  • DWR-Communication that is used to interact with the java backend

  • Standard-Integration amendment to define where the action is displayed

  • ACL to define who may use this action

  • hivemodule contributions to register javascript and dwr serivces

Dependencies

build.gradle

Support for DWR services is contained by the netui module. This dependency can be declared like this:

dependencies {
     implementation project(":core:netui")
}

Note

This dependency might already be transitively included by another dependency. An api dependency might be necessary if the service is in an exported package.

JavaScript

Actions are written in ExtJs javascript. The action javascript will usually be stored in the resources/resources/webapp/js folder of the module.

../../../_images/action-js-folder-structure.png

Every javascript file needs to be registered in the @Configuration class of the module, so that it can be loaded by the JavaScriptServlet:

@Bean
public JavaScriptContribution tasksJavaScriptContribution() {
    return new JavaScriptContribution("nice2.tasks", currentModule(), findModelResources(
        "resources/webapp/js/nice2/tasks/TriggerBatchjobAction.js",
        "resources/webapp/js/nice2/tasks/UpdateDueTimeAction.js",
        "resources/webapp/js/nice2/tasks/CancelTaskAction.js",
        "resources/webapp/js/nice2/tasks/DeleteTaskAction.js",
        "resources/webapp/js/nice2/tasks/progress/TaskProgressGuiManager.js",
        "resources/webapp/js/nice2/tasks/progress/TaskProgressContextMenu.js",
        "resources/webapp/js/nice2/tasks/progress/TaskProgressReceiver.js",
        "resources/webapp/js/nice2/tasks/progress/UpdateProgressRemoteClientAction.js"
    ));
}

If a custom javascript package is used, this needs to be registered as well (only once):

@Bean
public JavaScriptModuleContribution tasksJavaScriptModuleContribution() {
    return new JavaScriptModuleContribution("nice2-admin", currentModule(), List.of("nice2.tasks"));
}

Please find below an example of a simple JavaScript action that calls a dwr service to process the selection.

Ext.ns('nice2.optional.test');
nice2.optional.test.TestAction = Ext.extend(nice2.modules.entityExplorer2.actions.AbstractEntityExplorerAction, {
    _doPerform: function() {
        this.requireSingleSelection();
        nice2.netui.dwr.RemoteService.call({
            remoteService: 'nice2_optional_test_TestService',
            method: 'testMethod',
            args: [ this.getSelection() ],
            mask: false,
            scope: this,
            successMessage: getText('actions.test.TestAction.success'),
            success: Ext.emptyFn
        });
    }
});
NetuiActionRegistry.register('nice2.optional.test.TestAction', nice2.optional.test.TestAction);

AbstractEntityExplorerAction contains various utility functions that may be used when creating an action:

  • getSelection() returns the current selection. This selection may directly be sent to a DWR service

  • requireSingleSelection() this method may be called to evaluate the selection. If not exactly one entity is selected, an error will be shown.

  • requireSingleOrNoSelection() requires the selection to be 0 or 1 entity

  • requireSelectionWithMaximum(max) requires the selection to be between 0 and max entities

  • requireSingleOrUpToMaxSelection(max) requires the selection to be between 1 and max entities

  • createAutoHeightWindow() can be used to create an extjs window

  • createFormWindow() can be used to create an extjs window that renders a form

DWR-Communication

DWR is used to connect the ExtJs frontend with our java backend. If an action requires any backend interaction, i.e. if any data should be checked or amended, it will be done using DWR.

Java-Script

Accessing DWR services in javascript may be done using the nice2.netui.dwr.RemoteService.call method as followed.

nice2.netui.dwr.RemoteService.call({
    remoteService: 'nice2_optional_test_TestService',
    method: 'testMethod',
    args: [ this.getSelection() ],
    mask: false,
    scope: this,
    successMessage: getText('actions.test.TestAction.success'),
    success: function(response) {

    }
});

In this example the method testMethod of the DWR Service nice2_optional_test_TestService is called with the current selection as parameter.

Java

If a DWR service is created exclusively to be called from an action, it usually will be created in an actions package. To create a DWR service the methods to be called must be defined in an interface and its implementation is usually in a corresponding Impl class right next to it (unfortunately a DWR service always requires an interface even if it’s only referenced by a JS file):

../../../_images/action-service-folder-structure.png

Please find below a minimal example with a method that takes a selection and returns a string

Interface:

public interface TestActionService {
    String testDwrMethod(EntityExplorerActionSelection selection);
}

Implementation:

@DwrService(name = "nice2_optional_test_TestActionService", interfaceClass = TestActionService.class)
public class TestActionServiceImpl implements TestActionService {
    private final EntityExplorerActionSelectionService selectionService;

    public TestActionServiceImpl(EntityExplorerActionSelectionService selectionService) {
        this.selectionService = selectionService;
    }

    @Override
    String testDwrMethod(EntityExplorerActionSelection selection) {
        EntityList entities = selectionService.getSelectionEntityList(selection);
        //method content
        return "Test";
    }
}

A DWR service must be annotated with @DwrService. The parameter name defines the name that needs to be used in the javascript to reference the service. In addition the interface class needs to be specified as well.

Data-Types

DWR by default only supports basic Data-Types e.g String, int or long and simple Collections of those. This means that for example Entity objects cannot be passed as a return value to the ExtJs frontend. If a method needs to return multiple values (e.g. multiple fields of an entity) Maps are commonly used.

Warning

An outbound java List will be converted to a javascript array. An inbound javascript array will be converted to a java array. Return values of dwr methods can be Collections, parameters can only be simple types, custom beans or arrays of those.

@Override
Map<String, String> testDwrMethod(String[] params) {
    //method content
    return null
}

Custom Beans

If an object of multiple values needs to be passed to the frontend or from the frontend to a DWR service this can be achieved by creating a custom bean.

Java Bean

public class MembershipBean {
    private String key;
    private String email;
    private String currentUserKey;

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getCurrentUserKey() {
        return currentUserKey;
    }

    public void setCurrentUserKey(String currentUserKey) {
        this.currentUserKey = currentUserKey;
    }
}

A converter needs to be configured in the @Configuration class for each custom DWR bean:

@Bean
public ConverterContribution membershipBeanConverter() {
    BeanConverter converter = new BeanConverter();
    converter.setJavascript("nice2.optional.membership.MembershipBean");
    return new ConverterContribution(converter, MembershipBean.class.getName());
}

Bean in JavaScript

var bean = new nice2.customer.sps.MembershipBean();
Ext.apply(bean, {
    key: recipient.key,
    email: recipient.email,
    currentUserKey: recipient.currentUserKey
});

Standard-Integration

Adding an action to a XML form is as easy defining a new <action> in the corresponding action group (usually actiongroup_actions)

<action name="actiongroup_actions" label="actiongroup.actions" icon="cog_go">
  <action path="nice2.optional.test.TestAction" label="actions.test_action.title" icon="email_open"/>
</action>
  • path the path of the action. corresponds with the service point.

  • label The label of this action (textresource key).

  • icon the icon of the action.

  • group the action group to put this action in.

  • default a default action can be defined for a group. If defined, the group will be displayed as split button where this action is directly executable and the other actions in the group will be displayed in the dropdown menu.

  • showConfirmMessage if true, a confirmation box will be displayed before the action is executed.

  • confirmationText the text resource key for the text of the confirmation box. Only will be considered if the showConfirmationMessage attribute is set to true.

ACL

To grant access to a created action, a netuiactions acl rule has to be created in the action.acl file. If no action.acl exists it may be created and added to the module.acl (include 'action.acl';).

netuiactions("nice2.optional.test.TestAction"):
    grant netuiPerform to configurator;

ActionFactories

If an action needs to be dynamically added to multiple forms, if it needs to be added to each row or if you need to overwrite a standard action (e.g. New) custom ActionFactories may be created as shown below.

@Component
public class SingleMarkActionFactory extends AbstractActionFactory {

    private final Context ctx;

    public SingleMarkActionFactory(ActionsBuilder actionBuilder,
                                   Context ctx) {
        super(actionBuilder);
        this.ctx = ctx;
    }

    /**
     * Create a new {@link ActionGroupModel} for the specified situation.
     *
     * @return <code>null</code> if this factory provides no actions for this situation.
     */
    @Nullable
    @Override
    public Collection<ActionModel> createActions(Situation situation) {
        if(and(instanceOf(EntityNameSituation.class), not(isScreen("explorer-modal"))).apply(situation)) {
            EntityManager markEM = ctx.getEntityManager(((EntityNameSituation) situation).getEntityName());
            EntityModel markEntityModel = (EntityModel) markEM.getModel();
            if (markEntityModel != null && markEntityModel.isMarkable()) {
                if (isScope("update").apply(situation) && situation instanceof EntitySituation) {
                    return createMarkSingleAction((EntitySituation) situation, markEM, markEntityModel);
                } else if (isScope("list").apply(situation)) {
                    return createMarkMultipleAction();
                }
            }
        }
        return null;
    }

    private Collection<ActionModel> createMarkSingleAction(EntitySituation situation,
                                                           EntityManager markEM,
                                                           EntityModel markEntityModel) {
        ActionModel markAction = actionBuilder.newAction("nice2.marking.MarkSingleAction", "nice2.marking.MarkSingleAction");
        markAction.setEnabled(true);
        markAction.setIcon(getMarkMultipleIcon(situation, markEM, markEntityModel));
        markAction.setShortDescription("actions.mark.markBox");
        markAction.setName("");
        return asCollection(markAction);
    }

    private String getMarkMultipleIcon(EntitySituation situation,
                                       EntityManager markEM,
                                       EntityModel markEntityModel) {
        PrimaryKey key = PrimaryKey.createPrimary(markEntityModel, situation.getPrimaryKey());
        Entity entity = markEM.get(key);
        if(!entity.resolve("relMark").execute().isEmpty()) {
            return "star";
        } else {
            return "star_grey";
        }
    }

    private Collection<ActionModel> createMarkMultipleAction() {
        ActionGroupModel actionGroupModel = actionBuilder.getNiceGroup(ActionsBuilder.NiceGroup.ACTIONS);
        ActionModel markMultipleAction = actionBuilder.newAction("nice2.marking.MarkMultipleAction", "actions.marking.MarkMultipleAction");
        markMultipleAction.setEnabled(true);
        markMultipleAction.setIcon("star_half_grey");
        actionGroupModel.addAction(markMultipleAction);
        return asCollection(actionGroupModel);
    }
}

Action factories are discovered automatically as long as they are annotated with @Component.

How to enable legacy actions in the new client

Legacy actions are not enabled by default in the new client.

Set the application property nice2.forms.legacyActionsEnabled=true to enable them.

If legacy actions are enabled, disable specific legacy actions by adding them to the configuration point nice2.model.form.DisabledLegacyActions (e.g. once they’ve been reimplemented with React).

Customer legacy actions aren’t enabled automatically even if nice2.forms.legacyActionsEnabled=true is set. You need to whitelist them additionally to the application property via the configuration point nice2.model.form.EnabledCustomerLegacyActions.