How to Build Custom Record Views in Sugar

Note: This content was originally posted on Upsert's blog on October 31, 2019.

Have you ever wanted to create a custom route in Sugar that allows you to create, display, and edit a subset of a module's fields? Well, you're in luck!

In this post we will cover:

  • Creating views to handle record creation, viewing, and editing
  • Creating layouts to display those views
  • Creating routes to beautify your URLs

For our example, we will create a set of Account views that allow us to show a limited set of fields. This will work separately from the stock record view but behave the same.

  • To access the limited account record view, a user can directly navigate to <sugar_url>/#Accounts/<account_id>/limited
  • To access the limited account record edit view, a user can directly navigate to <sugar_url>/#Accounts/<account_id>/limited/edit
  • To access the limited account record create view, a user can directly navigate to <sugar_url>/#Accounts/limited/create

Why would you want to do this you ask? This can be beneficial in situations where you don't want to leverage Sugar's Role-Based Record View Layouts or if you have a data entry team that needs to populate fields without the noise that may come from a crowded record view.

Record View

To handle viewing and editing existing records, we will create a record-limited view that will be located in ./custom/modules/Accounts/clients/base/views/record-limited/record-limited.js.

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

({
    extendsFrom: 'AccountsRecordView',

    /**
     * @inheritdoc
     */

    _loadTemplate: function (options) {
        this.tplName = 'record';
        this.template = app.template.getView(this.tplName);
    },

    /**
     * @inheritdoc
     */

    setRoute: function (action) {
        if (!this.meta.hashSync) {
            return;
        }

        if (action == 'edit') {
            action = 'limited/' + action;
        } else if (action == 'detail' || _.isEmpty(action)) {
            action = 'limited';
        }

        app.router.navigate(app.router.buildRoute(this.module, this.model.id, action), { trigger: false });
    },
})

Let's break down the record-limited.js file:

extendsFrom: 'AccountsRecordView',
The extendsFrom property allows us to specify the component we want to extend our view from. Normally, you would see extendsFrom: 'RecordView' however, we want to ensure that we extend the base accounts record view found in ./modules/Accounts/clients/base/views/record/record.js so that the existing core functionality isn't lost and that the historical summary button continues to work.
/**
* @inheritdoc
*/

_loadTemplate: function (options) {
    this.tplName = 'record';
    this.template = app.template.getView(this.tplName);
},

The _loadTemplate function allows us to load a template by another name. By default, Sugar will look for a template matching our view name of record-limited.hbs in ./custom/modules/Accounts/clients/base/views/record-limited/. As we want to extend and reuse the core record view, we will set this.tplName to record.

/**
* @inheritdoc
*/

setRoute: function (action) {
    if (!this.meta.hashSync) {
        return;
    }

    if (action == 'edit' || action == 'create') {
        action = 'limited/' + action;
    } else if (action == 'detail' || _.isEmpty(action)) {
        action = 'limited';
    }

    app.router.navigate(app.router.buildRoute(this.module, this.model.id, action), { trigger: false });
},

The setRoute function allows us to make sure the URL routes are set correctly when returning from our view. This is mainly for aesthetic purposes but ensures that the user does not get confused by the URL or have any copy & paste issues.

Record View Metadata

Next, we will create the record-limited metadata that will be located in ./custom/modules/Accounts/clients/base/views/record-limited/record-limited.php. This will define the buttons and fields that are displayed in our view.

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

<?php

$viewdefs['Accounts'] = array(
  'base' => array(
    'view' => array(
      'record-limited' => array(
        'buttons' => array(
          0 => array(
            'type' => 'button',
            'name' => 'cancel_button',
            'label' => 'LBL_CANCEL_BUTTON_LABEL',
            'css_class' => 'btn-invisible btn-link',
            'showOn' => 'edit',
            'events' => array(
              'click' => 'button:cancel_button:click',
            ),
          ),
          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' => 'pdfaction',
                'name' => 'download-pdf',
                'label' => 'LBL_PDF_VIEW',
                'action' => 'download',
                'acl_action' => 'view',
              ),
              3 => array(
                'type' => 'pdfaction',
                'name' => 'email-pdf',
                'label' => 'LBL_PDF_EMAIL',
                'action' => 'email',
                'acl_action' => 'view',
              ),
              4 => array(
                'type' => 'divider',
              ),
              5 => array(
                'type' => 'rowaction',
                'event' => 'button:find_duplicates_button:click',
                'name' => 'find_duplicates_button',
                'label' => 'LBL_DUP_MERGE',
                'acl_action' => 'edit',
              ),
              6 => array(
                'type' => 'rowaction',
                'event' => 'button:duplicate_button:click',
                'name' => 'duplicate_button',
                'label' => 'LBL_DUPLICATE_BUTTON_LABEL',
                'acl_module' => 'Accounts',
                'acl_action' => 'create',
              ),
              7 => array(
                'type' => 'rowaction',
                'event' => 'button:historical_summary_button:click',
                'name' => 'historical_summary_button',
                'label' => 'LBL_HISTORICAL_SUMMARY',
                'acl_action' => 'view',
              ),
              8 => array(
                'type' => 'rowaction',
                'event' => 'button:audit_button:click',
                'name' => 'audit_button',
                'label' => 'LNK_VIEW_CHANGE_LOG',
                'acl_action' => 'view',
              ),
              9 => array(
                'type' => 'divider',
              ),
              10 => 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',
            'label' => 'LBL_PANEL_HEADER',
            'header' => true,
            'fields' => array(
              0 => array(
                'name' => 'picture',
                'type' => 'avatar',
                'size' => 'large',
                'dismiss_label' => true,
                'readonly' => true,
              ),
              1 => array(
                'name' => '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',
            'label' => 'LBL_RECORD_BODY',
            'columns' => 2,
            'labelsOnTop' => true,
            'placeholders' => true,
            'newTab' => false,
            'panelDefault' => 'expanded',
            'fields' => array(
              0 => 'industry',
              1 => 'website',
              2 => 'parent_name',
              3 => 'account_type',
              4 => 'service_level',
            ),
          ),
        ),
        'templateMeta' => array(
          'useTabs' => false,
        ),
      ),
    ),
  ),
);

This file is largely a duplicate of the core Accounts record view metadata, originally located in ./modules/Accounts/clients/base/views/record/record.php, with a limited set of fields. More information on view metadata can be found in the Sugar Developer Guide.


Record Layout

To display our new record-limited view, we will need to create a record-limited layout that will be located in ./custom/modules/Accounts/clients/base/layouts/record-limited/record-limited.php.

./custom/modules/Accounts/clients/base/layouts/record-limited/record-limited.php

<?php

$viewdefs['Accounts']['base']['layout']['record-limited'] = array(
    'components' => array(
        array(
            'layout' => array(
                'type' => 'default',
                'name' => 'sidebar',
                'components' => array(
                    array(
                        'layout' => array(
                            'type' => 'base',
                            'name' => 'main-pane',
                            'css_class' => 'main-pane span8',
                            'components' => array(
                                array(
                                    'view' => 'record-limited',
                                    'primary' => true,
                                ),
                                array(
                                    'layout' => 'extra-info',
                                ),
                                array(
                                    'layout' => array(
                                        'type' => 'filterpanel',
                                        'last_state' => array(
                                            'id' => 'record-filterpanel',
                                            'defaults' => array(
                                                'toggle-view' => 'subpanels',
                                            ),
                                        ),
                                        'refresh_button' => true,
                                        'availableToggles' => array(
                                            array(
                                                'name' => 'subpanels',
                                                'icon' => 'fa-table',
                                                'label' => 'LBL_DATA_VIEW',
                                            ),
                                            array(
                                                'name' => 'list',
                                                'icon' => 'fa-table',
                                                'label' => 'LBL_LISTVIEW',
                                            ),
                                            array(
                                                'name' => 'activitystream',
                                                'icon' => 'fa-clock-o',
                                                'label' => 'LBL_ACTIVITY_STREAM',
                                            ),
                                        ),
                                        'components' => array(
                                            array(
                                                'layout' => 'filter',
                                                'xmeta' => array(
                                                    'layoutType' => '',
                                                ),
                                                'loadModule' => 'Filters',
                                            ),
                                            array(
                                                'view' => 'filter-rows',
                                            ),
                                            array(
                                                'view' => 'filter-actions',
                                            ),
                                            array(
                                                'layout' => 'activitystream',
                                                'context' => array(
                                                    'module' => 'Activities',
                                                ),
                                            ),
                                            array(
                                                'layout' => 'subpanels',
                                            ),
                                        ),
                                    ),
                                ),
                            ),
                        ),
                    ),
                    array(
                        'layout' => array(
                            'type' => 'base',
                            'name' => 'dashboard-pane',
                            'css_class' => 'dashboard-pane',
                            'components' => array(
                                array(
                                    'layout' => array(
                                        'type' => 'dashboard',
                                        'last_state' => array(
                                            'id' => 'last-visit',
                                        ),
                                    ),
                                    'context' => array(
                                        'forceNew' => true,
                                        'module' => 'Home',
                                    ),
                                    'loadModule' => 'Dashboards',
                                ),
                            ),
                        ),
                    ),
                    array(
                        'layout' => array(
                            'type' => 'base',
                            'name' => 'preview-pane',
                            'css_class' => 'preview-pane',
                            'components' => array(
                                array(
                                    'layout' => 'preview',
                                ),
                            ),
                        ),
                    ),
                ),
            ),
        ),
    ),
);

This file is largely a duplicate of the core record layout metadata, originally located in ./clients/base/layouts/record/record.php, with the exception of our metadata index being $viewdefs['Accounts']['base']['layout']['record-limited'] and the view pointing to record-limited instead of record. More information on layouts can be found in the Sugar Developer Guide.


Creation View

To handle creating new records, we will create a create-limited view that will be located in ./custom/modules/Accounts/clients/base/views/create-limited/create-limited.js.

./custom/modules/Accounts/clients/base/views/create-limited/create-limited.js

({
    extendsFrom: 'CreateView',

    /**
     * @inheritdoc
     */

    initialize: function (options) {
        options.meta = options.meta || {};
        options.meta = _.extend(app.metadata.getView(null, 'create'), options.meta);
        options.meta = _.extend(app.metadata.getView(options.module, 'record-limited'), options.meta);
        this._super('initialize', [options]);
    },

    /**
     * @inheritdoc
     */

    saveAndClose: function () {
        this.initiateSave(_.bind(function () {
            if (this.closestComponent('drawer')) {
                app.drawer.close(this.context, this.model);
            } else {
                app.navigate(this.context, this.model, 'limited');
            }
        }, this));
    },
})

Let's break down the create-limited.js file:

extendsFrom: 'CreateView',

The extendsFrom property allows us to specify the component we want to extend our view from. In the record-limited view example above, we extended from AccountsRecordView. As we do not have a ./modules/Accounts/clients/base/views/create/create.js in the Sugar core product, we won't be able to extend from AccountsCreateView and can default to using CreateView.

/**
* @inheritdoc
*/

initialize: function (options) {
    options.meta = options.meta || {};
    options.meta = _.extend(app.metadata.getView(null, 'create'), options.meta);
    options.meta = _.extend(app.metadata.getView(options.module, 'record-limited'), options.meta);
    this._super('initialize', [options]);
},

The initialize function allows us to override and populate custom metadata into the view before it's loaded. Due to how Sugar view inheritance works, and because we want our create metadata to match what's defined in ./custom/modules/Accounts/clients/base/views/record-limited/record-limited.php, we can tell the controller to load the default create buttons from ./clients/base/views/create/create.php with the code snippet options.meta = _.extend(app.metadata.getView(null, 'create'), options.meta) and then to fill in the rest of the metadata from ./custom/modules/Accounts/clients/base/views/record-limited/record-limited.php with the code snippet options.meta = _.extend(app.metadata.getView(options.module, 'record-limited'), options.meta). You could opt to not use this approach and create a ./custom/modules/Accounts/clients/base/views/create-limited/create-limited.php file with your field definitions.

/**
* @inheritdoc
*/

saveAndClose: function () {
    this.initiateSave(_.bind(function () {
        if (this.closestComponent('drawer')) {
            app.drawer.close(this.context, this.model);
        } else {
            app.navigate(this.context, this.model, 'limited');
        }
    }, this));
},

The saveAndClose function is what gets called upon save. The key change here is that app.navigate(this.context, this.model, 'limited') directs users to the record-limited layout instead of the stock record layout.


Create Layout

To display our new create-limited view, we will need to create a create-limited layout that will be located in ./custom/modules/Accounts/clients/base/layouts/create-limited/create-limited.php.

./custom/modules/Accounts/clients/base/layouts/create-limited/create-limited.php

<?php

$viewdefs['Accounts']['base']['layout']['create-limited'] = array(
    'components' => array(
        array(
            'layout' => array(
                'type' => 'default',
                'name' => 'sidebar',
                'last_state' => array(
                    'id' => 'create-default',
                ),
                'components' => array(
                    array(
                        'layout' => array(
                            'type' => 'base',
                            'name' => 'main-pane',
                            'css_class' => 'main-pane span8',
                            'components' => array(
                                array(
                                    'view' => 'create-limited',
                                ),
                            ),
                        ),
                    ),
                    array(
                        'layout' => array(
                            'type' => 'base',
                            'name' => 'preview-pane',
                            'css_class' => 'preview-pane',
                            'components' => array(
                                array(
                                    'layout' => 'preview',
                                ),
                            ),
                        ),
                    ),
                ),
            ),
        ),
    ),
);

This file is largely a duplicate of the core create layout metadata, originally located in ./clients/base/layouts/create/create.php, with the exception of our metadata index being $viewdefs['Accounts']['base']['layout']['create-limited'] and the view pointing to create-limited instead of record.

Layout Routing

Now that we have our views and layouts in place, we can define our routes. To accomplish this, we must first define a javascript file containing our routes. This file can exist anywhere you like, though we recommend ./custom/include/JavaScript/.

./custom/include/JavaScript/myCustomRoutes.js

(function (app) {
    app.events.on("router:init", function () {
        var routes = [
            {
                route: 'Accounts/:id/limited',
                name: 'AccountsRecordLimited',
                callback: function () {
                    App.controller.loadView({
                        module: 'Accounts',
                        layout: 'record-limited',
                        modelId: arguments[0],
                        action: 'detail',
                    });
                }
            },
            {
                route: 'Accounts/:id/limited/edit',
                name: 'AccountsRecordLimitedEdit',
                callback: function () {
                    App.controller.loadView({
                        module: 'Accounts',
                        layout: 'record-limited',
                        modelId: arguments[0],
                        action: 'edit',
                    });
                }
            },
            {
                route: 'Accounts/limited/create',
                name: 'AccountsCreateLimited',
                callback: function () {
                    App.controller.loadView({
                        module: 'Accounts',
                        layout: 'create-limited',
                        create: true,
                        action: 'create',
                    });
                }
            }
        ];
        app.router.addRoutes(routes);
    })
})(SUGAR.App);

This file contains the 3 routes we will use for creating, viewing, and editing. The important thing to note here is that if you are accepting variables (i.e. ":id") from the route path, they will be available as arguments in the route's callback. More detailed information on routing can be found in the Developer Guide.

Next, we need to add the routes file to our JSGroupings. To accomplish this we will create ./custom/Extension/application/Ext/JSGroupings/CustomRecordViews.php and append our JavaScript file to the ./include/javascript/sugar_grp7.min.js file.

./custom/Extension/application/Ext/JSGroupings/CustomRecordViews.php

<?php

foreach ($js_groupings as $key => $groupings) {
    $target = current(array_values($groupings));
    if ($target == 'include/javascript/sugar_grp7.min.js') {
        $js_groupings[$key]['custom/include/JavaScript/myCustomRoutes.js'] = 'include/javascript/sugar_grp7.min.js';
    }
}

More information on using JSGroupings with routes can be found in the Developer Guide.

Finally, navigate to Admin > Repair > Quick Repair & Rebuild. Once complete, navigate to any of the following URLs to work with your new view:

  • Create - <sugar_url>/#Accounts/limited/create
  • View - <sugar_url>/#Accounts/<account_id>/limited
  • Edit - <sugar_url>/#Accounts/<account_id>/limited/edit

It is important to note that if you need to make additional changes to your routes, you will need to rebuild the js grouping files by navigating to Admin > Repair > Rebuild JS Grouping Files.

Note: This code example was written against Sugar 9.0.0 Professional. You can view the Sugar code for the example above here or download the module loadable package attached to this post.

_2019-10-31_-_How_to_Build_Custom_Record_Views_in_Sugar_v1.0.0.zip