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',
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.