Let the platform do the work

Adding Buttons to the Record View

Overview

This example explains how to create additional buttons on the record view and add events. We will extend and override the stock Accounts record view to add a custom button. The custom button will be called "Validate Postal Code" and ping the Zippopotamus REST service to validate the records billing state and postal code.

Steps To Complete

This tutorial requires the following steps, which are explained in the sections below:

  1. Defining the Metadata
  2. Adding Custom Buttons
  3. Defining the Button Label
  4. Extending and Overriding the Controller

Defining the Metadata

To add a button to the record view, you will first need to create the custom metadata for the view if it doesn't exist. You can easily do this by opening and saving your modules record layout in studio. Depending on your module, the path will then be ./custom/modules/<module>/clients/base/views/record/record.php. Once your file is in place, you will need to copy the button array from ./clients/base/views/record/record.php and add it to the $viewdefs['<module>']['base']['view']['record'] portion of your metadata array. An example of the button array is shown below:

  'buttons' => array(
        array(
            'type' => 'button',
            'name' => 'cancel_button',
            'label' => 'LBL_CANCEL_BUTTON_LABEL',
            'css_class' => 'btn-invisible btn-link',
            'showOn' => 'edit',
        ),
        array(
            'type' => 'rowaction',
            'event' => 'button:save_button:click',
            'name' => 'save_button',
            'label' => 'LBL_SAVE_BUTTON_LABEL',
            'css_class' => 'btn btn-primary',
            'showOn' => 'edit',
            'acl_action' => 'edit',
        ),
        array(
            'type' => 'actiondropdown',
            'name' => 'main_dropdown',
            'primary' => true,
            'showOn' => 'view',
            'buttons' => array(
                array(
                    'type' => 'rowaction',
                    'event' => 'button:edit_button:click',
                    'name' => 'edit_button',
                    'label' => 'LBL_EDIT_BUTTON_LABEL',
                    'acl_action' => 'edit',
                ),
                array(
                    'type' => 'shareaction',
                    'name' => 'share',
                    'label' => 'LBL_RECORD_SHARE_BUTTON',
                    'acl_action' => 'view',
                ),
                array(
                    'type' => 'pdfaction',
                    'name' => 'download-pdf',
                    'label' => 'LBL_PDF_VIEW',
                    'action' => 'download',
                    'acl_action' => 'view',
                ),
                array(
                    'type' => 'pdfaction',
                    'name' => 'email-pdf',
                    'label' => 'LBL_PDF_EMAIL',
                    'action' => 'email',
                    'acl_action' => 'view',
                ),
                array(
                    'type' => 'divider',
                ),
                array(
                    'type' => 'rowaction',
                    'event' => 'button:find_duplicates_button:click',
                    'name' => 'find_duplicates_button',
                    'label' => 'LBL_DUP_MERGE',
                    'acl_action' => 'edit',
                ),
                array(
                    'type' => 'rowaction',
                    'event' => 'button:duplicate_button:click',
                    'name' => 'duplicate_button',
                    'label' => 'LBL_DUPLICATE_BUTTON_LABEL',
                    'acl_module' => $module,
                ),
                array(
                    'type' => 'rowaction',
                    'event' => 'button:audit_button:click',
                    'name' => 'audit_button',
                    'label' => 'LNK_VIEW_CHANGE_LOG',
                    'acl_action' => 'view',
                ),
                array(
                    'type' => 'divider',
                ),
                array(
                    'type' => 'rowaction',
                    'event' => 'button:delete_button:click',
                    'name' => 'delete_button',
                    'label' => 'LBL_DELETE_BUTTON_LABEL',
                    'acl_action' => 'delete',
                ),
            ),
        ),
        array(
            'name' => 'sidebar_toggle',
            'type' => 'sidebartoggle',
        ),
    ),

Note: When copying this array into your metadata, you will need to replace $module with the text string of your module's name.

For standard button types, the button definitions will contain the following properties:

Property Potential Values Description
type button, rowaction, shareaction, actiondropdown The widget type
name   The name of the button
label   The label string key for the display text of the button
css_class   The CSS class to append to the button
showOn edit, view The ACL action of the button

For this example, we will add the custom button to the main dropdown. For actiondropdown types, there is an additional buttons array for you to specify the dropdown list of buttons. The button definitions in this array will contain the following properties:

Property Potential Values Description
type button, rowaction, shareaction, actiondropdown The widget type; Most custom buttons are 'rowaction'
event button:button_name:click The event name of the button
name   The name of the button
label   The label string key for the display text of the button
acl_action  edit, view The ACL action of the button

Adding Custom Buttons

For this example, modify the accounts' metadata to add the button definition to main_dropdown:

  array(
    'type' => 'rowaction',
    'event' => 'button:validate_postal_code:click',
    'name' => 'validate_postal_code',
    'label' => 'LBL_VALIDATE_POSTAL_CODE',
    'acl_action' => 'view',
),

A full example is shown below:

./custom/modules/Accounts/clients/base/views/record/record.php

  <?php

$viewdefs['Accounts'] =
array (
  'base' =>
  array (
    'view' =>
    array (
      'record' =>
      array (
        'buttons' =>
        array (
          0 =>
          array (
            'type' => 'button',
            'name' => 'cancel_button',
            'label' => 'LBL_CANCEL_BUTTON_LABEL',
            'css_class' => 'btn-invisible btn-link',
            'showOn' => 'edit',
          ),
          1 =>
          array (
            'type' => 'rowaction',
            'event' => 'button:save_button:click',
            'name' => 'save_button',
            'label' => 'LBL_SAVE_BUTTON_LABEL',
            'css_class' => 'btn btn-primary',
            'showOn' => 'edit',
            'acl_action' => 'edit',
          ),
          2 =>
          array (
            'type' => 'actiondropdown',
            'name' => 'main_dropdown',
            'primary' => true,
            'showOn' => 'view',
            'buttons' =>
            array (
              0 =>
              array (
                'type' => 'rowaction',
                'event' => 'button:edit_button:click',
                'name' => 'edit_button',
                'label' => 'LBL_EDIT_BUTTON_LABEL',
                'acl_action' => 'edit',
              ),
              1 =>
              array (
                'type' => 'shareaction',
                'name' => 'share',
                'label' => 'LBL_RECORD_SHARE_BUTTON',
                'acl_action' => 'view',
              ),
              2 =>
              array (
                'type' => 'rowaction',
                'event' => 'button:validate_postal_code:click',
                'name' => 'validate_postal_code',
                'label' => 'LBL_VALIDATE_POSTAL_CODE',
                'acl_action' => 'view',
              ),
              3 =>
              array (
                'type' => 'divider',
              ),
              4 =>
              array (
                'type' => 'rowaction',
                'event' => 'button:duplicate_button:click',
                'name' => 'duplicate_button',
                'label' => 'LBL_DUPLICATE_BUTTON_LABEL',
                'acl_module' => 'Accounts',
              ),
              5 =>
              array (
                'type' => 'rowaction',
                'event' => 'button:audit_button:click',
                'name' => 'audit_button',
                'label' => 'LNK_VIEW_CHANGE_LOG',
                'acl_action' => 'view',
              ),
              6 =>
              array (
                'type' => 'divider',
              ),
              7 =>
              array (
                'type' => 'rowaction',
                'event' => 'button:delete_button:click',
                'name' => 'delete_button',
                'label' => 'LBL_DELETE_BUTTON_LABEL',
                'acl_action' => 'delete',
              ),
            ),
          ),
          3 =>
          array (
            'name' => 'sidebar_toggle',
            'type' => 'sidebartoggle',
          ),
        ),
        'panels' =>
        array (
          0 =>
          array (
            'name' => 'panel_header',
            'header' => true,
            'fields' =>
            array (
              0 =>
              array (
                'name' => 'picture',
                'type' => 'avatar',
                'width' => 42,
                'height' => 42,
                'dismiss_label' => true,
                'readonly' => true,
              ),
              1 => 'name',
              2 =>
              array (
                'name' => 'favorite',
                'label' => 'LBL_FAVORITE',
                'type' => 'favorite',
                'dismiss_label' => true,
              ),
              3 =>
              array (
                'name' => 'follow',
                'label' => 'LBL_FOLLOW',
                'type' => 'follow',
                'readonly' => true,
                'dismiss_label' => true,
              ),
            ),
          ),
          1 =>
          array (
            'name' => 'panel_body',
            'columns' => 2,
            'labelsOnTop' => true,
            'placeholders' => true,
            'fields' =>
            array (
              0 => 'website',
              1 => 'industry',
              2 => 'parent_name',
              3 => 'account_type',
              4 => 'assigned_user_name',
              5 => 'phone_office',
            ),
          ),
          2 =>
          array (
            'name' => 'panel_hidden',
            'hide' => true,
            'columns' => 2,
            'labelsOnTop' => true,
            'placeholders' => true,
            'fields' =>
            array (
              0 =>
              array (
                'name' => 'fieldset_address',
                'type' => 'fieldset',
                'css_class' => 'address',
                'label' => 'LBL_BILLING_ADDRESS',
                'fields' =>
                array (
                  0 =>
                  array (
                    'name' => 'billing_address_street',
                    'css_class' => 'address_street',
                    'placeholder' => 'LBL_BILLING_ADDRESS_STREET',
                  ),
                  1 =>
                  array (
                    'name' => 'billing_address_city',
                    'css_class' => 'address_city',
                    'placeholder' => 'LBL_BILLING_ADDRESS_CITY',
                  ),
                  2 =>
                  array (
                    'name' => 'billing_address_state',
                    'css_class' => 'address_state',
                    'placeholder' => 'LBL_BILLING_ADDRESS_STATE',
                  ),
                  3 =>
                  array (
                    'name' => 'billing_address_postalcode',
                    'css_class' => 'address_zip',
                    'placeholder' => 'LBL_BILLING_ADDRESS_POSTALCODE',
                  ),
                  4 =>
                  array (
                    'name' => 'billing_address_country',
                    'css_class' => 'address_country',
                    'placeholder' => 'LBL_BILLING_ADDRESS_COUNTRY',
                  ),
                ),
              ),
              1 =>
              array (
                'name' => 'fieldset_shipping_address',
                'type' => 'fieldset',
                'css_class' => 'address',
                'label' => 'LBL_SHIPPING_ADDRESS',
                'fields' =>
                array (
                  0 =>
                  array (
                    'name' => 'shipping_address_street',
                    'css_class' => 'address_street',
                    'placeholder' => 'LBL_SHIPPING_ADDRESS_STREET',
                  ),
                  1 =>
                  array (
                    'name' => 'shipping_address_city',
                    'css_class' => 'address_city',
                    'placeholder' => 'LBL_SHIPPING_ADDRESS_CITY',
                  ),
                  2 =>
                  array (
                    'name' => 'shipping_address_state',
                    'css_class' => 'address_state',
                    'placeholder' => 'LBL_SHIPPING_ADDRESS_STATE',
                  ),
                  3 =>
                  array (
                    'name' => 'shipping_address_postalcode',
                    'css_class' => 'address_zip',
                    'placeholder' => 'LBL_SHIPPING_ADDRESS_POSTALCODE',
                  ),
                  4 =>
                  array (
                    'name' => 'shipping_address_country',
                    'css_class' => 'address_country',
                    'placeholder' => 'LBL_SHIPPING_ADDRESS_COUNTRY',
                  ),
                  5 =>
                  array (
                    'name' => 'copy',
                    'label' => 'NTC_COPY_BILLING_ADDRESS',
                    'type' => 'copy',
                    'mapping' =>
                    array (
                      'billing_address_street' => 'shipping_address_street',
                      'billing_address_city' => 'shipping_address_city',
                      'billing_address_state' => 'shipping_address_state',
                      'billing_address_postalcode' => 'shipping_address_postalcode',
                      'billing_address_country' => 'shipping_address_country',
                    ),
                  ),
                ),
              ),
              2 =>
              array (
                'name' => 'phone_alternate',
                'label' => 'LBL_OTHER_PHONE',
              ),
              3 => 'email',
              4 => 'phone_fax',
              5 => 'campaign_name',
              6 =>
              array (
                'name' => 'description',
                'span' => 12,
              ),
              7 => 'sic_code',
              8 => 'ticker_symbol',
              9 => 'annual_revenue',
              10 => 'employees',
              11 => 'ownership',
              12 => 'rating',
              13 =>
              array (
                'name' => 'date_entered_by',
                'readonly' => true,
                'type' => 'fieldset',
                'label' => 'LBL_DATE_ENTERED',
                'fields' =>
                array (
                  0 =>
                  array (
                    'name' => 'date_entered',
                  ),
                  1 =>
                  array (
                    'type' => 'label',
                    'default_value' => 'LBL_BY',
                  ),
                  2 =>
                  array (
                    'name' => 'created_by_name',
                  ),
                ),
              ),
              14 => 'team_name',
              15 =>
              array (
                'name' => 'date_modified_by',
                'readonly' => true,
                'type' => 'fieldset',
                'label' => 'LBL_DATE_MODIFIED',
                'fields' =>
                array (
                  0 =>
                  array (
                    'name' => 'date_modified',
                  ),
                  1 =>
                  array (
                    'type' => 'label',
                    'default_value' => 'LBL_BY',
                  ),
                  2 =>
                  array (
                    'name' => 'modified_by_name',
                  ),
                ),
                'span' => 12,
              ),
            ),
          ),
        ),
        'templateMeta' =>
        array (
          'useTabs' => false,
          'tabDefs' =>
          array (
            'LBL_RECORD_BODY' =>
            array (
              'newTab' => false,
              'panelDefault' => 'expanded',
            ),
            'LBL_RECORD_SHOWMORE' =>
            array (
              'newTab' => false,
              'panelDefault' => 'expanded',
            ),
          ),
        ),
      ),
    ),
  ),
);

Defining the Button Label

Next, define the label for the button:

./custom/Extension/modules/Accounts/Ext/Language/en_us.validatePostalCode.php

  <?php

$mod_strings['LBL_VALIDATE_POSTAL_CODE'] = 'Validate Postal Code';

Extending and Overriding the Controller

Once the button has been added to the metadata, extend and override the record view controller:

./custom/modules/Accounts/clients/base/views/record/record.js

  ({

    extendsFrom: 'RecordView',

    zipJSON: {},

    initialize: function (options) {
        this._super('initialize', [options]);

        //add listener for custom button
        this.context.on('button:validate_postal_code:click', this.validate_postal_code, this);
    },

    validate_postal_code: function() {
        //example of getting field data from current record
        var AcctID = this.model.get('id');
        var currentCity = this.model.get('billing_address_city');
        var currentZip = this.model.get('billing_address_postalcode');

        //jQuery AJAX call to Zippopotamus REST API
        $.ajax({
            url: 'http://api.zippopotam.us/us/' + currentZip,
            success: function(data) {
                    this.zipJSON = data;
                    var city = this.zipJSON.places[0]['place name'];

                    if (city === currentCity)
                    {
                        app.alert.show('address-ok', {
                            level: 'success',
                            messages: 'City and Zipcode match.',
                            autoClose: true
                        });
                    }
                    else
                    {
                        app.alert.show('address-ok', {
                            level: 'error',
                            messages: 'City and Zipcode do not match.',
                            autoClose: false
                        });
                    }
                }
            });
    }
})

Once the files are in place, navigate to Admin > Repair > Quick Repair and Rebuild.

Adding Buttons without Overriding View Controllers

Sometimes your customization might need to add the same buttons to multiple views, but you don't want to overwrite other possible customizations already on the view. The following will walk through creating a custom button like the above example, add adding the buttons to views without overriding the module view controllers.

Create a Custom Button

Buttons typically come in two forms, a standard button and a 'rowaction' in an Action Menu. We can create two buttons to handle both scenarios.

./clients/base/fields/zipcode-check-button/zipcode-check-button.js

  /**
 * Zipcode Check button will check if zipcode matches city field
 *
 * @class View.Fields.Base.ZipcodeCheckButtonField
 * @alias SUGAR.App.view.fields.BaseZipcodeCheckButtonField
 * @extends View.Fields.Base.ButtonField
 */
({
    extendsFrom: 'ButtonField',

    defaultZipCodeField: 'billing_address_postalcode',

    defaultCityField: 'billing_address_city',

    /**
     * @inheritdoc
     */
    initialize: function(options) {
        options.def.events = _.extend({}, options.def.events, {
            'click .zip-check-btn': 'validatePostalCode'
        });

        this._super('initialize', [options]);

        this.def.zip_code_field = _.isEmpty(this.def.zip_code_field)?this.defaultZipCodeField:this.def.zip_code_field;
        this.def.city_field = _.isEmpty(this.def.city_field)?this.defaultCityField:this.def.city_field;
    },

    validatePostalCode: function(evt) {
        var currentCity = this.model.get(this.def.city_field);
        var currentZip = this.model.get(this.def.zip_code_field);

        //jQuery AJAX call to Zippopotamus REST API
        $.ajax({
            url: 'http://api.zippopotam.us/us/' + currentZip,
            success: function(data) {
                this.zipJSON = data;
                var city = this.zipJSON.places[0]['place name'];

                if (city === currentCity)
                {
                    app.alert.show('address-ok', {
                        level: 'success',
                        messages: 'City and Zipcode match.',
                        autoClose: true
                    });
                }
                else
                {
                    app.alert.show('address-ok', {
                        level: 'error',
                        messages: 'City and Zipcode do not match.',
                        autoClose: false
                    });
                }
            }
        });
    }
})

./clients/base/fields/zipcode-check-rowaction/zipcode-check-rowaction.js

  /**
 * Zipcode Check button will check if zipcode matches city field
 *
 * @class View.Fields.Base.ZipcodeCheckRowactionField
 * @alias SUGAR.App.view.fields.BaseZipcodeCheckRowactionField
 * @extends View.Fields.Base.RowactionField
 */
({
    extendsFrom: 'RowactionField',

    defaultZipCodeField: 'billing_address_postalcode',

    defaultCityField: 'billing_address_city',

    /**
     * @inheritdoc
     */
    initialize: function(options) {
        this._super('initialize', [options]);

        this.def.zip_code_field = _.isEmpty(this.def.zip_code_field)?this.defaultZipCodeField:this.def.zip_code_field;
        this.def.city_field = _.isEmpty(this.def.city_field)?this.defaultCityField:this.def.city_field;
    },

    /**
     * Rowaction fields have a default event which calls rowActionSelect
     */
    rowActionSelect: function(evt) {
        this.validatePostalCode(evt);
    },

    validatePostalCode: function(evt) {
        var currentCity = this.model.get(this.def.city_field);
        var currentZip = this.model.get(this.def.zip_code_field);

        //jQuery AJAX call to Zippopotamus REST API
        $.ajax({
            url: 'http://api.zippopotam.us/us/' + currentZip,
            success: function(data) {
                this.zipJSON = data;
                var city = this.zipJSON.places[0]['place name'];

                if (city === currentCity)
                {
                    app.alert.show('address-check-msg', {
                        level: 'success',
                        messages: 'City and Zipcode match.',
                        autoClose: true
                    });
                }
                else
                {
                    app.alert.show('address-check-msg', {
                        level: 'error',
                        messages: 'City and Zipcode do not match.',
                        autoClose: false
                    });
                }
            }
        });
    }
})

Along with the JavaScript controllers for the buttons, you can copy over the detail.hbs and edit.hbs from the base controllers that each button is extended from into each folder. The folder structure will look as follows:

./clients/base/fields/zipcode-check-button/

  • zipcode-check-button.js
  • detail.hbs
  • edit.hbs

./clients/base/fields/zipcode-check-rowaction/

  • zipcode-check-rowaction.js
  • detail.hbs
  • edit.hbs

 In the Handlebar files, you should add a custom CSS class called zip-check-btn to each of the layouts, as a way to find the elements on the page. This is also used for the ZipcodeCheckButton to isolate the event trigger. Example below:

./cilents/base/fields/zipcode-check-button/edit.hbs

  <a href="{{#if fullRoute}}#{{fullRoute}}{{else}}{{#if def.route}}#{{buildRoute context=context model=model action=def.route.action}}{{else}}javascript:void(0);{{/if}}{{/if}}"
    class="btn{{#if def.primary}} btn-primary{{/if}} zip-check-btn"
    {{#if def.tooltip}}
        rel="tooltip"
        data-placement="bottom"
        title="{{str def.tooltip module}}"
    {{/if}}
    {{#if ariaLabel}}aria-label="{{ariaLabel}}"{{/if}}
    role="button" tabindex="{{tabIndex}}" name="{{name}}">{{#if def.icon}}<i class="fa {{def.icon}}"></i> {{/if}}{{label}}</a>

Once we have the button controllers and the templates setup, we can add the buttons to the View metadata for particular modules.

Appending Buttons to Metadata

After creating your buttons, you will need to append the buttons to the views metadata. For this, you can use the Extension Framework. The following examples will add a button to the action menu on the Accounts record view and a button to the Contacts record view. Please note that this example assumes that you have created the rowaction button above.

./custom/Extension/modules/Accounts/Ext/clients/base/views/record/addZipCodeCheckRowaction.php

  $buttons = isset($viewdefs['Accounts']['base']['view']['record']['buttons'])?$viewdefs['Accounts']['base']['view']['record']['buttons']:array();

if (!empty($buttons)) {
    foreach ($buttons as $key => $button) {
        if ($button['type'] == 'actiondropdown' && $button['name'] == 'main_dropdown') {
            $viewdefs['Accounts']['base']['view']['record']['buttons'][$key]['buttons'][] = array(
                'type' => 'divider',
            );
            $viewdefs['Accounts']['base']['view']['record']['buttons'][$key]['buttons'][] = array(
                'type' => 'zipcode-check-rowaction',
                'event' => 'button:zipcode_check:click',
                'name' => 'zipcode_check_button',
                'label' => 'LBL_ZIPCODE_CHECK_BUTTON_LABEL',
                'acl_action' => 'edit',
                'showOn' => 'view',
            );
            break;
        }
    }
}

./custom/Extension/modules/Contacts/Ext/clients/base/views/record/addZipCodeCheckButton.php

  $buttons = isset($viewdefs['Contacts']['base']['view']['record']['buttons'])?$viewdefs['Contacts']['base']['view']['record']['buttons']:array();
$zipCodeButton = array (
    'type' => 'zipcode-check-button',
    'event' => 'button:zipcode_check:click',
    'name' => 'zipcode_check_button',
    'label' => 'LBL_ZIPCODE_CHECK_BUTTON_LABEL',
    'acl_action' => 'edit',
    'zip_code_field' => 'primary_address_postalcode',
    'city_field' => 'primary_address_city'
);

if (!empty($buttons)){
    foreach($buttons as $key => $button){
        if ($button['type'] == 'actiondropdown' && $button['name'] == 'main_dropdown'){
            //Get everything from this point down
            $slicedBtns = array_slice($viewdefs['Contacts']['base']['view']['record']['buttons'],$key);
            //Remove everything from this point down
            array_splice($viewdefs['Contacts']['base']['view']['record']['buttons'],$key);
            //Add Zip Code Button
            $viewdefs['Contacts']['base']['view']['record']['buttons'][] = $zipCodeButton;
            //Add back the buttons we removed
            foreach($slicedBtns as $oldButton){
                $viewdefs['Contacts']['base']['view']['record']['buttons'][] = $oldButton;
            }
            break;
        }
    }
} else {
    $viewdefs['Contacts']['base']['view']['record']['buttons'] = array(
        $zipCodeButton
    );
}
unset($zipCodeButton);

The last thing that is needed now, is to define the button's label.

Defining the Button Labels

Since the button was made to work globally on multiple modules, we can define the label at the application level.

./custom/Extension/application/Ext/Language/en_us.ZipCodeCheckButton.php

  $app_strings['LBL_ZIPCODE_CHECK_BUTTON_LABEL'] = 'Verify Zip Code';

 Once all files are in place, you can run a Quick Repair and Rebuild and the buttons will display and check the Zipcode fields for both the Contacts and Accounts record views without having to extend either modules RecordView controllers.