Custom Dashlets

Overview

Creating custom dashlets.

Sugar Dashlets

A Module View is the simplest Sugar Dashlet to create. This is a customizable ListView of a Sugar Dashlet. For this section we will use the MyAccountsDashlet as an example.

./modules/Accounts/Dashlets/MyAccountsDashlet/MyAccountsDashlet.php
<?php
require_once('include/Dashlets/DashletGeneric.php');
class MyAccountsDashlet extends DashletGeneric {
    function MyAccountsDashlet($id, $def = null) {
        global $current_user, $app_strings;
        require('modules/Accounts/Dashlets/MyAccountsDashlet/MyAccountsDashlet.data.php');
        parent::DashletGeneric($id, $def);
        if(empty($def['title'])) $this->title = translate('LBL_HOMEPAGE_TITLE', 'Accounts');
        $this->searchFields = $dashletData['MyAccountsDashlet']['searchFields'];
        $this->columns = $dashletData['MyAccountsDashlet']['columns'];
        $this->seedBean = new Account();
    }
    /**
     * Overrides the generic process to include custom logic for email addresses,
     * since they are no longer stored in  a list view friendly manner.
     * (A record may have an undetermined number of email addresses).
     *
     * @param array $lvsParams
     */
     function process($lvsParams = array())
     {
        if (isset($this->displayColumns) && array_search('email1', $this->displayColumns) !== false) {
            $lvsParams['custom_select'] = ', email_address as email1';
            $lvsParams['custom_from'] = ' LEFT JOIN email_addr_bean_rel eabr ON eabr.deleted = 0 AND bean_module = \'Accounts\'' . ' AND eabr.bean_id = accounts.id AND primary_address = 1' . ' LEFT JOIN email_addresses ea ON ea.deleted = 0 AND ea.id = eabr.email_address_id';
        }
        if (isset($this->displayColumns) && array_search('parent_name', $this->displayColumns) !== false) {
            $lvsParams['custom_select'] = empty($lvsParams['custom_select']) ? ', a1.name as parent_name ' : $lvsParams['custom_select'] . ', a1.name as parent_name ';            $lvsParams['custom_from'] = empty($lvsParams['custom_from']) ? ' LEFT JOIN accounts a1 on a1.id = accounts.parent_id' : $lvsParams['custom_from'] . ' LEFT JOIN accounts a1 on a1.id = accounts.parent_id';
        }
        parent::process($lvsParams);
    }
}
?>
All the metadata for this Sugar Dashlet is defined in the constructor. $searchFields are the search inputs that can be applied to the view. Defining these here will tell which input fields to generate corresponding filters when the user configures the Sugar Dashlet. $columns define the available columns to the user. These contain the visible columns and the columns the user can make visible. Initially, both columns and searchFields are defined in MyAccountsDashlet.data.php.

./modules/Accounts/Dashlets/MyAccountsDashlet/MyAccountsDashlet.data.php
<?php

if (!defined('sugarEntry') || !sugarEntry)
    die('Not A Valid Entry Point');

global $current_user;

$dashletData['MyAccountsDashlet']['searchFields'] = array(
    'date_entered' => array(
        'default' => ''
    ),
    'account_type' => array(
        'default' => ''
    ),
    'industry' => array(
        'default' => ''
    ),
    'billing_address_country' => array(
        'default' => ''
    ),
    'team_id' => array(
        'default' => '',
        'label' => 'LBL_TEAMS'
    ),
    'assigned_user_id' => array(
        'type' => 'assigned_user_name',
        'default' => $current_user->name,
        'label' => 'LBL_ASSIGNED_TO'
    )
);
$dashletData['MyAccountsDashlet']['columns']      = array(
    'name' => array(
        'width' => '40',
        'label' => 'LBL_LIST_ACCOUNT_NAME',
        'link' => true,
        'default' => true
    ),
    'website' => array(
        'width' => '8',
        'label' => 'LBL_WEBSITE',
        'default' => true
    ),
    'phone_office' => array(
        'width' => '15',
        'label' => 'LBL_LIST_PHONE',
        'default' => true
    ),
    'phone_fax' => array(
        'width' => '8',
        'label' => 'LBL_PHONE_FAX'
    ),
    'phone_alternate' => array(
        'width' => '8',
        'label' => 'LBL_OTHER_PHONE'
    ),
    'billing_address_city' => array(
        'width' => '8',
        'label' => 'LBL_BILLING_ADDRESS_CITY'
    ),
    'billing_address_street' => array(
        'width' => '8',
        'label' => 'LBL_BILLING_ADDRESS_STREET'
    ),
    'billing_address_state' => array(
        'width' => '8',
        'label' => 'LBL_BILLING_ADDRESS_STATE'
    ),
    'billing_address_postalcode' => array(
        'width' => '8',
        'label' => 'LBL_BILLING_ADDRESS_POSTALCODE'
    ),
    'billing_address_country' => array(
        'width' => '8',
        'label' => 'LBL_BILLING_ADDRESS_COUNTRY',
        'default' => true
    ),
    'shipping_address_city' => array(
        'width' => '8',
        'label' => 'LBL_SHIPPING_ADDRESS_CITY'
    ),
    'shipping_address_street' => array(
        'width' => '8',
        'label' => 'LBL_SHIPPING_ADDRESS_STREET'
    ),
    'shipping_address_state' => array(
        'width' => '8',
        'label' => 'LBL_SHIPPING_ADDRESS_STATE'
    ),
    'shipping_address_postalcode' => array(
        'width' => '8',
        'label' => 'LBL_SHIPPING_ADDRESS_POSTALCODE'
    ),
    'shipping_address_country' => array(
        'width' => '8',
        'label' => 'LBL_SHIPPING_ADDRESS_COUNTRY'
    ),
    'email1' => array(
        'width' => '8',
        'label' => 'LBL_EMAIL_ADDRESS_PRIMARY'
    ),
    'parent_name' => array(
        'width' => '15',
        'label' => 'LBL_MEMBER_OF',
        'sortable' => false
    ),
    'date_entered' => array(
        'width' => '15',
        'label' => 'LBL_DATE_ENTERED'
    ),
    'date_modified' => array(
        'width' => '15',
        'label' => 'LBL_DATE_MODIFIED'
    ),
    'created_by_name' => array(
        'width' => '8',
        'label' => 'LBL_CREATED'
    ),
    'assigned_user_name' => array(
        'width' => '8',
        'label' => 'LBL_LIST_ASSIGNED_USER'
    ),
    'team_name' => array(
        'width' => '15',
        'label' => 'LBL_LIST_TEAM'
    )
);

?>
Please note that modifications made in studio to columns and searchFields will be located in ./custom/modules/Accounts/metadata/dashletviewdefs.php. These settings will overrdie the layout for all dashlets under that module name.

This data file along with the MyAccountsDashlet.meta.php file will create a generic module view Sugar Dashlet.

./Accounts/Dashlets/MyAccountsDashlet/MyAccountsDashlet.meta.php
<?php
    global $app_strings;
    $dashletMeta['MyAccountsDashlet'] = array(
        'module' => 'Accounts',
        'title' => translate('LBL_HOMEPAGE_TITLE', 'Accounts'),
        'description' => 'A customizable view into Accounts',
        'category' => 'Module Views'
    );
?>

Custom Sugar Dashlets

Sugar Dashlets are more than generic module views. They can provide unlimited functionality and integration.
For this section we will use the JotPad Sugar Dashlet as an example. The JotPad is a simple note taking Sugar Dashlet. A user double clicks on the Sugar Dashlet and can enter any text in the Sugar Dashlet. When the user clicks outside of the textarea, the text is automatically saved via AJAX.
There are six files that define this Sugar Dashlet residing in the ./modules/Home/Dashlets/JotPadDashlet/ directory:
  1. JotPadDashlet.php – JotPad Class
  2. JotPadDashlet.meta.php – metadata about the Sugar Dashlet
  3. JotPadDashlet.tpl – Display Template
  4. JotPadDashletOptions.tpl – Configuration template
  5. JotPadDashletScript.tpl - Javascript
  6. JotPadDashlet.en_us.lang.php – English Language file

JotPadDashlet.php

The JotPadDashlet.php file handles the display of the dashlet.
./modules/Home/Dashlets/JotPadDashlet/JotPadDashlet.php:
<?php

if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');

require_once('include/Dashlets/Dashlet.php');

class JotPadDashlet extends Dashlet {
    var $savedText; // users's saved text
    var $height = '200'; // height of the pad

    /**
     * Constructor
     *
     * @global string current language
     * @param guid $id id for the current dashlet (assigned from Home module)
     * @param array $def options saved for this dashlet
     */
    function JotPadDashlet($id, $def) {
        $this->loadLanguage('JotPadDashlet'); // load the language strings here

        if(!empty($def['savedText']))  // load default text is none is defined
            $this->savedText = $def['savedText'];
        else
            $this->savedText = $this->dashletStrings['LBL_DEFAULT_TEXT'];

        if(!empty($def['height'])) // set a default height if none is set
            $this->height = $def['height'];

        parent::Dashlet($id); // call parent constructor

        $this->isConfigurable = true; // dashlet is configurable
        $this->hasScript = true;  // dashlet has javascript attached to it

        // if no custom title, use default
        if(empty($def['title'])) $this->title = $this->dashletStrings['LBL_TITLE'];
        else $this->title = $def['title'];
    }

    /**
     * Displays the dashlet
     *
     * @return string html to display dashlet
     */
    function display() {
        $ss = new Sugar_Smarty();
        $ss->assign('savedText', SugarCleaner::cleanHtml($this->savedText));
        $ss->assign('saving', $this->dashletStrings['LBL_SAVING']);
        $ss->assign('saved', $this->dashletStrings['LBL_SAVED']);
        $ss->assign('id', $this->id);
        $ss->assign('height', $this->height);

        $str = $ss->fetch('modules/Home/Dashlets/JotPadDashlet/JotPadDashlet.tpl');
        return parent::display($this->dashletStrings['LBL_DBLCLICK_HELP']) . $str . '
'; // return parent::display for title and such
    }

    /**
     * Displays the javascript for the dashlet
     *
     * @return string javascript to use with this dashlet
     */
    function displayScript() {
        $ss = new Sugar_Smarty();
        $ss->assign('saving', $this->dashletStrings['LBL_SAVING']);
        $ss->assign('saved', $this->dashletStrings['LBL_SAVED']);
        $ss->assign('id', $this->id);

        $str = $ss->fetch('modules/Home/Dashlets/JotPadDashlet/JotPadDashletScript.tpl');
        return $str; // return parent::display for title and such
    }

    /**
     * Displays the configuration form for the dashlet
     *
     * @return string html to display form
     */
    function displayOptions() {
        global $app_strings;

        $ss = new Sugar_Smarty();
        $ss->assign('titleLbl', $this->dashletStrings['LBL_CONFIGURE_TITLE']);
        $ss->assign('heightLbl', $this->dashletStrings['LBL_CONFIGURE_HEIGHT']);
        $ss->assign('saveLbl', $app_strings['LBL_SAVE_BUTTON_LABEL']);
        $ss->assign('clearLbl', $app_strings['LBL_CLEAR_BUTTON_LABEL']);
        $ss->assign('title', $this->title);
        $ss->assign('height', $this->height);
        $ss->assign('id', $this->id);

        return parent::displayOptions() . $ss->fetch('modules/Home/Dashlets/JotPadDashlet/JotPadDashletOptions.tpl');
    }

    /**
     * called to filter out $_REQUEST object when the user submits the configure dropdown
     *
     * @param array $req $_REQUEST
     * @return array filtered options to save
     */
    function saveOptions($req) {
        global $sugar_config, $timedate, $current_user, $theme;
        $options = array();
        $options['title'] = $_REQUEST['title'];
        if(is_numeric($_REQUEST['height'])) {
            if($_REQUEST['height'] > 0 && $_REQUEST['height'] <= 300) $options['height'] = $_REQUEST['height'];
            elseif($_REQUEST['height'] > 300) $options['height'] = '300';
            else $options['height'] = '100';
        }

        $options['savedText'] = $this->savedText;
        return $options;
    }

    /**
     * Used to save text on textarea blur. Accessed via Home/CallMethodDashlet.php
     * This is an example of how to to call a custom method via ajax
     */
    function saveText() {
        $json = getJSONobj();
    	if(isset($_REQUEST['savedText'])) {
            $optionsArray = $this->loadOptions();
            $optionsArray['savedText']=$json->decode(html_entity_decode($_REQUEST['savedText']));
            $optionsArray['savedText']=SugarCleaner::cleanHtml(nl2br($optionsArray['savedText']));
            $this->storeOptions($optionsArray);

        }
        else {
            $optionsArray['savedText'] = '';
        }
        echo 'result = ' . $json->encode(array('id' => $_REQUEST['id'],
                                       'savedText' => $optionsArray['savedText']));
    }
}


?>

JotPadDashletOptions.tpl

The JotPadDashletOptions.tpl file handles the display of the dashlet options.
./modules/Home/Dashlets/JotPadDashlet/JotPadDashletOptions.tpl
<div style='width: 500px'>
<form name='configure_{$id}' action="index.php" method="post" onSubmit='return SUGAR.dashlets.postForm("configure_{$id}", SUGAR.mySugar.uncoverPage);'>
<input type='hidden' name='id' value='{$id}'>
<input type='hidden' name='module' value='Home'>
<input type='hidden' name='action' value='ConfigureDashlet'>
<input type='hidden' name='to_pdf' value='true'>
<input type='hidden' name='configure' value='true'>
<table width="400" cellpadding="0" cellspacing="0" border="0" class="edit view" align="center">
<tr>
    <td valign='top' nowrap class='dataLabel'>{$titleLbl}
</td> 
   <td valign='top' class='dataField'>
        <input class="text" name="title" size='20' value='{$title}'>
    </td>
</tr>
<tr>
    <td valign='top' nowrap class='dataLabel'>{$heightLbl}</td>
    <td valign='top' class='dataField'>
        <input class="text" name="height" size='3' value='{$height}'>
    </td>
</tr>
<tr>
    <td align="right" colspan="2">
        <input type='submit' class='button' value='{$saveLbl}'>
    </td>
</tr>
</table>
</form>
</div>
The important thing to note here is the onSubmit. All configure forms should have this statement to uncover the page to remove the configuration dialog.
Note: It is important to separate your JavaScript into a separate JavaScript file. This is because Sugar Dashlets are dynamically added to a page through AJAX. The HTML included into JavaScript is not evaluated when dynamically included.
It is important that all JavaScript functions are included in this script file. Inline JavaScript (<a href onclick=’’ etc) will still function. If the Sugar Dashlet has JavaScript and a user dynamically adds it to the page, the Sugar Dashlet will not be accessible until after the user reloads the page.
Therefore it is beneficial to use as many generic methods in Dashlet.js as possible (Dashlets.callMethod() specifically!).

JotPadDashletScripts.tpl

The JotPadDashletScripts.tpl handles the generic javascript for the dashlet.
./modules/Home/Dashlets/JotPadDashlet/JotPadDashletScripts.tpl
{literal}<script>if(typeof JotPad == 'undefined') { // since the dashlet can be included multiple times a page, don't redefine these functions
    JotPad = function() {
        return {
            /**
             * Called when the textarea is blurred
             */
            blur: function(ta, id) {
                ajaxStatus.showStatus('{/literal}{$saving}{literal}'); // show that AJAX call is happening
                // what data to post to the dashlet
                var va=YAHOO.lang.JSON.stringify(ta.value);
                postData = 'to_pdf=1&module=Home&action=CallMethodDashlet&method=saveText&id=' + id + '&savedText=' + va;
                var cObj = YAHOO.util.Connect.asyncRequest('POST','index.php',{success: JotPad.saved, failure: JotPad.saved}, postData);
            },
            /**
             * Called when the textarea is double clicked on
             */
            edit: function(divObj, id) {
                ta = document.getElementById('jotpad_textarea_' + id);
                if(SUGAR.isIE) ta.value = divObj.innerHTML.replace(/<br>/gi, "\n");
                else ta.value = divObj.innerHTML.replace(/<br>/gi, '');
                ta.value = ta.value.replace(/&amp;/, "&");
                divObj.style.display = 'none';
                ta.style.display = '';
                ta.focus();
            },
            /**
             * handle the response of the saveText method
             */
            saved: function(data) {
                eval(data.responseText);
                ajaxStatus.showStatus('{/literal}{$saved}{literal}');
                if(typeof result != 'undefined') {
                    ta = document.getElementById('jotpad_textarea_' + result['id']);
                    theDiv = document.getElementById('jotpad_' + result['id']);
                    theDiv.innerHTML = result['savedText'];
                }
                ta.style.display = 'none';
                theDiv.style.display = '';
                window.setTimeout('ajaxStatus.hideStatus()', 2000);
            }
        };
    }();
}
</script>{/literal}

Refreshing the Sugar Dashlet Cache

To add a Sugar Dashlet to your SugarCRM installation, you can use the dashlets installdef in Module Loader to install your Sugar Dashlet Package to the ./custom/Home/Dashlets/ directory. If you are developing or need to copy the dashlet to a different modules directory, you will need to navigate to Admin > Repair > Rebuild Sugar Dashlets. This will rebuild the dashlet cache locatd in./cache/dashlets/dashlets.php that maps the dashlets.

Packaging Generic Sugar Dashlets

If you are packaging a generic dashlet that is not module specific, you can use the dashlets installdef. This will install the dashlet to ./custom/modules/Home/Dashlets/<dashlet>/.

manifest.php
<?php
   $manifest =array(
       'acceptable_sugar_flavors' => array(),
       'acceptable_sugar_versions' => array(),
       'author' => 'SugarCRM',
       'description' => 'Installs the dashlet using the dashlets installdef',       'icon' => '',
       'is_uninstallable' => true,
       'name' => 'Dashlet installer example',
       'published_date' => '2013-01-29 2013 13:49:58',
       'type' => 'module',
       'version' => '1.0',
   );
   $installdefs =array(
       'id' => 'package_1359467398',
       'dashlets' => array(
            0 => array(
                //The name to install the dashlet under
                'name' => 'MyDashlet',
                //This directory contains the dashlet files
                'from' => '<basepath>/MyDashlet',
            ),
        ),
    );
?>
If you are creating a module specific dashlet, you will have to move the dashlet to the directory using the copy installdef as shown below:
manifest.php
<?php
   $manifest =array(
       'acceptable_sugar_flavors' => array(),
       'acceptable_sugar_versions' => array(),
       'author' => 'SugarCRM',
       'description' => 'Installs the dashlet using the copy installdef',
       'icon' => '',
       'is_uninstallable' => true,
       'name' => 'Dashlet installer example',
       'published_date' => '2013-01-29 2013 13:49:58',
       'type' => 'module',
       'version' => '1.0',
   );
   $installdefs =array(
       'id' => 'package_1359467399',
       'copy' => array(
            0 => array(
                'from' => '<basepath>/MyDashlet/',
                'to' => 'custom/modules/<module>/Dashlets/MyDashlet',
            ),
        ),
    );
?>
Please note that if you are installing a dashlet using the copy installdef, you will need to navigate to Admin > Repair > Rebuild Sugar Dashlets. This will rebuild the dashlet cache.

More information on the manifest file can be found in the Introduction to the Manifest section.

Creating Custom Chart Dashlets

Creating a custom chart dashlet is very similar to creating the MyAccountsDashlet described above. The main difference is that you will need to override the display() method in your class to build the chart, using the SugarChartFactory class included with SugarCRM. Beginning in Sugar 6.2, we have switched the charts to be rendered through JavaScript. The SugarChartFactory returns a subclass of SugarChart. See below for an example of display() method as used in the Outcome by Month dashlet.

./modules/Charts/Dashlets/OutcomeByMonthDashlet/OutcomeByMonthDashlet.php
    /**
     * @see DashletGenericChart::display()
     */    public function display()
    {
        $currency_symbol = $GLOBALS['sugar_config']['default_currency_symbol'];
        if ($GLOBALS['current_user']->getPreference('currency')){
            $currency = new Currency();
            $currency->retrieve($GLOBALS['current_user']->getPreference('currency'));
            $currency_symbol = $currency->symbol;
        }
        require("modules/Charts/chartdefs.php");
        $chartDef = $chartDefs['outcome_by_month'];
        require_once('include/SugarCharts/SugarChartFactory.php');
        $sugarChart = SugarChartFactory::getInstance();
        $sugarChart->setProperties('',
            translate('LBL_OPP_SIZE', 'Charts') . ' ' . $currency_symbol . '1' .translate('LBL_OPP_THOUSANDS', 'Charts'), $chartDef['chartType']);
        $sugarChart->base_url = $chartDef['base_url'];
        $sugarChart->group_by = $chartDef['groupBy'];
        $sugarChart->url_params = array();
        $sugarChart->getData($this->constructQuery());
        $sugarChart->is_currency = true;
        $sugarChart->data_set = $sugarChart->sortData($sugarChart->data_set, 'm', false, 'sales_stage', true, true);
        $xmlFile = $sugarChart->getXMLFileName($this->id);
        $sugarChart->saveXMLFile($xmlFile, $sugarChart->generateXML());
           return $this->getTitle('<div align="center"></div>') . '<div align="center">' . $sugarChart->display($this->id, $xmlFile, '100%', '480', false) . '</div>'. $this->processAutoRefresh();
    }
    /**
     * @see DashletGenericChart::constructQuery()
     */
    protected function constructQuery()
    {
        $query = "SELECT sales_stage,". db_convert('opportunities.date_closed','date_format',array("'%Y-%m'"),array("'YYYY-MM'"))." as m, ". "sum(amount_usdollar/1000) as total, count(*) as opp_count FROM opportunities ";
        $this->getSeedBean()->add_team_security_where_clause($query);
        $query .= " WHERE opportunities.date_closed >= ".db_convert("'".$this->obm_date_start."'",'date') . " AND opportunities.date_closed <= ".db_convert("'".$this->obm_date_end."'",'date') . " AND opportunities.deleted=0";
        if (count($this->obm_ids) > 0)
            $query .= " AND opportunities.assigned_user_id IN ('" . implode("','",$this->obm_ids) . "')";
            $query .= " GROUP BY sales_stage,". db_convert('opportunities.date_closed','date_format',array("'%Y-%m'"),array("'YYYY-MM'")) . " ORDER BY m";
        return $query;
    }