We welcome all trick or treaters to Upsert's Haunted House where we like to give out coder treats. You may feel that developing in Sugar is scary but we are here to make the experience a breeze.
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:
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.
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.
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.
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.
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.
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.
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
.
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:
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 and download the Sugar code for the example above here.
Happy Halloween!
Are you new to Sugar? Start a free trial today.
Please follow us on social media or subscribe to our RSS feed to keep up-to-date on new blog posts and announcements:
To read more about our company and services, please visit our home page.