How to add a button to the toolbar

From InfiniteERP Wiki
Jump to: navigation, search

Introduction

This howto discusses how a button can be added the main toolbar shown in grids and forms. The toolbar contains two types of buttons, the application buttons on the left (visualized using an icon) and the custom buttons on the right (shown with a label). This howto describes how to add a button to the left part, the application buttons.



To follow this howto you need to be able to, at a junior level, develop javascript as well as server-side java and understand Data Access Layer concepts.

Example Module

This howto is supported by an example module which shows example of the code shown and discussed in this howto.

The code of the example module can be downloaded from this mercurial repository: https://code.openbravo.com/erp/mods/org.openbravo.client.application.examples/

The example module is available through the Central Repository (See 'Client Application Examples'), for more information see the Examples Client Application project page.

Bulbgraph.png   The example module also contains implementations of other howtos.

Implementing a toolbar button

Bulbgraph.png   When implementing your own components it often makes sense to extend existing components. Make sure that your module then depends on the module that provides the base types. This ensures that the javascript is loaded in the correct order. You must add a dependency from your module to the Openbravo 3.0 Framework (org.openbravo.v3.framework) module

The button which we will implement will compute and show the sum of a set of selected orders to the user. This howto is divided in 2 parts, the first part focuses on visualizing the button in the correct windows and tabs and making sure the button is enabled/disabled appropriately. The second part will discuss how to implement backend logic and how to call the server side logic when the button gets clicked and show its results.

The first part consists of the following steps:

  • an icon for the visualization
  • a css style and javascript linking the icon to the button
  • javascript implementing the buttons click/action method and to register the button in the global registry
  • add javascript to enable/disable the button when records are selected in the grid
  • a ComponentProvider java class to register the javascript and css in Openbravo

These steps will visualize the button but not actually do anything yet. The follow up steps will add logic:

  • a server side actionhandler to implement the server side logic (summing the orders and returning the result to the client)
  • client side javascript to call the server and process the result

Each of these steps is described in more detail below.

Visualizing the button - Implementation steps

Defining the icon and a css

The icon and its related style is defined through an icon file. For standard visualization using the Openbravo style the icon should be 24x24 without a background colour.

The icon should be placed in a specific directory in your module: web/org.openbravo.userinterface.smartclient/openbravo/skins/Default/[modulename]. Normally it makes sense to store the icon in a subfolder. The example module has the icon file in: web/org.openbravo.userinterface.smartclient/openbravo/skins/Default/org.openbravo.client.application.examples/images.



Then add a css file which links this icon to a specific css style. The css file must also be located in this directory in your module: web/org.openbravo.userinterface.smartclient/openbravo/skins/Default/[modulename]. In the example module the css file is located here: web/org.openbravo.userinterface.smartclient/openbravo/skins/Default/org.openbravo.client.application.examples/ob-example-styles.css Within the css file add a style defined like this: <source lang="css">.OBToolbarIconButton_icon_obexapp_sum {

 background-repeat: no-repeat;
 background-position: center center;
 background-image: url(./images/iconButton-sum.png);

} </source> Note: the name of the css class is important, it should start with 'OBToolbarIconButton_icon_', the part after that (obexapp_sum) is used later in this tutorial. It makes sense to use the module's dbprefix in this last part to prevent name collisions with other modules.

The javascript to create and register the button

The next step is to implement the javascript which defines the button and registers it to be shown on tabs. Start by creating a javascript file in this location: web/org.openbravo.client.application.examples/js/example-toolbar-button.js. In the example module this file is present: web/org.openbravo.client.application.examples/js/example-toolbar-button.js.

This is the overall javascript: <source lang="javascript">(function () {

 var buttonProps = {
     action: function(){
       alert('You clicked me!');
     },
     buttonType: 'obexapp_sum',
     prompt: OB.I18N.getLabel('OBEXAPP_SumData'),
     updateState: function(){
         var view = this.view, form = view.viewForm, grid = view.viewGrid, selectedRecords = grid.getSelectedRecords();
         if (view.isShowingForm && form.isNew) {
           this.setDisabled(true);
         } else if (view.isEditingGrid && grid.getEditForm().isNew) {
           this.setDisabled(true);
         } else {
           this.setDisabled(selectedRecords.length === 0);
         }
     }
   };
 
 // register the button for the sales order tab
 // the first parameter is a unique identification so that one button can not be registered multiple times.
 OB.ToolbarRegistry.registerButton(buttonProps.buttonType, isc.OBToolbarIconButton, buttonProps, 100, '186');

}()); </source>

Let's walk through the different parts. The javascript starts and ends with this part: <source lang="javascript">(function () { ... }()); </source> This is done to prevent the local variables to be available globally, it creates a function and executes it immediately.

Then the first part of the buttonprops" <source lang="javascript"> action: function(){

       alert('You clicked me!');
     },
     buttonType: 'obexapp_sum',
     prompt: OB.I18N.getLabel('OBEXAPP_SumData'),

</source>

  • Sets the action method which gets called when the user clicks the button.
  • The buttonType maps back to the name used in the css style, it controls the icon and styling and is used as an identification also
  • The prompt is shown when the user hovers over the button with the mouse, the label is retrieved through the OB.I18N.getLabel method to support translation, add a label in the Messages table to correctly visualize it:

The updateState function is called to enable and disable the button when the user navigates through the system. <source lang="javascript"> updateState: function(){

         var view = this.view, form = view.viewForm, grid = view.viewGrid, selectedRecords = grid.getSelectedRecords();
         if (view.isShowingForm && form.isNew) {
           this.setDisabled(true);
         } else if (view.isEditingGrid && grid.getEditForm().isNew) {
           this.setDisabled(true);
         } else {
           this.setDisabled(selectedRecords.length === 0);
         }
     }

</source> The function enables/disables the button if the form or grid is new and if there are no records selected.

Then this code registers the button for the tab with id '186': <source lang="javascript"> OB.ToolbarRegistry.registerButton(buttonProps.buttonType, isc.OBToolbarIconButton, buttonProps, 100, '186'); </source> Note:

  • the first parameter is a unique identification so that one button can not be registered multiple times.
  • as the second parameter the button javascript class is passed in, as a default always use isc.OBToolbarIconButton
  • the buttonProps define the button characteristics
  • the third parameter defines the order in the toolbar, the standard buttons are placed with an interval of 10, so you can place your button in between other buttons.
  • the last parameter is the tabId (a String), you can pass null to register a button for all tabs. From 3.00MP2 and further it is also possible to pass in an array of tabIds (strings) to register a button for multiple tabs.
  • to register a button for multiple tabs call the registerButton multiple times for different tabs

Note: in 3.00MP0 a button can only be registered for one specific tab or for all tabs. In 3.00MP2 this will be solved by allowing an array of tab ids (see this issue to track the solution)

The ComponentProvider

The previous steps added static resources (javascript and css) to the system. Now Openbravo should know where to find these resources when initializing and generating the user interface. For this the css and javascript and resources have to be registered. This is done through a ComponentProvider. For more detailed information on the ComponentProvider concept visit this page.

<source lang="java"> @ApplicationScoped @ComponentProvider.Qualifier(ExampleComponentProvider.EXAMPLE_VIEW_COMPONENT_TYPE) public class ExampleComponentProvider extends BaseComponentProvider {

 public static final String EXAMPLE_VIEW_COMPONENT_TYPE = "OBEXAPP_ExampleViewType";
 /*
  * (non-Javadoc)
  * 
  * @see org.openbravo.client.kernel.ComponentProvider#getComponent(java.lang.String,
  * java.util.Map)
  */
 @Override
 public Component getComponent(String componentId, Map<String, Object> parameters) {
   throw new IllegalArgumentException("Component id " + componentId + " not supported."); 
   /* in this howto we only need to return static resources so there is no need to return anything here */
 }
 @Override
 public List<ComponentResource> getGlobalComponentResources() {
   final List<ComponentResource> globalResources = new ArrayList<ComponentResource>();
   globalResources.add(createStaticResource(
       "web/org.openbravo.client.application.examples/js/example-toolbar-button.js", false));
   globalResources.add(createStyleSheetResource(
       "web/org.openbravo.userinterface.smartclient/openbravo/skins/"
           + KernelConstants.SKIN_VERSION_PARAMETER
           + "/org.openbravo.client.application.examples/example-styles.css", false));
   return globalResources;
 }
 @Override
 public List<String> getTestResources() {
   return Collections.emptyList();
 }

} </source>

A short explanation:

  • The annotations in the top of the class are related to Weld, see the description here. The annotations define that only one instance of this class is created (a singleton) and define an identifier for this instance.
  • The getGlobalResources is the important here, it shows how to register the global resources defined in the example module. For your own module just follow the same path structure and approach.

To explain how Openbravo can find the ComponentProvider: Openbravo/Weld will analyze the classpath and find all the classes which have a @ComponentProvider annotation.

The result

To see the result, restart Tomcat, clear the cache of the browser (sometimes css styles are not picked up) and go back to the application and then specifically the sales order window. You should see this:

Note: the button is not visualized in other windows/tabs because it is registered for only the sales order header tab.



Adding server side logic - Implementation steps

The next step in this how to is to add the server side logic and calling this logic from the client. The server side logic is implemented using the action handler concept. The action handler concept allows you to create classes on the server which are callable from the client. This how to will illustrate this.

Implement the server side action handler

The server side action handler (SumOrderActionHandler.java) receives an array of order numbers of the selected orders. It will sum the order values and then return the total as a JSON string.

This is the implementation of the server side: <source lang="java">public class SumOrderActionHandler extends BaseActionHandler {

 protected JSONObject execute(Map<String, Object> parameters, String data) {
   try {
     // get the data as json
     final JSONObject jsonData = new JSONObject(data);
     final JSONArray orderIds = jsonData.getJSONArray("orders");
     // start with zero
     BigDecimal total = new BigDecimal("0");
     // iterate over the orderids
     for (int i = 0; i < orderIds.length(); i++) {
       final String orderId = orderIds.getString(i);
       // get the order
       final Order order = OBDal.getInstance().get(Order.class, orderId);
       // and add its grand total
       total = total.add(order.getGrandTotalAmount());
     }
     // create the result
     JSONObject json = new JSONObject();
     json.put("total", total.doubleValue());
     // and return it
     return json;
   } catch (Exception e) {
     throw new OBException(e);
   }
 }

} </source> Notes:

  • The BaseActionHandler is extended, this is often the best approach when implementing an ActionHandler, in this case only the execute methods needs to be implemented.
  • Data can be send in 2 ways to server: as parameters and as part of the request body. Therefore the execute method has 2 parameters. In this example the request body is used
  • Use a BigDecimal for numbers, as this far more precise than a double, unfortunately json only supports doubles. In the core Openbravo system numbers are therefore send from client-server (and vice versa) as strings.
  • the logic iterates over the order ids and retrieves the order using the Data Access Layer.
  • the result is returned as json again

Calling the server side from the client, displaying the result

Then on the client the action method of the button has to be implemented to call the server. Here is the implementation. See the web/org.openbravo.client.application.examples/js/example-toolbar-button.js for the full listing: <source lang="javascript"> action: function(){

       var callback, orders = [], i, view = this.view, grid = view.viewGrid, selectedRecords = grid.getSelectedRecords();
       // collect the order ids
       for (i = 0; i < selectedRecords.length; i++) {
         orders.push(selectedRecords[i].id);
       }
       
       // define the callback function which shows the result to the user
       callback = function(rpcResponse, data, rpcRequest) {
         isc.say(OB.I18N.getLabel('OBEXAPP_SumResult', [data.total]));
       }
       
       // and call the server
       OB.RemoteCallManager.call('org.openbravo.client.application.examples.SumOrderActionHandler', {orders: orders}, {}, callback);

</source> Note:

  • the selectedRecords contain the full record information of the order (businessPartner etc.), in this case we only use the id
  • the call to the server is asynchronously therefore a callback is used, which is called when the server returns the result. The callback gets 3 parameters, the data parameter contains the JSONObject returned by the server's execute method.
  • the call to the RemoteCallManager has these parameters:
    • the class name of the action handler
    • the data being sent as the request body
    • request parameters (in this case nothing there)
    • and the callback

Then when the server returns the callback is called which will display a prompt. Note that the label used in the callback uses parameter substitution. Parameters are specified using a %0, %1 etc.:


The result

The result shows the sum of the 2 selected order headers: