Skip navigation
All Places > Developer > Blog
1 2 3 Previous Next

Developer

215 posts

Hello Sugar Developers!

 

We want to make sure your customizations and integrations are ready for the Sugar Q3 2020 release. So we're hosting a webinar just for you! In addition to details about the latest release, we will also be looking at topics that have been requested frequently since our last webinar.

 

What we will be covering:

Some of the big changes that are likely to impact you include the following:

  • New modules
  • Updates to Opportunities module
  • Module Loader API improvements
  • UI changes
  • Bug fixes

 

Webinar Information:

We are holding two sessions to accommodate various geographical locations. On the registration page, you will have the choice of ONE of the following times.

 

Tuesday, June 23 at 4:30 - 5:30 PM PDT (7:30 - 8:30 PM EDT)
OR 
Wednesday, June 24 at 7:00 - 8:00 AM PDT (10:00 - 11:00 AM EDT)

 

REGISTER NOW

 

We will be posting the webinar recordings to the Sugar Community for those who are unable to attend the live sessions.

Integrations are all about enabling communication between disparate systems. So, when building a new integration, one of your top priorities should be to create frameworks for data transfer. Specifically, methods for detecting events in your endpoints and for sending and receiving new data rapidly.

 

Luckily, we like to give your integrations super powers, so every Adapter comes with pre-built, standardized event handling and bulk action frameworks. If you use Sugar Integrate, you can take advantage of these frameworks out of the box to get your integrations ready to go faster than ever.

 

 

Eventing

Normally for detecting events happening within an endpoint, you have two options: webhooks or polling.

 

Some platforms have built-in functionality to post events to webhooks, and you can configure that functionality through the Sugar Integrate UI and make sure that any data you receive is standardized. There are, however, many API vendors that do not offer webhook functionality out of the box. For these APIs you’ll need to use a polling framework.

 

 

Polling works by querying the endpoint for any objects that have changed within a set polling interval. If the query finds anything, it will return the new or changed data. While this sounds like a simple process, successfully implementing polling can be very complicated and varies from vendor to vendor.

 

One of the great features of Sugar Integrate is that all of our Adapters have a pre-built polling framework, so we handle the messy backend and automatically update any new data, just like using a webhook.

 

To set up a simple polling framework in Sugar Integrate, all you have to do is set up the polling event type and provide some objects to monitor for changes. For example if you set up Sugar Integrate to poll contacts every 15 minutes, that will say, “every 15 minutes, go out and check to see if any contacts have changed, and if that is the case, post the changes to a webhook.”

 

 

These events can then be used to trigger procedures. So, when Sugar Integrate polls Sugar Sell and notes a change in contacts, a custom procedure could be kicked off to do something with that data (like send a message to a Slack channel, or manipulate the data and then send it to another platform, etc).

 

Bulk

Sugar Integrate has two ways of implementing bulk actions with our Adapters. For the endpoints that natively support batch or bulk APIs, we can take advantage of those directly, but many do not support this functionality. For these endpoints, we’ve built a custom framework on top of the standard API to sort of create fake bulk actions.

 

For example, if you want to pull all the contacts out of an adapter that doesn't have a native batch or bulk API, you can implement this add-on framework which will one-by-one, pull out all those contacts and push them into a single file that you can then download.

 

One thing to note about our bulk framework is that it works asynchronously. What that means is that in order to create a bulk download file, you’ll actually execute two API calls. The first one will be the POST /bulk/query call, which will create a bulk job and then pull, individually, all the contacts out of that endpoint. The second one you’ll use is the GET /bulk/{id}/{object name} , which will actually grab the downloaded file once that first bulk job is finished. We have a similar process for uploading in bulk to an endpoint as well.

 

 

For any of the endpoints that do not natively support bulk or batch, our bulk framework is definitely something you’ll want to know, since it is an enhancement on top of the base API that is only available through our platform. It gives you a big advantage over a standard DIY integration, because you can use it to sync first-time data or import existing data from one CRM system to another, so it’s definitely worth playing around with.

In Sugar Q1 2020 (10.0), we made some significant updates to our color palette. Seven LessJS variables have been removed. This has caused a small number of cases where there are issues with custom themes that relied on these variables. But, don't worry! This is an easy fix.

 

To add a custom color variable to your theme, locate the ~/styleguide/less/fixed_variables.less file. The color variables already defined in Sugar start near line 42.

 

 

We are going to create our own custom LessJS file using this one as a guide. So, first, we will create a Module Loadable Package that consists of 2 files - the manifest and custom.less. In the manifest's installdefs section, you will add a copy directive that will put the custom.less file into custom/themes/custom.less (some of you may already have this file. if so, you will simply want to edit it in whatever package you have already used for your customization. It can be a new version that you will upload and install.

 

In this file, you can add as many new variables as you like. In this example, I have added a definition for the variable called "@mint". Now I can use it throughout the CSS (LessJS) in my custom theme.

@mint: #18e7d2;

 

These are the color variables that were removed:

@moss: #33800d;
@stone: #0f7799;
@cider: #7e6017;
@rose: #ebaaaa;
@cream: #fdf8ee;
@mint: #18e7d2;
@brightBlue: #1202f5;

 

I won't go into the details here on how to install an MLP. For more information see this support article.

For more information on custom themes, see this article.

In case you missed it, I want to draw everyone's attention to the new Sugar Integrate space here in the Developer community.

 

We've launched a new product that I think you guys will love but it's very different than Sugar Enterprise. So we wanted to create a space specifically to support getting our worldwide developer community skilled and drilled on our new integration platform.

 

Where it makes sense, we'll be cross posting blogs and content into both spaces. But the new space is where you'll find the details you need to get you and your team ready to build and deploy integrations using the new platform.

Hello Sugar Developers!

 

We have another release ready for you with new features and fixes! We are calling this one Sugar Q2 2020 (or Sugar 10.0.0). This release is for both cloud and on-premise customers. 

 

In March, we conducted a webinar highlighting the changes of Sugar 10.0.0. If you missed it, you can watch the recording or browse through the slides.

 

Here's a quick list of some of the features in this Q2 2020 release:

  • Field Label Placement - By default, record view field label positioning will be to the left of the field ffxing the “white space issue” on record views. This is a new setting in the advanced tab of the User profile. So, note that it is per-user and cannot be set globally.  In fact, this setting is replacing the labelsOnTop viewDef which is deprecated as of this release. Calls and Meetings do not play nicely with side labels. So, these record views may look a bit "off". If this setting affects one of your customizations, you may want to look at a custom css fix to override the side placement (as was mentioned in the release webinar).
  • Product Catalog module - The product catalog module is now available to all users OOTB
  • Edit Preview View Layout - Previously, Preview views would use the same basic layout as record views and could only be changed via a customization. Now, the Preview view can be configured in Studio to be unique from the record views
  • Leads Tile View - Tile View has been enabled for leads. A lead can be converted by dragging the tile to the Converted column. Note, it cannot be dragged out of that column (because a lead cannot be un-converted)
  • New Dashlets available for Ent - Ent now has the Record Dashlet, Interactions Dashlet, and Comments Dashlet available.
  • Relate Fields Denormalization - Relate Fields Denormalization is a Sugar administration tool that can be used by customers with very large database tables to optimize the database structure for faster sorting and data load of relate-type field values on list view pages. The denormalization process will copy the values of stock Sugar relate fields from their own tables into a text field (e.g., denorm_field_name) in the table of the related module (i.e. the parent table) via cron job. The new text field is then used for sorting in list views. Then, a logic hook will continue to monitor any value changes made to the relate field, the source field that the relate field points to, and to the linked record ID. The logic hook will also react to changes made to the relationship. 
    Note: While denormalizing the data will speed up sorting in list views, it may increase the time it takes to update records. So, you need to determine what is most important in your situation - faster sorting in list views or faster record updates.
  • Home Dashboard list limit - The maximum number of Dashboards that will display in the Home Dashboard List has been increased from 20 to 50
  • REST API endpoint deprecated - Currently, records with many SugarLogic related value formulas can cause the URI to become too long using GET, causing a 414 error. Therefore, the existing GET endpoint for the ExpressionEngine's related values API (/ExpressionEngine/:record/related) has been deprecated. It has been replaced with a POST endpoint of the same name.
  • Module Loader API - new REST Endpoints have been added for working with Module Loadable Packages. View the release notes for the specific endpoints and their parameters.

 

Check out the below resources that have the rest of the details.

Hello Sugar Developers!

It's that time again - a new Release of Sugar is upon us. To ensure that you are ready for the Sugar Q2 2020 release, we're hosting another set of webinars just for you!

 

What we will be covering

Some of the big changes that are likely to impact you including the following:

  • New Module Loader API
  • Data denormalization utility
  • UI changes
  • API changes
  • Bug fixes

Webinar Information:

We are holding two sessions to accommodate various geographical locations.On the registration page, you will have the choice of ONE of the following times.

 

Tuesday, March 17th 4:30 - 5:30 PM PT

OR

Wednesday, March 18th 7:00 - 8:00 AM PT

 

Register Now!


We will be posting the webinar recordings to the Sugar Community for those who are unable to attend the live sessions.

We have 4 different sets of servers on one of my SugarCRM Enterprise installs.  We also have 4 people who do development, QA or Admin work so it is vitally important that everyone is aware of what server they are on.  We all move among the servers all day and sometimes we forget which server we are on.  We don't want a developer testing something or adding/deleting records in Production or a QA engineer to test something on a development server.  I tried a few different options, mostly coloring the tabs and while this worked for a short while we found ourselves ignoring the colors after a week or two.  I needed something that was more in your face.  So I added a watermark to the code.

Watermark

 

You cant ignore that.  It is only seen by the 4 people in question and all other users see nothing.  It appears on every page, custom modules, admin screen, everywhere.  All I did was add a custom base.js file at custom/clients/base/layouts/base/base.js that looks like this

/*
* Your installation or use of this SugarCRM file is subject to the applicable
* terms available at
* http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/.
* If you do not agree to all of the applicable terms or do not have the
* authority to bind the entity as an authorized representative, then do not
* install or use this SugarCRM file.
*
* Copyright (C) SugarCRM Inc. All rights reserved.
*/

/**
* The Base Layout that all Layouts should extend from before extending
* {@link #View.Layout}.
*
* Use this controller to specify your customizations for the Base platform.
* This should contain any special override that only applies to Base platform
* and not to Sidecar's library.
*
* Any Layout in a module can skip the default fallback and extend this one
* directly. In your `BaseModuleMyLayout` component that lives in the file
* `modules/<module>/clients/base/layouts/my-layout/my-layout.js`, you can
* directly extend the `BaseLayout` skipping the normal extend flow which will
* extend automatically from `BaseMyLayout` that might live in
* `clients/base/layouts/my-layout/my-layout.js`. Simply define your controller
* with:
*
* ```
* ({
*     extendsFrom: 'BaseLayout',
*     // ...
* })
* ```
*
* This controller exists to force the component to be created and not fallback
* to the default flow (which happens when the component isn't found).
*
* @class View.Layouts.Base.BaseLayout
* @alias SUGAR.App.view.layouts.BaseBaseLayout
* @extends View.Layout
*/

({
    /**
     * The Base Layout will always clear any tooltips after `render` or `dispose`.
     */

    initialize: function() {
        //watermark code 1.0
        //We have 4 users that need to see the watermark, we hide it for everyone else
        //  I might change this to use the department field on the user in the future but
        //  this works for now.
        let userID = app.user.id;
        if (userID === '20821fb3-541e-2d6e-1b89-56b8c6d65ce9' ||
            userID === 'b8f60911-ead1-31d9-1709-56cb52895212' ||
            userID === '0b069d40-2b7f-11e7-b49b-52d504b662cb' ||
            userID === '1cf7b3d3-73d0-73d9-9cbd-5527de354afc') {
            let url = window.location.href;
            //I test for the different URLs of the different machines.  If none are matched I assume
            // its a local copy on the users machine
            if (url.indexOf('production') !== -1) {
                $('head').append('<link rel="stylesheet" type="text/css" href="custom/themes/default/css/prod.css">');
            } else if (url.indexOf('qa') !== -1) {
                $('head').append('<link rel="stylesheet" type="text/css" href="custom/themes/default/css/dive.css">');
            } else if (url.indexOf('development') !== -1) {
                $('head').append('<link rel="stylesheet" type="text/css" href="custom/themes/default/css/dev.css">');
            } else {
                $('head').append('<link rel="stylesheet" type="text/css" href="custom/themes/default/css/local.css">');
            }
        }


        this._super('initialize', arguments);
        if (app.tooltip) {
            this.on('render', app.tooltip.clear);
        }
    }
})

 

This adds a CSS file from the custom/themes/default/css directory to the page depending on its URL.  You would have to modify it to use your URL naming scheme.  You would need to create 1 CSS file for each of your server types.  The CSS it loads looks like this

 

custom/themes/default/css/local.css

html:after {
    /* common custom values */
    content: "LOCAL"; /* your site name */
    font-size: 225px; /* font size */
    color: rgba(0, 0, 50, .1);
    /* alpha, could be even rgba(0,0,0,.05) */

    /* rest of the logic */
    z-index: 9999;
    cursor: default;
    display: block;
    position: fixed;
    top: 33%;
    right: 0;
    bottom: 0;
    left: 15%;
    font-family: sans-serif;
    font-weight: bold;
    font-style: italic;
    text-align: center;
    line-height: 100%;

    /* not sure about who implemented what ..
      ... so bring it all */

    -webkit-pointer-events: none;
    -moz-pointer-events: none;
    -ms-pointer-events: none;
    -o-pointer-events: none;
    pointer-events: none;

    -webkit-transform: rotate(-40deg);
    -moz-transform: rotate(-40deg);
    -ms-transform: rotate(-40deg);
    -o-transform: rotate(-40deg);
    transform: rotate(-40deg);

    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    -o-user-select: none;
    user-select: none;
}

 

And that's all there is to it.  This seems like a tiny thing but it has been the second most useful customization I have yet made to our system.

Testing email notifications is something we find ourselves doing rather often while developing Sugar customizations. Ideally, while doing this, we don’t want to send out emails to recipients not involved in the test. While I usually manage to follow this guideline, recently I failed spectacularly. I was using a local Sugar instance (hosted on vagrant) to develop a scheduled job that would set the value of a field in Meetings module. The email settings in this local instance were pointing to our production email server (first mistake). I used the $bean->save() function in the Meetings module, which triggered a flood of record assignment notifications to all Sugar users who have a meeting assigned to them. This was a somewhat embarrassing result that I did not want to repeat. I started looking into ways to test email notifications without sending out actual emails. Turned out this can be done by using MailCatcher.

 

The first step is to install MailCatcher on the virtual machine where the local Sugar instance is hosted. The second step is to setup email on Sugar to work with MailCatcher, as shown in the image below.

mail settings to use with MailCatcher

 

Once you have MailCatcher working on localhost, all outbound emails will show up almost instantly in the MailCatcher tab at http://localhost:1080 (see image below), and will not go any further than that. This makes email testing much less stressful.

Code Completion

I have finally gotten around to improving my development environment and I chose to start with code completion. I use phpStorm but I think everything here would apply to any editor out there. When Sugar went to the BeanFactory to instantiate a bean, the code completion stopped working. If you define your bean class in the DocBlock or in an inline phpDocumentor tag, some of the functionality returns but not all. Custom fields and relationships are never available as they are never added to the public variable list on the class. To demonstrate what I am talking about this is how a DocBlock can handle code completion:
Code Completion Example

 

You see that the 'name' field is not available until after I tell the editor that $bean represents the Call class.  But, even then the custom fields that I have added to the Calls module are not available - only the stock ones. Of course you cannot define the $bean of every function as some function accept data from several modules.  It might be a Call one time and a Meeting another.  In these cases you can define the $bean as a SugarBean instead of a Call or Meeting and get some functionality out of it.

 

An inline tag works in a similar way and is used to define the class of a BeanFactory call.
Inline Tag Example
Again, the fields are not shown until after I tell the editor that $quoteBean represents the IN_Orders class.  After that, all the stock fields and relationships (and functions I guess) are available to you.

 

So, that's step one. I have to get better about documenting my functions and using inline tags to decorate my BeanFactory calls. Not hard I guess, but that is only getting me half my code completion options. I need my custom fields on that list. The code completion options are taken from the classes and the variables in them. So somehow we have to get the custom fields that were added to the modules onto the classes...

 

I went through about 10 different methods before I settled on 2. One is effortless but means you have to update stock files. The other is more coding work but uses custom files.  I use the first option as updating my stock files in this way is not a big deal.  My .gitignore file keeps them from getting added to my commits. So, the changes only exist on my development files and the utility I use to add them has a clean up option. We will get to that later but rest assured, unless you are actually editing your live files directly (don't do that) you will be fine with option one.

 

The Editor Helper Tool
The Editor Helper Tool

This tool is available for download from this blog post and is licensed under the MIT license. It allows you to update your class files all at once and has an option to remove everything it added if that becomes necessary. When you run it (It adds itself to the Admin menu) in either the 'Normal' or 'Custom' modes the first thing it does is remove any code it added on the previous run. Then it looks at every module for custom fields and adds them to the class files. So, if you do a git clone or a version upgrade, you can just go to the admin menu and run this tool. It will set everything up again no matter what.

 

So, if you run it in the 'Normal' mode, the Accounts class file (modules/Accounts/Accounts.php) will end up looking like this:

Accounts Class File

 

Now the class file has all my custom fields inserted into it and code completion will work for all fields - stock and custom. This is safe for me to do on my system as it only exists on my development box. My .gitignore will prevent me from ever creating a commit that would add these changes to production code as they are not needed to actually run Sugar and are only there to make the editor work better. Whenever I add or remove custom fields I can just re-run this tool and it will update the classes for me. In this mode I wouldn't have to change my coding style at all. I can just do things like this to make sure both $bean and $masterAccount are seen as Accounts in the code.

     /**
      * @param Account $bean
      * @param string $events
      * @param array $arguments
      */

     private function beforeSavePartner($bean, $events, $arguments): void
        {
           if (!empty($bean->parent_id)) {
              /** @var Account $masterAccount */
              $masterAccount = BeanFactory::getBean('Accounts', $bean->parent_id, IGNORE_TEAMS);
              if (!empty($masterAccount->dealerID) {
                  $bean->masterid_c = $masterAccount->dealerid_c;
              } else {
                  $bean->masterid_c = '';
                  $bean->parent_id = null;
              }
           } else {
              $bean->masterid_c = '';
           }
}

 

If I ran it in 'Custom' mode I would get a file created in 'custom/modules/Accounts/customAccounts.php' that looks like this:

Custom Accounts Class

 

In this scenario I would have to alter my .gitignore to keep these files from being committed to production code.  Then I would have to alter my code like in the following example. You should notice that in the DocBlock I had to define $bean as being either an Account or a customAccount.  Otherwise, PHP would complain.  Since $masterAccount is all mine, I can just define it as  customAccount.

     /**
      * @param Account|customAccount $bean
      * @param string $events
      * @param array $arguments
      */

     private function beforeSavePartner($bean, $events, $arguments): void
        {
           if (!empty($bean->parent_id)) {
              /** @var customAccount $masterAccount */
              $masterAccount = BeanFactory::getBean('Accounts', $bean->parent_id, IGNORE_TEAMS);
              if (!empty($masterAccount->dealerID) {
                  $bean->masterid_c = $masterAccount->dealerid_c;
              } else {
                  $bean->masterid_c = '';
                  $bean->parent_id = null;
              }
           } else {
              $bean->masterid_c = '';
           }
}


PHPStorm Live Templates

I know I said at the top that all of this would be compatible with all editors but I just wanted to share this one PHPStorm thing as I use it in relation to this all the time. I created a 'Live Template' for BeanFactory calls so I would not have to type the BeanFactory call AND the inline tag every time. I just type BF<tab> and then fill in the form like this:

Live Template

 

This can be created in your PHPStorm like this. Just open preferences and search for 'live' and you will see Live Templates, select that and then add a template to the PHP section.

Live Template Setup 1

 

Then you have to set up the variables for the template. This is done by clicking on the 'Edit variables' button and filling out the form there.

Live Templates Setup 2

 

Is any of this necessary to customize Sugar? No, not really. But, it has made coding for Sugar much easier for me. I hope it can do the same for you. 

Hello Sugar Developers!

 

It's that time again - we have completed a new quarterly release. Sugar 9.3 (Winter '20) is a cloud release but the updates in this version will be available to on-premise customers in the upcoming Sugar 9.0 release this Spring.

 

In December, we conducted a webinar highlighting the changes of Sugar 9.3. If you missed it, you can watch the recording or browse through the slides.

 

Here's a quick list of some of the features in this Winter '20 release:

  • A new REST endpoint has been added to return related activity records for a specified record
  • New fields were added to the Cases module
  • No-touch renewal pipeline automation has been added to SugarSell
  • There's also a new Renewals console
  • New Active Subscriptions Dashlet
  • We've added First Response SLA Reporting
  • 2 new SugarBean functions for business center calculations

 

Check out the below resources that have the rest of the details.

On October 28, 2019, we conducted a webinar on the topic of How to deploy code to SugarCloud using Module Loader. In that presentation, we demonstrated a Module Loadable Package that would do 3 things:

 

  1. Modify the UI via custom.less
  2. Add a post-install script
  3. Add global javascript

 

I'd like to take some time to look a bit deeper into making and using Module Loadable Packages in Sugar.

 

Use Case

So, let's start with a new example. This package will add a custom dashlet to the record view of Accounts and Leads modules. The dashlet will pull data from an external API based on a field value from the current module. That's it.

 

Could it do more? Sure. It is my opinion, however, that each package should be singular in action. In other words, each package that *I* create will do one thing. If I also want to add a new script library that changes all telephone numbers into clickable links, for example, I would create a separate package with those files and instructions - even though it might be faster for me at this moment to just throw that in with the package I am working on.

 

Creating and using small, singularly-focused packages allows me to keep things simple. So, if something fails, I have a reasonable amount of code and files to look through that are all working toward the same end. Sure, arguments can be made for creating one large package that can be installed/uninstalled once. In fact, that’s the way we approached all of Professor M. It is a single package that loads absolutely every customization that we wanted for the project. I am of the opinion that smaller is better (think microservices). There is a plan in the works to revamp Professor M a bit. At that time, we will likely split things up into more manageable pieces.

 

Planning

OK, back to this project. The first thing I want to do is determine the scope and functionality of this dashlet. I know it should display on the Accounts and Leads modules. I know that it should only show for the record view. Let's set it for the most current major version of Sugar (as of now that is 9.x). But what will the dashlet actually do?

 

Since my Sugar instance is using Professor M, I think it makes sense to display to the user a list of statistics of OTHER schools in the same area as the record which they are viewing. That way, a user can show a potential student what other schools around them are charging for tuition (or what the job placement rate is, etc). For this, I needed to find a data source. I created a free account with api.data.gov so that I could get an API key to call the REST endpoint at api.data.gov/ed/collegescorecard/v1/schools. The last thing I want to do is ensure that what I put into the dashlet is pleasing to the eye. For that, I will add some custom CSS.

 

The Package

Now that I have all of the high-level details ironed out, it is time to start creating the module loadable package. To start, let's create the manifest. From the details in the last paragraph, my manifest variable will look like this:

 

$manifest = array(
        'acceptable_sugar_flavors' => array('PRO','ENT','ULT'),
        'acceptable_sugar_versions' => array(
            'regex_matches' => array('9.*.*'),
        ),
        'author' => 'Michael Shaheen',
        'description' => 'Adds College Stats dashlet that pulls data from api.data.gov',
        'is_uninstallable' => true,
        'name' => 'College Stats dashlet',
        'published_date' => date("Y-m-d H:i:s"),
        'type' => 'module',
        'version' => '1.0',
    );

 

The next thing I like to do is create the file structure of the package that closely mirrors where the files will live in my Sugar instance. For this dashlet, I will place files at /custom/clients/base/views/college-stats (my new directory for this dashlet). Inside of that directory, we need 3 files:

 

  • college-stats.js: the javascript controller 
  • college-stats.php: a metadata php file that essentially defines the dashlet and makes it available for use in my Sugar instance 
  • college-stats.hbs: a handlebars template to define the layout and display the data

 

The Code - "Hello World"

What specifically goes into those files? Let's start with the metadata file. Within /custom/clients/base/views/college-stats/college-stats.php we will add our dashlet definition like so:

 

$viewdefs['base']['view']['college-stats'] = array(
     'dashlets' => array(
          array(
               //Display label for this dashlet
               'label' => 'College Stats',

               //Description label for this Dashlet
               'description' => 'Lists tuition statistics for colleges in the same state as the current record. Will show all states if none is specified',

               'config' => array(),
               'preview' => array(),

               //Filter array decides where this dashlet is allowed to appear
               'filter' => array(
                    //Modules where this dashlet can appear
                    'module' => array(
                         'Contacts',
                         'Accounts',
                         'Leads',
                    ),
                    
                    //Views where this dashlet can appear
                    'view' => array(
                         'record',
                    )
               )
          ),
     ),
);

 

This assignment statement is saying that we would like to add a dashlet that can be displayed in the Contacts, Accounts, or Leads modules and only for the record view of those modules.

 

Now we can start populating the javascript controller. To /custom/clients/base/views/college-stats/college-stats.js, we will add the following:

 

({
     plugins: ['Dashlet'],

     _retrieveData: function() {
          this.schools = "hello";
     },
     
     initialize: function(options) {
          this.schools = [];
          // call the parent's (View's) initialize function
          // passing options as an array
          this._super('initialize', [options]);
          this._retrieveData();
     },
})

 

In this very basic code, we are simply declaring that we are using the dashlet plugin, initializing a variable that will hold our data called schools, calling the View's initialize function, and calling a function that we will use to grab our data from the external source. For now, that function _retrieveData is setting the value of schools = "hello". This is a functioning controller for our dashlet. All that's left is to write the handlebars template to display the data that is being retrieved. So, inside of /custom/clients/base/views/college-stats/college-stats.hbs, we will add HTML and a placeholder for our data like so:

 

<div class="control-group dashlet-options">
    <div class="controls controls-two btn-group-fit">
        <div class="row-fluid">
            <div class="">
                <p>This is the internal header of our dashlet</p>
            </div>
        </div>       
    </div>
</div>
<div class="ext_schools">
{{schools}}
</div>

 

I put in some HTML that I copied from other dashlets so that the look would be somewhat consistent. The important piece in this template is the {{schools}} line. This is going to look at the data in the controller and display whatever is contained in the variable called schools.

 

These are the files that will make our dashlet accessible in the Contacts, Accounts, and Leads modules' record views. Once installed and added to a dashboard, we will see a dashlet with a header and the word "Hello". Not very useful in the long run but it is a first step. Unfortunately, this isn't enough to get our dashlet into our Sugar instance.

 

I approached this project from an entirely cloud-based position. I had no local environment for testing - just a cloud sandbox instance. This is NOT a practice that I recommend. At all. Ugh it took a very long time to work on because to test each change I made, I had to go into Admin, then to module loader where I would uninstall the old package, upload and install the new package, then navigate to the Accounts module, go into an account record and add the dashboard. Just explaining the process is tedious and I left out all of the clicks and the waiting for the processes to complete! So, if you can, do your development work in a local build (see the Developer Builds space in the Developer Community) where you can work directly in the files that you are manipulating. Then, when happy, create the package and use Module Loader to install it into your cloud instance. Since I did not do that, this project took a lot longer than I had hoped.

 

Nevertheless, I persisted. In order for me to get my dashlet into my cloud instance, I had to update the manifest to describe what to do with each of my files. It should look something like this:

 

    $installdefs = array(
        'id' => 'college-stats-dashlet',
        'copy' => array(
            0 => array(
                'from' => '<basepath>/Files/custom/clients/base/views/college-stats/college-stats.js',
                'to' => 'custom/clients/base/views/college-stats/college-stats.js',
            ),
            1 => array(
                'from' => '<basepath>/Files/custom/clients/base/views/college-stats/college-stats.hbs',
                'to' => 'custom/clients/base/views/college-stats/college-stats.hbs',
            ),
            2 => array(
                'from' => '<basepath>/Files/custom/clients/base/views/college-stats/college-stats.php',
                'to' => 'custom/clients/base/views/college-stats/college-stats.php',
            ),
        ),
    );

 

NOW all that's left to do is zip up the package and upload/install it into my cloud instance. Remember, your archived zip cannot contain any extraneous files. So, on a mac, be sure to zip it with the CLI command:

 

zip -r --filesync ../college-stats.zip * -x "*.DS_Store" -x "*.git*" -x "__MAC*"

 

This will zip up the current directory and all of its children excluding any files that match *.DS_Store, *.git, or __MAC*. It will place that archive in the directory above the current one and call it "college-stats.zip".

 

After uploading and installing the package, I can see it is now accessible as a dashlet on the Leads module:

 

I can add it to my dashboard and I can see my "hello" statement in the dashlet.

 

Hello world dashlet example

 

The Code - With Data

The next step is to use actual data. I used Postman to do some test calls to the REST API. By doing so I was able to fiddle around until I found the proper query for my purposes. And, for now, I am going to copy/paste some results from that call into my controller so that I can have some data to work with. To do this, I will change the _retrieveData function to return my copied data. Like so:

 

_retrieveData: function() {
    this.schools = [
        {
            "latest.cost.tuition.out_of_state": 16836,
            "school.school_url": "www.dli.pa.gov",
            "latest.cost.tuition.in_state": 16836,
            "school.name": "Commonwealth Technical Institute",
            "school.state": "PA",
            "id": 212975,
            "latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings": 14600,
            "school.city": "Johnstown",
            "latest.aid.median_debt.completers.overall": null,
            "latest.repayment.1_yr_repayment.completers": null
        },
        {
            "school.name": "Susquehanna County Career and Technology Center",
            "school.state": "PA",
            "id": 441672,
            "school.school_url": "www.scctc-school.org",
            "school.city": "Springville",
            "latest.aid.median_debt.completers.overall": null,
            "latest.repayment.1_yr_repayment.completers": null,
            "latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings": null,
            "latest.cost.tuition.out_of_state": null,
            "latest.cost.tuition.in_state": null
        },
        {
            "latest.repayment.1_yr_repayment.completers": 30,
            "latest.cost.tuition.out_of_state": 21126,
            "school.school_url": "www.brynathyn.edu",
            "latest.cost.tuition.in_state": 21126,
            "school.name": "Bryn Athyn College of the New Church",
            "school.state": "PA",
            "id": 210492,
            "school.city": "Bryn Athyn",
            "latest.aid.median_debt.completers.overall": 26571.5,
            "latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings": null
        },
        {
            "latest.repayment.1_yr_repayment.completers": 55,
            "latest.cost.tuition.out_of_state": 15662,
            "school.school_url": "www.goPMI.org",
            "latest.cost.tuition.in_state": 15662,
            "school.name": "Precision Manufacturing Institute",
            "school.state": "PA",
            "id": 446455,
            "school.city": "Meadville",
            "latest.aid.median_debt.completers.overall": null,
            "latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings": null
        },
        {
            "latest.repayment.1_yr_repayment.completers": 528,
            "school.school_url": "www.empire.edu",
            "school.name": "Empire Beauty School-North Hills",
            "school.state": "PA",
            "id": 450605,
            "latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings": 19900,
            "school.city": "Pittsburgh",
            "latest.aid.median_debt.completers.overall": 10666.5,
            "latest.cost.tuition.out_of_state": null,
            "latest.cost.tuition.in_state": null
        },
        {
            "latest.repayment.1_yr_repayment.completers": 34,
            "school.school_url": "www.cde.edu",
            "school.name": "CDE Career Institute",
            "school.state": "PA",
            "id": 451495,
            "school.city": "Tannersville",
            "latest.aid.median_debt.completers.overall": 6480.0,
            "latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings": null,
            "latest.cost.tuition.out_of_state": null,
            "latest.cost.tuition.in_state": null
        },
        {
            "latest.repayment.1_yr_repayment.completers": 2593,
            "latest.cost.tuition.out_of_state": 11005,
            "school.school_url": "www.mccann.edu",
            "latest.cost.tuition.in_state": 11005,
            "school.name": "McCann School of Business & Technology",
            "school.state": "PA",
            "id": 438212,
            "latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings": 27100,
            "school.city": "Pottsville",
            "latest.aid.median_debt.completers.overall": 24549.5
        },
        {
            "latest.repayment.1_yr_repayment.completers": 125,
            "school.school_url": "www.lcctc.edu",
            "school.name": "Lebanon County Area Vocational Technical School",
            "school.state": "PA",
            "id": 418542,
            "latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings": 38400,
            "school.city": "Lebanon",
            "latest.aid.median_debt.completers.overall": 15485.0,
            "latest.cost.tuition.out_of_state": null,
            "latest.cost.tuition.in_state": null
        },
        {
            "school.name": "Geisinger Commonwealth School of Medicine",
            "school.state": "PA",
            "id": 456542,
            "school.school_url": "https://www.geisinger.edu/education",
            "school.city": "Scranton",
            "latest.aid.median_debt.completers.overall": null,
            "latest.repayment.1_yr_repayment.completers": null,
            "latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings": null,
            "latest.cost.tuition.out_of_state": null,
            "latest.cost.tuition.in_state": null
        },
        {
            "latest.repayment.1_yr_repayment.completers": 4120,
            "school.school_url": "www.strayer.edu/pennsylvania/warrendale",
            "latest.cost.tuition.in_state": 13857,
            "school.name": "Strayer University-Warrendale Campus",
            "school.state": "PA",
            "id": 44378405,
            "latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings": 53500,
            "school.city": "Warrendale",
            "latest.aid.median_debt.completers.overall": 34239.5,
            "latest.cost.tuition.out_of_state": null
        }
    ];
}

 

Now that I know the data structure, I can write the Handlebars template to display this data properly. Take a look at the data keys in that sample set above. Each field uses dot notation. So, if in the Handlebars template I try to place one of those fields into the HTML by simply using {{ school.school_url }}, nothing will display. There is no field called schools[0].school.school_url. There is, however, a field called schools[0]["school.school_url"]. To represent that in Handlebars, we simply add the square brackets inside of the double-curly brackets. Like this: {{ [ school.school_url ] }}. With that in mind, I set up my template like this:

 

<div class="control-group dashlet-options">
    <div class="controls controls-two btn-group-fit">
        <div class="row-fluid">
            <div class="">
                <p>This is the internal header of our dashlet</p>
            </div>
        </div>       
    </div>
</div>
<div class="ext_schools">
{{#each schools}}
<div class="row-fluid ext_school">

{{#if [id]}}
<div class="dta_block ext_school_id"><span class="dta_lbl">ID:</span><span class="dta_val">{{[id]}}</span></div>
{{/if}}

{{#if [school.name]}}
<div class="dta_block ext_school_name"><span class="dta_lbl">Name:</span><span class="dta_val">{{[school.name]}}</span></div>
{{/if}}

{{#if [school.city]}}
<div class="dta_block ext_school_city"><span class="dta_lbl">City:</span><span class="dta_val">{{[school.city]}}</span></div>
{{/if}}

{{#if [school.state]}}
<div class="dta_block ext_school_state"><span class="dta_lbl">State:</span><span class="dta_val">{{[school.state]}}</span></div>
{{/if}}

{{#if [school.school_url]}}
<div class="dta_block ext_school_url"><span class="dta_lbl">Website:</span><span class="dta_val"><a href="{{[school.school_url]}}" target="_blank">{{[school.school_url]}}</a></span></div>
{{/if}}

{{#if [latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings]}}
<div class="dta_block ext_school_earnings10"><span class="dta_lbl">Mean Earnings 10yr After Entry:</span><span class="dta_val">{{[latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings]}}</span></div>
{{/if}}

{{#if [latest.repayment.1_yr_repayment.completers]}}
<div class="dta_block ext_school_repay1yr"><span class="dta_lbl">Loan Repayment After 1yr:</span><span class="dta_val">{{[latest.repayment.1_yr_repayment.completers]}}</span></div>
{{/if}}

{{#if [latest.cost.tuition.in_state]}}
<div class="dta_block ext_school_tuition_instate"><span class="dta_lbl">In-state Tuition:</span><span class="dta_val">{{[latest.cost.tuition.in_state]}}</span></div>
{{/if}}

{{#if [latest.cost.tuition.out_of_state]}}
<div class="dta_block ext_school_tuition_outstate"><span class="dta_lbl">Out-of-state Tuition:</span><span class="dta_val">{{[latest.cost.tuition.out_of_state]}}</span></div>
{{/if}}

</div>
{{/each}}
</div>

 

There's some simple HTML with classes on each element. The Handlebars-specific code consists of a loop for the schools variable {{#each schools}} and a bunch of conditionals checking to see if the variable exists before displaying it {{#if [school.school_url]}}.

 

With that, we should check on how these changes affect our dashlet. So, I will uninstall the current package and upload/install the new package. Then we can view the changes.

 

 

The Code - Styling

I think I'd like my next step to be styling. It will make it easier to see my changes going forward if the data isn't all jumbled in the dashlet. In previous examples, we have added a custom.less file with our styling changes for the site's UI. This is a very viable method. But, what if another package also adds a custom.less file? That file (if installed after ours) will replace the one from this package. That's not ideal. In our building blocks git repo, there is a plugin called CssLoader. It will take CSS files and add them to the loaded CSS for the site. There is no overwriting involved. The tradeoff is that we must use straight CSS instead of LessJS. If you already have Less written I like converters like this one https://www.webtoolkitonline.com/less-to-css.html. The CssLoader plugin happens to now be already installed in the core application. So, all we need to do is reference it in our Javascript controller by updating our plugins array to include CssLoader and adding the path to our CSS file(s):

 

    plugins: ['Dashlet','CssLoader'],
    css: ["/custom/include/css/college-stats.css"],

 

Now, the CSS file does not exist yet. I will create that file within my package at /Files/custom/include/css/college-stats.css and then add an entry to the copy array of my manifest that defines where to put the CSS file in my Sugar instance. That value will be the same as the path I put into the controller /custom/include/css/college-stats.css.

 

Finally, here is the content of our CSS file:

 

.ext_school { padding: 10px; width: 90% !important; margin: 16px auto; border-top: solid 1px #e8e8e8; border-bottom: solid 1px #cacaca; background: #f9f9f9; font-size: 14px; }
.ext_school .dta_block .dta_lbl { display:inline-block; font-weight: 700; color: #003865; margin-right: 4px; }
.ext_school .dta_block .dta_val { display: inline-block; }
.ext_school a { color: #ff8200; text-decoration: none; }
.ext_school a:hover { color: #009cde; text-decoration: underline; }

 

When we look at our new changes in the application, we will now see a styled dashlet:

 

 

Pretty sweet!

 

The Code - External Data Request

Let's keep going by pulling in live data from the external REST API. Because of cross-site-reference rules, we cannot call the external API directly from our Javascript controller. The approach we'll take, then, will be to make our own custom API endpoint in our instance of Sugar to act as a proxy to the api.data.gov endpoint.

 

This means adding a file for our endpoint at /Files/custom/clients/base/api/ExternalCollegeStatsAPI.php. Remember to add a copy directive to the manifest for this new file. For the content of the file, we will need to register the new endpoint and add the functions to return the college stats data.

 

The first thing we will add to our file is the endpoint registration code:

 

<?php

class ExternalCollegeStatsAPI extends SugarApi
{
public function registerApiRest()
    {
        return array(
            //GET & POST
            'ExternalCollegeStats' => array(
                //request type
                'reqType' => array('GET'),

                //set authentication
                'noLoginRequired' => false,

                //endpoint path will equal External/CollegeStats/{variable}
                'path' => array('External', 'CollegeStats', '?'),

                //endpoint variables. to access the last value in the url, we will use "state"
                'pathVars' => array('', '', 'state'),

                //method to call
                'method' => 'CallExternal',
            ),
        );
    }

}

 

What does all of that code do? We start by creating a new class for the endpoint that extends the SugarApi class. Then in the registerApiRest function, we must define a few values. We want this to be just a GET request so we can set 'reqType' => array('GET'). I'd like to be able to call the endpoint by going to <base_url>/rest/v11_6/External/CollegeStats/PA where "PA" is the state for which I'd like to see results. To define that path, we need to set 'path' => array('External', 'CollegeStats', '?'). With the path defined, we need a way of getting to the state variable at the end of the URL path. If we set 'pathVars' => array('', '', 'state'), we can access the value through a variable that we are calling state. Lastly, in our registration function, we need to designate what method will be called to return the data. So we set 'method' => 'CallExternal', and then add a function to this file called CallExternal($api, $args).

 

Within that new CallExternal function, we can add the code and logic to return the data. When I first built this package, I did an intermediary step before calling the external API. I moved the dummy data from the Javascript controller into the new endpoint. I simply told the endpoint to return that array. This was really to test that my API endpoint was registered properly and that I could get data from it. For this article, however, I'm jumping right into the actual call.

 

Firstly, I added the API key and the URL of the external endpoint:

 

    $api_key = "XXXXXXXyour-real-key-goes-hereXXXXXXX";
    $base_url = "https://api.data.gov/ed/collegescorecard/v1/schools";

remember to update the $api_key value in your own package to use the api key from registering with api.data.gov

 

Now let's add an array of the fields to send with the call:

 

$data = array(
'api_key' => $api_key,
'per_page' => 50,
'fields' => 'school.name,school.school_url,school.city,school.state,id,latest.aid.median_debt.completers.overall,latest.repayment.1_yr_repayment.completers,latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings,latest.cost.tuition.out_of_state,latest.cost.tuition.in_state'
);

 

You can see from here that I am sending my API key, how many items I wish to see per page, and the fields from the full dataset that I would like to retrieve. Note for this endpoint, the field's value must be a comma-delimited string with no whitespace.

 

I'd like to have the data retrieved reflect the current Sugar record. So, I'll try to grab a "state" field from the current record and send it to this call. We'll get to the front-end logic around this in a minute. For now, we just want to ensure that our custom endpoint can handle that parameter.

 

if ( isset($args['state']) && !empty($args['state']) && $args['state'] != "all" ) {
$data['school.state'] = $args['state'];
}

 

I added a condition for state != "all" because if all states are requested, the endpoint just needs to omit that value.

 

I feel like we have our setup complete. Now we need to make the actual external call. Some examples in the Developer Community and the Documentation use file_get_contents to make the call. Unfortunately, this is a blacklisted function for Module Loadable Packages in the cloud. Package Scanner will reject installing the package when it finds this function used. So, I used cURL. The code below should be familiar to anyone who has used cURL before.

 

$ch = curl_init();
$query = http_build_query($data);
$url = $base_url . '?' . $query;
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$result = curl_exec($ch);

if ( $result !== false ) {
        $response->success = true;
        $response->body = $result;
} else {
        $response->success = false;
        $response->body = curl_error($ch);
        $response->error = curl_errno($ch);
}
curl_close($ch);
return $response;

 

That's all the code that we need for our custom endpoint that will proxy our call to the external API. Now we have to write the Javascript code in our controller to consume this endpoint. Let's replace the dummy data in the _retrieveData method with the following:

 

var cntrlr = this;

App.api.call('GET', App.api.buildURL('External/CollegeStats/' + "PA"), null, {
     success: function (data) {
          if (data.success == true) {
               var results = JSON.parse(data.body).results;
               if (results.length > 0) {
                    cntrlr.schools = results;

                    // the data needs to be added and rendered
                    _.extend(cntrlr, cntrlr.schools);
                    cntrlr.render();
               }
          }
     },

     error: function (e) {
          throw e;
     }
});

 

This is a pretty basic Sugar API call. I specified the URL path and added "PA" as the state for now. In the success callback, I parse the data and grab the node from it that I need - results - and assign it to our schools variable. Remember scope. I had to set a variable called cntrlr = this so that I could still reference the current controller from within the API callback. One other thing to note here is that since we are inside of the callback and render has (likely) already happened, we will need to re-call the controller's render function.

 

Install this new version of the dashlet package and we should see a similar outcome as before. Our data is all from the state of Pennsylvania for now because we hard-coded that in. How do we make this dashlet grab and use the state associated with the current module record that we are viewing? With just a bit more Javascript:

 

var currentState;

var currentModule = this.module;

// ensure we are using the correct STATE field for the current module (e.g. Accounts doesn't have a primary_address_state field)

switch(currentModule) {

     case "Accounts":
          currentState = this.model.get('billing_address_state');
          break;
     default:
          currentState = this.model.get('primary_address_state');
}

var stateAbbrev = cntrlr._getStateAbbreviation(currentState);

 

First, we grab the current module with this.module. Remember, each module in Sugar has different fields. I know that if we are viewing this dashlet alongside an Account record, we should use the billing_address_state field and our other modules both use primary_address_state. So, my switch statement reflects that. Unfortunately, the data from the module comes across as a full state name (like "Pennsylvania") and the external endpoint requires a state abbreviation. So, I made a quick helper function that takes a state name and returns the proper abbreviation:

 

_getStateAbbreviation: function(stateName) {
     var states = [{"name": "Alabama","abbreviation": "AL"},{"name": "Alaska","abbreviation": "AK"},{"name": "American Samoa","abbreviation": "AS"},{"name": "Arizona","abbreviation": "AZ"},{"name": "Arkansas","abbreviation": "AR"},{"name": "California","abbreviation": "CA"},{"name": "Colorado","abbreviation": "CO"},{"name": "Connecticut","abbreviation": "CT"},{"name": "Delaware","abbreviation": "DE"},{"name": "District Of Columbia","abbreviation": "DC"},{"name": "Federated States Of Micronesia","abbreviation": "FM"},{"name": "Florida","abbreviation": "FL"},{"name": "Georgia","abbreviation": "GA"},{"name": "Guam","abbreviation": "GU"},{"name": "Hawaii","abbreviation": "HI"},{"name": "Idaho","abbreviation": "ID"},{"name": "Illinois","abbreviation": "IL"},{"name": "Indiana","abbreviation": "IN"},{"name": "Iowa","abbreviation": "IA"},{"name": "Kansas","abbreviation": "KS"},{"name": "Kentucky","abbreviation": "KY"},{"name": "Louisiana","abbreviation": "LA"},{"name": "Maine","abbreviation": "ME"},{"name": "Marshall Islands","abbreviation": "MH"},{"name": "Maryland","abbreviation": "MD"},{"name": "Massachusetts","abbreviation": "MA"},{"name": "Michigan","abbreviation": "MI"},{"name": "Minnesota","abbreviation": "MN"},{"name": "Mississippi","abbreviation": "MS"},{"name": "Missouri","abbreviation": "MO"},{"name": "Montana","abbreviation": "MT"},{"name": "Nebraska","abbreviation": "NE"},{"name": "Nevada","abbreviation": "NV"},{"name": "New Hampshire","abbreviation": "NH"},{"name": "New Jersey","abbreviation": "NJ"},{"name": "New Mexico","abbreviation": "NM"},{"name": "New York","abbreviation": "NY"},{"name": "North Carolina","abbreviation": "NC"},{"name": "North Dakota","abbreviation": "ND"},{"name": "Northern Mariana Islands","abbreviation": "MP"},{"name": "Ohio","abbreviation": "OH"},{"name": "Oklahoma","abbreviation": "OK"},{"name": "Oregon","abbreviation": "OR"},{"name": "Palau","abbreviation": "PW"},{"name": "Pennsylvania","abbreviation": "PA"},{"name": "Puerto Rico","abbreviation": "PR"},{"name": "Rhode Island","abbreviation": "RI"},{"name": "South Carolina","abbreviation": "SC"},{"name": "South Dakota","abbreviation": "SD"},{"name": "Tennessee","abbreviation": "TN"},{"name": "Texas","abbreviation": "TX"},{"name": "Utah","abbreviation": "UT"},{"name": "Vermont","abbreviation": "VT"},{"name": "Virgin Islands","abbreviation": "VI"},{"name": "Virginia","abbreviation": "VA"},{"name": "Washington","abbreviation": "WA"},{"name": "West Virginia","abbreviation": "WV"},{"name": "Wisconsin","abbreviation": "WI"},{"name": "Wyoming","abbreviation": "WY"}];
     if (!_.isUndefined(stateName) && stateName.length != 2) {
          var obj = _.find(states, function (obj) { return obj.name.toLowerCase() === stateName.toLowerCase(); });
          if (!_.isUndefined(obj) && !_.isUndefined(obj.abbreviation)) {
               return obj.abbreviation;
          }
     }
     return "all";
},

 

If the function cannot make a match for the stateName parameter, it will return the text "all". You hopefully recall that in our endpoint definition, I added a condition for "all". This was necessary because our path requires a last value as a parameter.

 

In the final controller, you will see a few other helper functions that I added just to format the data to be more readable. I also added a help file for the API endpoint and language definitions.

 

I hope this is helpful to some of you out there who are adding dashlets or modules that need to bring related data in from an external source. If you see room for improvement, please take the attached package and run with your changes. Then, share them with us here. Or, as always, you can reach us at developers@sugarcrm.com.

Back in September of this year (2019), we held the Developer's Webinar for the 9.2 release of Sugar. During that presentation, I touched on how the Portal can be customized. Among the things illustrated in the webinar was adding a module to the Portal. This is apparently something that you all wanted to know more about. So, let's use this space to focus on just that.

 

There isn't really much to it - just a few things to remember.

 

First, you need to create a Module Loadable Package. The package can do pretty much anything you'd like it to do. In the example from the webinar, we had the module display announcements.

 

Once you have the package ready, add the following vardef to make the new module visible in the portal:

 

$dictionary['sugar_NAME_OF_YOUR_MODULE']['portal_visibility'] = [
    'class' => 'Visible',
];

 

If your module will have record or list views, you will need a portal directory under the clients directory. Within that portal folder, be sure to add the appropriate directories and files like so:

 

<basepath>/sugar_PortalAnnouncements/clients/portal/views/list/list.php

 

Now upload and install the package in module loader.

 

To allow this module to be seen/used on the Customer Portal, we must update the permissions for the role that is assigned to our Portal users. In Admin > Role Management find and select the Customer Self Service Portal role.

 

 

The next screen will show all of the modules and their permission levels for this role.

 

 

Find your new module in the list, and set the permissions that it requires. If you are unsure, base the permissions off of a pre-existing module on the Portal like Bugs. These are the basic settings to make the module appear on the Portal:

  • Access - Enabled
  • Access Type - Normal
  • List - All
  • View - All

For more details on role permissions, refer to the documentation for Setting Module-Level Permissions.

 

Finally, go to Admin > Sugar Portal > Configure Portal. Your new module will be listed in the Hidden column. To see it in the Portal, simply move it into the Displayed Modules list. And, that's it! Your new custom module has been added to the Customer Portal.

It's that time again! We have held another quarterly webinar where we discussed all the big changes that Sugar Developers need to know about in our upcoming Winter '20 releases.

 

In addition to details about the latest release, we also looked at topics that have been requested frequently since our last webinar.

 

What we covered:

Some of the big changes that are likely to impact you including the following:

  • Sugar Sell Renewals Console
  • Enhanced Sugar Serve SLA reporting
  • New Change Timer module

 

We also spent some time on:

  • Sugar Market integrations


In case you missed it:

If you missed the live sessions, don't worry. You can watch the recorded session here. The slide deck is also available for download or viewing here.

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.

In case you missed it, here's the recording!

Recording: Webinar: How to deploy code to SugarCloud using Module Loader 

The Module Loader is one of the most useful tools available to a SugarCloud developer when customizing Sugar. It is the safest (and only) method for manipulating files within the SugarCloud. Join us for a talk on how to use the Module Loader effectively for your customizations.

 

What we will be covering:

We will demonstrate and discuss the following topics:

  • Module Loadable Package definition and structure
  • The manifest
  • Package scanner
  • Versioning
  • Important rules to keep in mind
  • How to avoid common problems and when to reach out for help


Webinar Information:

Join us for the live webinar:

Monday, October 28th 7:00 - 8:00 AM PDT (10am EDT)

 

Can't make it? Don't worry! We will be posting the webinar recording to this Sugar Developers Community for those who are unable to attend the live sessions.

 

If you have further input or questions, you can reach out to us directly at developers@sugarcrm.com.