New home dashboard written with Angular + APIv4.
Functionality is mostly unchanged.
The motivation for this is to support afforms embedded within dashboard dashlets.
CRM_Utils_JSON::output($dedupeRules);
}
- /**
- * Function used for CiviCRM dashboard operations.
- */
- public static function dashboard() {
- switch ($_REQUEST['op']) {
- case 'save_columns':
- CRM_Core_BAO_Dashboard::saveDashletChanges($_REQUEST['columns'] ?? NULL);
- break;
-
- case 'delete_dashlet':
- $dashletID = CRM_Utils_Type::escape($_REQUEST['dashlet_id'], 'Positive');
- CRM_Core_DAO_Dashboard::deleteRecord(['id' => $dashletID]);
- }
-
- CRM_Utils_System::civiExit();
- }
-
/**
* Retrieve signature based on email id.
*/
* Run dashboard.
*/
public function run() {
- // Add dashboard js and css
- $resources = CRM_Core_Resources::singleton();
- $resources->addScriptFile('civicrm', 'js/jquery/jquery.dashboard.js', 0, 'html-header', FALSE);
- $resources->addStyleFile('civicrm', 'css/dashboard.css');
- $this->assign('contactDashlets', CRM_Core_BAO_Dashboard::getContactDashletsForJS());
-
CRM_Utils_System::setTitle(ts('CiviCRM Home'));
$contactID = CRM_Core_Session::getLoggedInContactID();
}
}
+ $loader = new Civi\Angular\AngularLoader();
+ $loader->setPageName('civicrm/dashboard');
+
+ // For each dashlet that requires an angular directive, load the angular module which provides that directive
+ $modules = [];
+ foreach (CRM_Core_BAO_Dashboard::getContactDashlets() as $dashlet) {
+ if (!empty($dashlet['directive'])) {
+ foreach ($loader->getAngular()->getModules() as $name => $module) {
+ if (!empty($module['exports'][$dashlet['directive']])) {
+ $modules[] = $name;
+ continue;
+ }
+ }
+ }
+ }
+ $loader->setModules($modules);
+
+ $loader->load();
+
return parent::run();
}
+ /**
+ * partialsCallback from crmDashboard.ang.php
+ *
+ * Generates an html template for each angular-based dashlet.
+ *
+ * @param $moduleName
+ * @param $module
+ * @return array
+ */
+ public static function angularPartials($moduleName, $module) {
+ $partials = [];
+ foreach (CRM_Core_BAO_Dashboard::getContactDashlets() as $dashlet) {
+ if (!empty($dashlet['directive'])) {
+ $partials["~/$moduleName/directives/{$dashlet['directive']}.html"] = "<{$dashlet['directive']}></{$dashlet['directive']}>";
+ }
+ }
+ return $partials;
+ }
+
+ /**
+ * settingsFactory from crmDashboard.ang.php
+ *
+ * @return array
+ */
+ public static function angularSettings() {
+ return [
+ 'dashlets' => CRM_Core_BAO_Dashboard::getContactDashlets(),
+ ];
+ }
+
}
+++ /dev/null
-<?php
-/*
- +--------------------------------------------------------------------+
- | Copyright CiviCRM LLC. All rights reserved. |
- | |
- | This work is published under the GNU AGPLv3 license with some |
- | permitted exceptions and without any warranty. For full license |
- | and copyright information, see https://civicrm.org/licensing |
- +--------------------------------------------------------------------+
- */
-
-/**
- *
- * @package CRM
- * @copyright CiviCRM LLC https://civicrm.org/licensing
- */
-
-/**
- * CiviCRM Dashlet.
- */
-class CRM_Contact_Page_Dashlet extends CRM_Core_Page {
-
- /**
- * Run dashboard.
- */
- public function run() {
- CRM_Utils_System::setTitle(ts('Dashlets'));
-
- $this->assign('admin', CRM_Core_Permission::check('administer CiviCRM'));
-
- // get all dashlets
- $allDashlets = CRM_Core_BAO_Dashboard::getDashlets(FALSE);
-
- // get dashlets for logged in contact
- $currentDashlets = CRM_Core_BAO_Dashboard::getContactDashlets();
- $contactDashlets = $availableDashlets = [];
-
- foreach ($currentDashlets as $item) {
- $key = "{$item['dashboard_id']}-0";
- $contactDashlets[$item['column_no']][$key] = [
- 'label' => $item['label'],
- 'is_reserved' => $allDashlets[$item['dashboard_id']]['is_reserved'],
- ];
- unset($allDashlets[$item['dashboard_id']]);
- }
-
- foreach ($allDashlets as $dashletID => $values) {
- $key = "{$dashletID}-0";
- $availableDashlets[$key] = [
- 'label' => $values['label'],
- 'is_reserved' => $values['is_reserved'],
- ];
- }
-
- $this->assign('contactDashlets', $contactDashlets);
- $this->assign('availableDashlets', $availableDashlets);
-
- return parent::run();
- }
-
-}
class CRM_Core_BAO_Dashboard extends CRM_Core_DAO_Dashboard {
/**
- * Add Dashboard.
+ * Create or update Dashboard.
*
* @param array $params
- * Values.
*
- *
- * @return object
+ * @return CRM_Core_DAO_Dashboard
*/
public static function create($params) {
$hook = empty($params['id']) ? 'create' : 'edit';
}
/**
- * Get the list of dashlets enabled by admin.
- *
- * @param bool $all
- * All or only active.
- * @param bool $checkPermission
- * All or only authorized for the current user.
- *
- * @return array
- * array of dashlets
- */
- public static function getDashlets($all = TRUE, $checkPermission = TRUE) {
- $dashlets = [];
- $dao = new CRM_Core_DAO_Dashboard();
-
- if (!$all) {
- $dao->is_active = 1;
- }
-
- $dao->domain_id = CRM_Core_Config::domainID();
-
- $dao->find();
- while ($dao->fetch()) {
- if ($checkPermission && !self::checkPermission($dao->permission, $dao->permission_operator)) {
- continue;
- }
-
- $values = [];
- CRM_Core_DAO::storeValues($dao, $values);
- $dashlets[$dao->id] = $values;
- }
-
- return $dashlets;
- }
-
- /**
- * Get the list of dashlets for the current user or the specified user.
- *
- * Additionlly, initializes the dashboard with defaults if this is the
- * user's first visit to their dashboard.
- *
- * @param int $contactID
- * Defaults to the current user.
+ * Get all available contact dashlets
*
* @return array
* array of dashlets
*/
- public static function getContactDashlets($contactID = NULL) {
- $contactID = $contactID ? $contactID : CRM_Core_Session::getLoggedInContactID();
- $dashlets = [];
+ public static function getContactDashlets() {
+ if (!isset(Civi::$statics[__CLASS__][__FUNCTION__])) {
+ Civi::$statics[__CLASS__][__FUNCTION__] = [];
+ $params = [
+ 'select' => ['*', 'dashboard_contact.*'],
+ 'join' => [
+ ['DashboardContact AS dashboard_contact', FALSE, ['dashboard_contact.contact_id', '=', CRM_Core_Session::getLoggedInContactID()]],
+ ],
+ 'where' => [
+ ['domain_id', '=', 'current_domain'],
+ ],
+ 'orderBy' => ['dashboard_contact.weight' => 'ASC'],
+ ];
- // Get contact dashboard dashlets.
- $results = civicrm_api3('DashboardContact', 'get', [
- 'contact_id' => $contactID,
- 'is_active' => 1,
- 'dashboard_id.is_active' => 1,
- 'dashboard_id.domain_id' => CRM_Core_Config::domainID(),
- 'options' => ['sort' => 'weight', 'limit' => 0],
- 'return' => [
- 'id',
- 'weight',
- 'column_no',
- 'dashboard_id',
- 'dashboard_id.name',
- 'dashboard_id.label',
- 'dashboard_id.url',
- 'dashboard_id.fullscreen_url',
- 'dashboard_id.cache_minutes',
- 'dashboard_id.permission',
- 'dashboard_id.permission_operator',
- ],
- ]);
+ // Get Dashboard + any joined DashboardContact records.
+ $results = civicrm_api4('Dashboard', 'get', $params);
- foreach ($results['values'] as $item) {
- if (self::checkPermission(CRM_Utils_Array::value('dashboard_id.permission', $item), CRM_Utils_Array::value('dashboard_id.permission_operator', $item))) {
- $dashlets[$item['id']] = [
- 'dashboard_id' => $item['dashboard_id'],
- 'weight' => $item['weight'],
- 'column_no' => $item['column_no'],
- 'name' => $item['dashboard_id.name'],
- 'label' => $item['dashboard_id.label'],
- 'url' => $item['dashboard_id.url'],
- 'cache_minutes' => $item['dashboard_id.cache_minutes'],
- 'fullscreen_url' => $item['dashboard_id.fullscreen_url'] ?? NULL,
- ];
+ // If empty, then initialize default dashlets for this user.
+ if (!array_filter($results->column('dashboard_contact.id'))) {
+ self::initializeDashlets();
}
- }
+ $results = civicrm_api4('Dashboard', 'get', $params);
- // If empty, then initialize default dashlets for this user.
- if (!$results['count']) {
- // They may just have disabled all their dashlets. Check if any records exist for this contact.
- if (!civicrm_api3('DashboardContact', 'getcount', ['contact_id' => $contactID, 'dashboard_id.domain_id' => CRM_Core_Config::domainID()])) {
- $dashlets = self::initializeDashlets();
+ foreach ($results as $item) {
+ if ($item['is_active'] && self::checkPermission($item['permission'], $item['permission_operator'])) {
+ Civi::$statics[__CLASS__][__FUNCTION__][] = $item;
+ }
}
}
-
- return $dashlets;
+ return Civi::$statics[__CLASS__][__FUNCTION__];
}
/**
- * @return array
- */
- public static function getContactDashletsForJS() {
- $data = [[], []];
- foreach (self::getContactDashlets() as $item) {
- $data[$item['column_no']][] = [
- 'id' => (int) $item['dashboard_id'],
- 'name' => $item['name'],
- 'title' => $item['label'],
- 'url' => self::parseUrl($item['url']),
- 'cacheMinutes' => $item['cache_minutes'],
- 'fullscreenUrl' => self::parseUrl($item['fullscreen_url']),
- ];
- }
- return $data;
- }
-
- /**
- * Setup default dashlets for new users.
- *
- * When a user accesses their dashboard for the first time, set up
- * the default dashlets.
+ * Set default dashlets for new users.
*
- * @return array
- * Array of dashboard_id's
- * @throws \CiviCRM_API3_Exception
+ * Called when a user accesses their dashboard for the first time.
*/
public static function initializeDashlets() {
- $dashlets = [];
- $getDashlets = civicrm_api3("Dashboard", "get", [
- 'domain_id' => CRM_Core_Config::domainID(),
- 'option.limit' => 0,
- ]);
- $contactID = CRM_Core_Session::getLoggedInContactID();
- $allDashlets = CRM_Utils_Array::index(['name'], $getDashlets['values']);
+ $allDashlets = (array) civicrm_api4('Dashboard', 'get', [
+ 'where' => [['domain_id', '=', 'current_domain']],
+ ], 'name');
$defaultDashlets = [];
$defaults = ['blog' => 1, 'getting-started' => '0'];
foreach ($defaults as $name => $column) {
'dashboard_id' => $allDashlets[$name]['id'],
'is_active' => 1,
'column_no' => $column,
- 'contact_id' => $contactID,
];
}
}
CRM_Utils_Hook::dashboard_defaults($allDashlets, $defaultDashlets);
if (is_array($defaultDashlets) && !empty($defaultDashlets)) {
- foreach ($defaultDashlets as $id => $defaultDashlet) {
- $dashboard_id = $defaultDashlet['dashboard_id'];
- $dashlet = $getDashlets['values'][$dashboard_id];
- if (!self::checkPermission(CRM_Utils_Array::value('permission', $dashlet), CRM_Utils_Array::value('permission_operator', $dashlet))) {
- continue;
- }
- else {
- $assignDashlets = civicrm_api3("dashboard_contact", "create", $defaultDashlet);
- $values = $assignDashlets['values'][$assignDashlets['id']];
- $dashlets[$assignDashlets['id']] = [
- 'dashboard_id' => $values['dashboard_id'],
- 'weight' => $values['weight'],
- 'column_no' => $values['column_no'],
- 'name' => $dashlet['name'],
- 'label' => $dashlet['label'],
- 'cache_minutes' => $dashlet['cache_minutes'],
- 'url' => $dashlet['url'],
- 'fullscreen_url' => $dashlet['fullscreen_url'] ?? NULL,
- ];
- }
- }
+ \Civi\Api4\DashboardContact::save(FALSE)
+ ->setRecords($defaultDashlets)
+ ->setDefaults(['contact_id' => CRM_Core_Session::getLoggedInContactID()])
+ ->execute();
}
- return $dashlets;
- }
-
- /**
- * @param $url
- * @return string
- */
- public static function parseUrl($url) {
- // Check if it is already a fully-formed url
- if ($url && substr($url, 0, 4) != 'http' && $url[0] != '/') {
- $urlParam = explode('?', $url);
- $url = CRM_Utils_System::url($urlParam[0], CRM_Utils_Array::value(1, $urlParam), FALSE, NULL, FALSE);
- }
- return $url;
}
/**
* Check dashlet permission for current user.
*
- * @param string $permission
- * Comma separated list.
+ * @param array $permissions
* @param string $operator
*
* @return bool
- * true if use has permission else false
+ * true if user has permission to view dashlet
*/
- public static function checkPermission($permission, $operator) {
- if ($permission) {
- $permissions = explode(',', $permission);
+ public static function checkPermission($permissions, $operator) {
+ if ($permissions) {
$config = CRM_Core_Config::singleton();
static $allComponents;
}
}
- /**
- * Save changes made by user to the Dashlet.
- *
- * @param array $columns
- *
- * @param int $contactID
- *
- * @throws RuntimeException
- */
- public static function saveDashletChanges($columns, $contactID = NULL) {
- if (!$contactID) {
- $contactID = CRM_Core_Session::getLoggedInContactID();
- }
-
- if (empty($contactID)) {
- throw new RuntimeException("Failed to determine contact ID");
- }
-
- $dashletIDs = [];
- if (is_array($columns)) {
- foreach ($columns as $colNo => $dashlets) {
- if (!is_int($colNo)) {
- continue;
- }
- $weight = 1;
- foreach ($dashlets as $dashletID => $isMinimized) {
- $dashletID = (int) $dashletID;
- $query = "INSERT INTO civicrm_dashboard_contact
- (weight, column_no, is_active, dashboard_id, contact_id)
- VALUES({$weight}, {$colNo}, 1, {$dashletID}, {$contactID})
- ON DUPLICATE KEY UPDATE weight = {$weight}, column_no = {$colNo}, is_active = 1";
- // fire update query for each column
- CRM_Core_DAO::executeQuery($query);
-
- $dashletIDs[] = $dashletID;
- $weight++;
- }
- }
- }
-
- // Find dashlets in this domain.
- $domainDashlets = civicrm_api3('Dashboard', 'get', [
- 'return' => array('id'),
- 'domain_id' => CRM_Core_Config::domainID(),
- 'options' => ['limit' => 0],
- ]);
-
- // Get the array of IDs.
- $domainDashletIDs = [];
- if ($domainDashlets['is_error'] == 0) {
- $domainDashletIDs = CRM_Utils_Array::collect('id', $domainDashlets['values']);
- }
-
- // Restrict query to Dashlets in this domain.
- $domainDashletClause = !empty($domainDashletIDs) ? "dashboard_id IN (" . implode(',', $domainDashletIDs) . ")" : '(1)';
-
- // Target only those Dashlets which are inactive.
- $dashletClause = $dashletIDs ? "dashboard_id NOT IN (" . implode(',', $dashletIDs) . ")" : '(1)';
-
- // Build params.
- $params = [
- 1 => [$contactID, 'Integer'],
- ];
-
- // Build query.
- $updateQuery = "UPDATE civicrm_dashboard_contact
- SET is_active = 0
- WHERE $domainDashletClause
- AND $dashletClause
- AND contact_id = %1";
-
- // Disable inactive widgets.
- CRM_Core_DAO::executeQuery($updateQuery, $params);
- }
-
/**
* Add dashlets.
*
}
}
- /**
- * @param array $params
- * Each item is a spec for a dashlet on the contact's dashboard.
- * @return bool
- */
- public static function addContactDashletToDashboard(&$params) {
- $valuesString = NULL;
- $columns = [];
- foreach ($params as $dashboardIDs) {
- $contactID = $dashboardIDs['contact_id'] ?? NULL;
- $dashboardID = $dashboardIDs['dashboard_id'] ?? NULL;
- $column = $dashboardIDs['column_no'] ?? 0;
- $columns[$column][$dashboardID] = 0;
- }
- self::saveDashletChanges($columns, $contactID);
- return TRUE;
- }
-
- /**
- * @deprecated
- * @param int $dashletID
- * @return bool
- */
- public static function deleteDashlet($dashletID) {
- CRM_Core_Error::deprecatedFunctionWarning('CRM_Core_DAO_Dashboard::deleteRecord');
- try {
- CRM_Core_DAO_Dashboard::deleteRecord(['id' => $dashletID]);
- }
- catch (CRM_Core_Exception $e) {
- return FALSE;
- }
- return TRUE;
- }
-
}
*
* Generated from xml/schema/CRM/Core/Dashboard.xml
* DO NOT EDIT. Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:d1da8f66f209f70a5e8a1fd440bb4694)
+ * (GenCodeChecksum:1f32e4ff906a25d432cc28598e388f60)
*/
/**
*/
public $cache_minutes;
+ /**
+ * Element name of angular directive to invoke (lowercase hyphenated format)
+ *
+ * @var string
+ */
+ public $directive;
+
/**
* Class constructor.
*/
'localizable' => 0,
'add' => '4.7',
],
+ 'directive' => [
+ 'name' => 'directive',
+ 'type' => CRM_Utils_Type::T_STRING,
+ 'title' => ts('Angular directive'),
+ 'description' => ts('Element name of angular directive to invoke (lowercase hyphenated format)'),
+ 'maxlength' => 255,
+ 'size' => CRM_Utils_Type::HUGE,
+ 'where' => 'civicrm_dashboard.directive',
+ 'table_name' => 'civicrm_dashboard',
+ 'entity' => 'Dashboard',
+ 'bao' => 'CRM_Core_BAO_Dashboard',
+ 'localizable' => 0,
+ 'add' => '5.33',
+ ],
];
CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']);
}
<access_arguments>access CiviCRM</access_arguments>
<weight>0</weight>
</item>
- <item>
- <path>civicrm/dashlet</path>
- <title>CiviCRM Dashlets</title>
- <page_type>1</page_type>
- <page_callback>CRM_Contact_Page_Dashlet</page_callback>
- <access_arguments>access CiviCRM</access_arguments>
- <weight>1</weight>
- </item>
<item>
<path>civicrm/contact/search</path>
<title>Find Contacts</title>
<page_callback>CRM_Contact_Page_AJAX::buildSubTypes</page_callback>
<access_arguments>access CiviCRM</access_arguments>
</item>
-<item>
- <path>civicrm/ajax/dashboard</path>
- <page_callback>CRM_Contact_Page_AJAX::dashboard</page_callback>
- <access_arguments>access CiviCRM</access_arguments>
- <page_type>3</page_type>
-</item>
<item>
<path>civicrm/ajax/signature</path>
<page_callback>CRM_Contact_Page_AJAX::getSignature</page_callback>
// }
}
- /*
- * Important! All upgrade functions MUST add a 'runSql' task.
- * Uncomment and use the following template for a new upgrade version
- * (change the x in the function name):
+ /**
+ * Upgrade function.
+ *
+ * @param string $rev
*/
-
- // /**
- // * Upgrade function.
- // *
- // * @param string $rev
- // */
- // public function upgrade_5_0_x($rev) {
- // $this->addTask(ts('Upgrade DB to %1: SQL', [1 => $rev]), 'runSql', $rev);
- // $this->addTask('Do the foo change', 'taskFoo', ...);
- // // Additional tasks here...
- // // Note: do not use ts() in the addTask description because it adds unnecessary strings to transifex.
- // // The above is an exception because 'Upgrade DB to %1: SQL' is generic & reusable.
- // }
-
- // public static function taskFoo(CRM_Queue_TaskContext $ctx, ...) {
- // return TRUE;
- // }
+ public function upgrade_5_33_alpha1($rev) {
+ $this->addTask(ts('Upgrade DB to %1: SQL', [1 => $rev]), 'runSql', $rev);
+ $this->addTask('Add column civicrm_dashboard.directive', 'addColumn', 'civicrm_dashboard', 'directive', "varchar(255) COMMENT 'Element name of angular directive to invoke (lowercase hyphenated format)'");
+ }
}
}
/**
- * This hook is called while viewing contact dashboard.
+ * This hook is called while initializing the default dashlets for a contact dashboard.
*
* @param array $availableDashlets
* List of dashlets; each is formatted per api/v3/Dashboard
$angularModules['exportui'] = include "$civicrm_root/ang/exportui.ang.php";
$angularModules['api4Explorer'] = include "$civicrm_root/ang/api4Explorer.ang.php";
$angularModules['api4'] = include "$civicrm_root/ang/api4.ang.php";
+ $angularModules['crmDashboard'] = include "$civicrm_root/ang/crmDashboard.ang.php";
foreach (\CRM_Core_Component::getEnabledComponents() as $component) {
$angularModules = array_merge($angularModules, $component->getAngularModules());
--- /dev/null
+<?php
+// Dashboard Angular module configuration
+return [
+ 'ext' => 'civicrm',
+ 'js' => [
+ 'ang/crmDashboard.js',
+ 'ang/crmDashboard/*.js',
+ 'ang/crmDashboard/*/*.js',
+ ],
+ 'css' => ['css/dashboard.css'],
+ 'partials' => ['ang/crmDashboard'],
+ 'partialsCallback' => ['CRM_Contact_Page_DashBoard', 'angularPartials'],
+ 'basePages' => ['civicrm/dashboard'],
+ 'requires' => ['crmUi', 'crmUtil', 'ui.sortable', 'dialogService', 'api4'],
+ 'settingsFactory' => ['CRM_Contact_Page_DashBoard', 'angularSettings'],
+ 'permissions' => ['administer CiviCRM'],
+];
--- /dev/null
+(function(angular, $, _) {
+ angular.module('crmDashboard', CRM.angRequires('crmDashboard'));
+
+ angular.module('crmDashboard', CRM.angular.modules);
+
+})(angular, CRM.$, CRM._);
--- /dev/null
+<div id="civicrm-dashboard">
+ <fieldset class="crm-inactive-dashlet-fieldset">
+ <legend>
+ <a href class="crm-hover-button" ng-click="$ctrl.showInactive = !$ctrl.showInactive">
+ <i class="crm-i fa-{{ $ctrl.showInactive ? 'minus' : 'plus' }}" aria-hidden="true"></i>
+ {{ $ctrl.inactive.length === 1 ? ts('1 Available Dashlet') : ts('%1 Available Dashlets', {1: $ctrl.inactive.length}) }}
+ </a>
+ </legend>
+ <div ng-if="$ctrl.showInactive" ng-model="$ctrl.inactive" ui-sortable="$ctrl.sortableOptions" class="crm-dashboard-droppable">
+ <div class="help">
+ {{ ts('Drag and drop dashlets onto the left or right columns below to add them to your dashboard. Changes are automatically saved.') }}
+ <a crm-ui-help="hs({id: 'dash_configure', title: ts('Dashboard Configuration')})"></a>
+ </div>
+ <div ng-repeat="dashlet in $ctrl.inactive" class="crm-inactive-dashlet">
+ <crm-inactive-dashlet dashlet="dashlet" delete="$ctrl.deleteDashlet($index)"></crm-inactive-dashlet>
+ </div>
+ </div>
+ </fieldset>
+ <div class="crm-flex-box" ng-if="!$ctrl.fullscreenDashlet">
+ <div ng-repeat="column in $ctrl.columns" class="crm-flex-{{2 + $index}} crm-dashboard-droppable" ng-model="column" ui-sortable="$ctrl.sortableOptions">
+ <div ng-repeat="dashlet in column" class="crm-dashlet">
+ <crm-dashlet dashlet="dashlet" remove="$ctrl.removeDashlet($parent.$index, $index)" fullscreen="$ctrl.showFullscreen(dashlet)" ></crm-dashlet>
+ </div>
+ </div>
+ </div>
+</div>
--- /dev/null
+<div class="crm-dashlet-header" ng-if="!$ctrl.isFullscreen">
+ <a href class="crm-i fa-times" title="{{:: ts('Remove from dashboard') }}" aria-hidden="true" ng-click="$ctrl.remove()"></a>
+ <a href class="crm-i fa-expand" title="{{:: ts('View fullscreen') }}" aria-hidden="true" ng-click="$ctrl.fullscreen()"></a>
+ <a href class="crm-i fa-refresh" title="{{:: ts('Refresh') }}" aria-hidden="true" ng-click="$ctrl.forceRefresh()"></a>
+ <a href class="crm-dashlet-collapse crm-i fa-caret-{{ $ctrl.collapsed ? 'right' : 'down' }}" title="{{ $ctrl.collapsed ? ts('Expand') : ts('Collapse') }}" aria-hidden="true" ng-click="$ctrl.toggleCollapse()"></a>
+ <h3>{{ $ctrl.dashlet.label }}</h3>
+</div>
+<div class="crm-dashlet-content" ng-show="!$ctrl.collapsed">
+ <div ng-if="$ctrl.dashlet.directive" ng-include="'~/crmDashboard/directives/' + $ctrl.dashlet.directive + '.html'"></div>
+</div>
--- /dev/null
+<crm-dashlet dashlet="model" is-fullscreen="true"></crm-dashlet>
--- /dev/null
+<div class="crm-dashlet-header">
+ <a href class="crm-i fa-trash" title="{{:: ts('Permanently Delete %1', {1: $ctrl.dashlet.label}) }}" aria-hidden="true" crm-confirm="$ctrl.confirmParams" on-yes="$ctrl.delete()" ng-if="$ctrl.isAdmin && !$ctrl.dashlet.is_reserved"></a>
+ <h3>{{ $ctrl.dashlet.label }}</h3>
+</div>
--- /dev/null
+(function(angular, $, _) {
+
+ angular.module('crmDashboard').component('crmDashboard', {
+ templateUrl: '~/crmDashboard/Dashboard.html',
+ controller: function ($scope, $element, crmApi4, crmUiHelp, dialogService) {
+ var ts = $scope.ts = CRM.ts(),
+ ctrl = this;
+ this.columns = [[], []];
+ this.inactive = [];
+ this.contactDashlets = {};
+ this.sortableOptions = {
+ connectWith: '.crm-dashboard-droppable',
+ handle: '.crm-dashlet-header'
+ };
+ $scope.hs = crmUiHelp({file: 'CRM/Contact/Page/Dashboard'});
+
+ this.$onInit = function() {
+ // Sort dashlets into columns
+ _.each(CRM.crmDashboard.dashlets, function(dashlet) {
+ if (dashlet['dashboard_contact.is_active']) {
+ ctrl.columns[dashlet['dashboard_contact.column_no']].push(dashlet);
+ } else {
+ ctrl.inactive.push(dashlet);
+ }
+ });
+
+ $scope.$watchCollection('$ctrl.columns[0]', onChange);
+ $scope.$watchCollection('$ctrl.columns[1]', onChange);
+ };
+
+ var save = _.debounce(function() {
+ $scope.$apply(function() {
+ var toSave = [];
+ _.each(ctrl.inactive, function(dashlet) {
+ if (dashlet['dashboard_contact.id']) {
+ toSave.push({
+ dashboard_id: dashlet.id,
+ id: dashlet['dashboard_contact.id'],
+ is_active: false
+ });
+ }
+ });
+ _.each(ctrl.columns, function(dashlets, col) {
+ _.each(dashlets, function(dashlet, index) {
+ var item = {
+ dashboard_id: dashlet.id,
+ is_active: true,
+ column_no: col,
+ weight: index
+ };
+ if (dashlet['dashboard_contact.id']) {
+ item.id = dashlet['dashboard_contact.id'];
+ }
+ toSave.push(item);
+ });
+ });
+ var apiCall = crmApi4('DashboardContact', 'save', {
+ records: toSave,
+ defaults: {contact_id: 'user_contact_id'}
+ }, 'dashboard_id');
+ apiCall.then(function(results) {
+ CRM.status(ts('Saved'));
+ _.each(ctrl.columns, function(dashlets) {
+ _.each(dashlets, function(dashlet) {
+ dashlet['dashboard_contact.id'] = results[dashlet.id].id;
+ });
+ });
+ });
+ });
+ }, 2000);
+
+ this.removeDashlet = function(column, index) {
+ ctrl.inactive.push(ctrl.columns[column][index]);
+ ctrl.columns[column].splice(index, 1);
+ };
+
+ this.deleteDashlet = function(index) {
+ crmApi4('Dashboard', 'delete', {where: [['id', '=', ctrl.inactive[index].id]]}).then(function() {
+ CRM.status(ts('Deleted'));
+ });
+ ctrl.inactive.splice(index, 1);
+ };
+
+ this.showFullscreen = function(dashlet) {
+ ctrl.fullscreenDashlet = true;
+ var options = CRM.utils.adjustDialogDefaults({
+ width: '90%',
+ height: '90%',
+ autoOpen: false,
+ title: dashlet.label
+ });
+ dialogService.open('fullscreenDashlet', '~/crmDashboard/FullscreenDialog.html', dashlet, options)
+ .then(function() {
+ ctrl.fullscreenDashlet = null;
+ }, function() {
+ ctrl.fullscreenDashlet = null;
+ });
+ };
+
+ function onChange(newVal, oldVal) {
+ if (oldVal !== newVal) {
+ save();
+ }
+ }
+
+ }
+ });
+
+})(angular, CRM.$, CRM._);
--- /dev/null
+(function(angular, $, _) {
+
+ angular.module('crmDashboard').component('crmDashlet', {
+ bindings: {
+ dashlet: '<',
+ remove: '&',
+ fullscreen: '&',
+ isFullscreen: '<'
+ },
+ templateUrl: '~/crmDashboard/Dashlet.html',
+ controller: function ($scope, $element, $timeout, $interval) {
+ var ts = $scope.ts = CRM.ts(),
+ ctrl = this,
+ lastLoaded,
+ checker;
+
+ function getCache() {
+ return CRM.cache.get('dashboard', {})[ctrl.dashlet.id] || {};
+ }
+
+ function setCache(content) {
+ var data = CRM.cache.get('dashboard', {}),
+ cached = data[ctrl.dashlet.id] || {};
+ data[ctrl.dashlet.id] = {
+ content: content || cached.content || null,
+ collapsed: ctrl.collapsed,
+ lastLoaded: content ? $.now() : (cached.lastLoaded || null)
+ };
+ CRM.cache.set('dashboard', data);
+ lastLoaded = data[ctrl.dashlet.id].lastLoaded;
+ }
+
+ function isFresh() {
+ return lastLoaded && (ctrl.dashlet.cache_minutes * 60000 + lastLoaded) > $.now();
+ }
+
+ function setChecker() {
+ if (angular.isUndefined(checker)) {
+ checker = $interval(function() {
+ if (!ctrl.collapsed && !isFresh() && (!document.hasFocus || document.hasFocus())) {
+ stopChecker();
+ reload(ctrl.dashlet.url);
+ }
+ }, 1000);
+ }
+ }
+
+ function stopChecker() {
+ if (angular.isDefined(checker)) {
+ $interval.cancel(checker);
+ checker = undefined;
+ }
+ }
+
+ this.toggleCollapse = function() {
+ ctrl.collapsed = !ctrl.collapsed;
+ setCache();
+ };
+
+ this.forceRefresh = function() {
+ if (ctrl.dashlet.url) {
+ reload(ctrl.dashlet.url);
+ } else if (ctrl.dashlet.directive) {
+ var directive = ctrl.dashlet.directive;
+ ctrl.dashlet.directive = null;
+ $timeout(function() {
+ ctrl.dashlet.directive = directive;
+ }, 10);
+ }
+ };
+
+ function reload(path) {
+ var extern = path.slice(0, 1) === '/' || path.slice(0, 4) === 'http',
+ url = extern ? path : CRM.url(path);
+ CRM.loadPage(url, {target: $('.crm-dashlet-content', $element)});
+ }
+
+ this.$onInit = function() {
+ if (this.isFullscreen && this.dashlet.fullscreen_url) {
+ reload(this.dashlet.fullscreen_url);
+ return;
+ }
+
+ var cache = getCache();
+ lastLoaded = cache.lastLoaded;
+ ctrl.collapsed = !this.fullscreen && !!cache.collapsed;
+
+ if (ctrl.dashlet.url) {
+ var fresh = cache.content && isFresh();
+ if (fresh) {
+ $('.crm-dashlet-content', $element).html(cache.content).trigger('crmLoad');
+ setChecker();
+ }
+
+ $element.on('crmLoad', function(event, data) {
+ if ($(event.target).is('.crm-dashlet-content')) {
+ setCache(data.content);
+ setChecker();
+ }
+ });
+
+ if (!fresh) {
+ reload(ctrl.dashlet.url);
+ }
+ }
+
+ };
+
+ this.$onDestroy = function() {
+ stopChecker();
+ };
+ }
+ });
+
+})(angular, CRM.$, CRM._);
--- /dev/null
+(function(angular, $, _) {
+
+ angular.module('crmDashboard').component('crmInactiveDashlet', {
+ bindings: {
+ dashlet: '<',
+ delete: '&'
+ },
+ templateUrl: '~/crmDashboard/InactiveDashlet.html',
+ controller: function ($scope, $element) {
+ var ts = $scope.ts = CRM.ts(),
+ ctrl = this;
+ ctrl.isAdmin = CRM.checkPerm('administer CiviCRM');
+
+ this.$onInit = function() {
+ ctrl.confirmParams = {
+ type: 'delete',
+ obj: ctrl.dashlet,
+ width: 400,
+ message: ts('Do you want to remove this dashlet as an "Available Dashlet", AND delete it from all user dashboards?')
+ };
+ };
+ }
+ });
+
+})(angular, CRM.$, CRM._);
* @return array
*/
function civicrm_api3_dashboard_contact_create($params) {
- $errors = _civicrm_api3_dashboard_contact_check_params($params);
- if ($errors !== NULL) {
- return $errors;
- }
return _civicrm_api3_basic_create(_civicrm_api3_get_BAO(__FUNCTION__), $params, 'DashboardContact');
}
$fields['dashboard_id']['api.required'] = TRUE;
}
-/**
- * Check permissions on contact dashboard retrieval.
- *
- * @param array $params
- * Array per getfields metadata.
- *
- * @return array|null
- */
-function _civicrm_api3_dashboard_contact_check_params(&$params) {
- if (!empty($params['dashboard_id'])) {
- $allDashlets = CRM_Core_BAO_Dashboard::getDashlets(TRUE, $params['check_permissions'] ?? FALSE);
- if (!isset($allDashlets[$params['dashboard_id']])) {
- return civicrm_api3_create_error('Invalid or inaccessible dashboard ID');
- }
- }
- return NULL;
-}
-
/**
* Delete an existing dashboard-contact.
*
position: absolute;
}
+.crm-container .crm-flex-box {
+ display: flex;
+}
+.crm-container .crm-flex-box > * {
+ flex: 1;
+ min-width: 0; /* prevents getting squashed by whitespace:nowrap content */
+}
+.crm-container .crm-flex-box > .crm-flex-2 {
+ flex: 2;
+}
+.crm-container .crm-flex-box > .crm-flex-3 {
+ flex: 3;
+}
+.crm-container .crm-flex-box > .crm-flex-4 {
+ flex: 4;
+}
+
+.crm-container .crm-draggable {
+ cursor: move;
+}
+
.crm-container input {
box-sizing: content-box;
}
-/**
-* @file
-* CSS for the jQuery.dashboard() plugin.
-*
-* Released under the GNU General Public License. See LICENSE.txt.
-*/
-
-#crm-container .column {
- float: left;
- margin: 0;
- /* padding-bottom and min-height make sure that there is always a sizable drop zone. */
+#civicrm-dashboard > .crm-flex-box {
min-height: 200px;
- padding: 0 0 40px;
- list-style-type: none;
-}
-
-#crm-container .column-0 {
- width: 40%;
-}
-
-#crm-container .column-1 {
- width: 60%;
-}
-
-/* The placeholder that indicates where a dragged widget will land if dropped now. */
-#crm-container .placeholder {
- margin: 5px;
- border: 3px dashed #CDE8FE;
}
-/* Spacing between widgets. */
-#crm-container li.widget,
-#crm-container li.empty-placeholder {
- margin: 0 3px 10px 3px;
-}
-#crm-container li.widget {
- padding: 0;
-}
-
-/* Spacing inside widgets. */
-#crm-container .widget-wrapper {
- padding: 0;
- overflow: hidden;
- margin-right: .25em;
+.crm-container .crm-dashlet {
+ margin: 10px;
box-shadow: 1px 1px 4px 1px rgba(0,0,0,0.2);
border-radius: 3px;
}
-#crm-container .widget-wrapper.db-hover-handle {
- box-shadow: 1px 1px 8px 3px rgba(0,0,0,0.2);
+.crm-container .crm-dashlet-content {
+ background-color: #ffffff;
+ padding: 0.5em;
+ overflow-x: auto;
+ min-height: 10em;
}
-#crm-container .ui-sortable-helper .widget-wrapper {
+.crm-container .ui-sortable-helper.crm-dashlet {
box-shadow: 1px 1px 8px 3px rgba(0,0,0,0.5);
}
-/* widget header / title */
-#crm-container .widget-header {
- background: transparent none repeat scroll 0 0;
- cursor: move;
- margin: 0 50px 0 0;
- padding: 0;
- color: #fcfcfc;
-}
-
-/* widget content / body*/
-#crm-container .widget-content {
- background-color: #ffffff;
- padding:0.5em;
- overflow-x:auto;
- min-height: 10em;
-}
-
-#crm-container .widget-controls {
+.crm-container .crm-dashlet-header {
background-color: #5D677B;
- /* Standards-browsers do this anyway because all inner block elements are floated. IE doesn't because it's crap. */
display: block;
padding: 5px 0;
}
-#crm-container .widget-icon, #crm-container .full-screen-close-icon img {
- display: block;
- float: right;
- margin-left: 2px;
- margin-top: 2px;
- cursor: pointer;
-}
-
-#full-screen-header {
- display: block;
- padding: .2em .4em;
- background: #F0F0E8;
- /* Although this is an <a> link, it doesn't have an href="" attribute. */
- cursor: pointer;
-}
-
-/* Make the throbber in-yer-face. */
-#crm-container .throbber {
- text-align: right;
- background:url("../packages/jquery/css/images/throbber.gif") no-repeat scroll 0 0 transparent;
- height:20px;
- width:20px;
-}
-
-#crm-container p.loadtext {
- margin:1.6em 0 0 26px;
+.crm-container .crm-dashlet-header h3 {
+ background: transparent;
+ cursor: move;
+ margin: 0 50px 0 0;
+ padding: 0;
+ color: #fcfcfc;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
}
-#crm-container .widget-controls .crm-i {
+.crm-container .crm-dashlet-header a {
font-size: 14px;
padding: 0 6px;
font-weight: normal;
- float: left;
+ float: right;
cursor: pointer;
color: #fcfcfc;
opacity: .7;
}
-
-#crm-container .widget-controls:hover .crm-i {
- opacity: 1;
-}
-
-#crm-container .widget-controls .crm-i.fa-expand,
-#crm-container .widget-controls .crm-i.fa-times {
- float:right;
-}
-
-/* CSS for Dashlets */
-
-#crm-container #available-dashlets {
- width: 98%;
- border: 2px dashed #CDE8FE;
- background-color: #999999;
- min-height:40px;
-}
-
-#crm-container .dash-column {
- border: 2px solid #696969;
- min-height: 100px;
- background-color: #EEEEEE
-}
-
-#crm-container .dashlets-header {
- font-weight: bold;
-}
-
-#crm-container #dashlets-header-col-0 {
+.crm-container .crm-dashlet-header a.crm-dashlet-collapse {
float: left;
- width: 40%;
}
-#crm-container #dashlets-header-col-1 {
- margin-left: 42%;
- width: 56%;
-}
-
-#crm-container #existing-dashlets-col-0 {
- float: left;
- width: 40%;
-}
-
-#crm-container #existing-dashlets-col-1 {
- margin-left: 42%;
- width: 56%;
+.crm-container .crm-dashlet-header:hover .crm-i {
+ opacity: 1;
}
-#crm-container .portlet {
- margin: .5em;
- width: 75%;
- display: inline-block;
+fieldset.crm-inactive-dashlet-fieldset {
+ border: 0 none;
}
-#crm-container .portlet-header {
- margin: 0.3em;
- padding: 0.5em;
- cursor: pointer;
+fieldset.crm-inactive-dashlet-fieldset > div {
+ background-color: lightgray;
+ padding: 8px;
}
-#crm-container #available-dashlets .portlet {
- width: auto;
+fieldset.crm-inactive-dashlet-fieldset legend {
+ background-color: #eee;
+ display: block;
+ top: 4px;
}
-#crm-container .portlet-header .crm-i {
- float: right;
- color: #52534D;
+.crm-container .crm-inactive-dashlet {
+ display: inline-block;
+ width: 230px;
+ height: 70px;
+ margin: 10px;
+ box-shadow: 1px 1px 4px 1px rgba(0,0,0,0.2);
+ background-color: #eee;
}
-
-#crm-container .portlet-content {
- padding: 0.4em;
+.crm-container .crm-inactive-dashlet .crm-dashlet-header h3 {
+ font-size: 100%;
+ padding-left: 5px;
}
-#crm-container .ui-sortable-placeholder {
- border: 1px dotted black;
+#civicrm-dashboard .ui-sortable-placeholder {
+ border: 2px dashed grey;
visibility: visible !important;
- height: 50px !important;
+ height: 80px !important;
}
-#crm-container .ui-sortable-placeholder * {
+#civicrm-dashboard .ui-sortable-placeholder * {
visibility: hidden;
}
-
-@media screen and (max-device-width: 50em), screen and (max-width: 50em) {
- #crm-container .column {
- width: 100%;
- min-height: 0;
- }
- #crm-container .column-0 {
- padding: 0;
- }
-}
-.crm-flex-box {
- display: flex;
-}
-.crm-flex-box > * {
- flex: 1;
-}
-.crm-flex-box > .crm-flex-2 {
- flex: 2;
-}
-.crm-flex-box > .crm-flex-3 {
- flex: 3;
-}
-.crm-flex-box > .crm-flex-4 {
- flex: 4;
-}
-.crm-draggable {
- cursor: move;
-}
-
#bootstrap-theme #crm-search-results-page-size {
width: 5em;
}
+++ /dev/null
-// https://civicrm.org/licensing
-/* global CRM, ts */
-/*jshint loopfunc: true */
-(function($, _) {
- 'use strict';
- // Constructor for dashboard object.
- $.fn.dashboard = function(options) {
- // Public properties of dashboard.
- var dashboard = {};
- dashboard.element = this.empty();
- dashboard.ready = false;
- dashboard.columns = [];
- dashboard.widgets = {};
-
- /**
- * Public methods of dashboard.
- */
-
- // Saves the order of widgets for all columns including the widget.minimized status to options.ajaxCallbacks.saveColumns.
- dashboard.saveColumns = function(showStatus) {
- // Update the display status of the empty placeholders.
- $.each(dashboard.columns, function(c, col) {
- if ( typeof col == 'object' ) {
- // Are there any visible children of the column (excluding the empty placeholder)?
- if (col.element.children(':visible').not(col.emptyPlaceholder).length > 0) {
- col.emptyPlaceholder.hide();
- }
- else {
- col.emptyPlaceholder.show();
- }
- }
- });
-
- // Don't save any changes to the server unless the dashboard has finished initiating.
- if (!dashboard.ready) {
- return;
- }
-
- // Build a list of params to post to the server.
- var params = {};
-
- // For each column...
- $.each(dashboard.columns, function(c, col) {
-
- // IDs of the sortable elements in this column.
- var ids = (typeof col == 'object') ? col.element.sortable('toArray') : [];
-
- // For each id...
- $.each(ids, function(w, id) {
- if (typeof id == 'string') {
- // Chop 'widget-' off of the front so that we have the real widget id.
- id = id.substring('widget-'.length);
- // Add one flat property to the params object that will look like an array element to the PHP server.
- // Unfortunately jQuery doesn't do this for us.
- if (typeof dashboard.widgets[id] == 'object') params['columns[' + c + '][' + id + ']'] = (dashboard.widgets[id].minimized ? '1' : '0');
- }
- });
- });
-
- // The ajaxCallback settings overwrite any duplicate properties.
- $.extend(params, opts.ajaxCallbacks.saveColumns.data);
- var post = $.post(opts.ajaxCallbacks.saveColumns.url, params, function() {
- invokeCallback(opts.callbacks.saveColumns, dashboard);
- });
- if (showStatus !== false) {
- CRM.status({}, post);
- }
- };
-
- /**
- * Private properties of dashboard.
- */
-
- // Used to determine whether two resort events are resulting from the same UI event.
- var currentReSortEvent = null;
-
- // Merge in the caller's options with the defaults.
- var opts = $.extend({}, $.fn.dashboard.defaults, options);
-
- var localCache = window.localStorage && localStorage.dashboard ? JSON.parse(localStorage.dashboard) : {};
-
- init(opts.widgetsByColumn);
-
- return dashboard;
-
- /**
- * Private methods of dashboard.
- */
-
- // Initialize widget columns.
- function init(widgets) {
- var markup = '<li class="empty-placeholder">' + opts.emptyPlaceholderInner + '</li>';
-
- // Build the dashboard in the DOM. For each column...
- // (Don't iterate on widgets since this will break badly if the dataset has empty columns.)
- var emptyDashboard = true;
- for (var c = 0; c < opts.columns; c++) {
- // Save the column to both the public scope for external accessibility and the local scope for readability.
- var col = dashboard.columns[c] = {
- initialWidgets: [],
- element: $('<ul id="column-' + c + '" class="column column-' + c + '"></ul>').appendTo(dashboard.element)
- };
-
- // Add the empty placeholder now, hide it and save it.
- col.emptyPlaceholder = $(markup).appendTo(col.element).hide();
-
- // For each widget in this column.
- $.each(widgets[c], function(num, item) {
- var id = (num+1) + '-' + item.id;
- col.initialWidgets[id] = dashboard.widgets[item.id] = widget($.extend({
- element: $('<li class="widget"></li>').appendTo(col.element),
- initialColumn: col
- }, item));
- emptyDashboard = false;
- });
- }
-
- if (emptyDashboard) {
- emptyDashboardCondition();
- } else {
- completeInit();
- }
-
- invokeCallback(opts.callbacks.init, dashboard);
- }
-
- // function that is called when dashboard is empty
- function emptyDashboardCondition( ) {
- $(".show-refresh").hide( );
- $("#empty-message").show( );
- }
-
- // Cache dashlet info in localStorage
- function saveLocalCache() {
- localCache = {};
- $.each(dashboard.widgets, function(id, widget) {
- localCache[id] = {
- content: widget.content,
- lastLoaded: widget.lastLoaded,
- minimized: widget.minimized
- };
- });
- if (window.localStorage) {
- localStorage.dashboard = JSON.stringify(localCache);
- }
- }
-
- // Contructors for each widget call this when initialization has finished so that dashboard can complete it's intitialization.
- function completeInit() {
- // Only do this once.
- if (dashboard.ready) {
- return;
- }
-
- // Make widgets sortable across columns.
- dashboard.sortableElement = $('.column').sortable({
- connectWith: ['.column'],
-
- // The class of the element by which widgets are draggable.
- handle: '.widget-header',
-
- // The class of placeholder elements (the 'ghost' widget showing where the dragged item would land if released now.)
- placeholder: 'placeholder',
- activate: function(event, ui) {
- var h= $(ui.item).height();
- $('.placeholder').css('height', h +'px');
- },
-
- opacity: 0.2,
-
- // Maks sure that only widgets are sortable, and not empty placeholders.
- items: '> .widget',
-
- forcePlaceholderSize: true,
-
- // Callback functions.
- update: resorted,
- start: hideEmptyPlaceholders
- });
-
- // Update empty placeholders.
- dashboard.saveColumns();
- dashboard.ready = true;
- invokeCallback(opts.callbacks.ready, dashboard);
-
- // Auto-refresh widgets when content is stale
- window.setInterval(function() {
- if (!document.hasFocus || document.hasFocus()) {
- $.each(dashboard.widgets, function (i, widget) {
- if (!widget.cacheIsFresh()) {
- widget.reloadContent();
- }
- });
- }
- }, 5000);
- }
-
- // Callback for when any list has changed (and the user has finished resorting).
- function resorted(e, ui) {
- // Only do anything if we haven't already handled resorts based on changes from this UI DOM event.
- // (resorted() gets invoked once for each list when an item is moved from one to another.)
- if (!currentReSortEvent || e.originalEvent != currentReSortEvent) {
- currentReSortEvent = e.originalEvent;
- dashboard.saveColumns();
- }
- }
-
- // Callback for when a user starts resorting a list. Hides all the empty placeholders.
- function hideEmptyPlaceholders(e, ui) {
- for (var c in dashboard.columns) {
- if( (typeof dashboard.columns[c]) == 'object' ) dashboard.columns[c].emptyPlaceholder.hide();
- }
- }
-
- // @todo use an event library to register, bind to and invoke events.
- // @param callback is a function.
- // @param theThis is the context given to that function when it executes. It becomes 'this' inside of that function.
- function invokeCallback(callback, theThis, parameterOne) {
- if (callback) {
- callback.call(theThis, parameterOne);
- }
- }
-
- /**
- * widget object
- * Private sub-class of dashboard
- * Constructor starts
- */
- function widget(widget) {
- // Merge default options with the options defined for this widget.
- widget = $.extend({}, $.fn.dashboard.widget.defaults, localCache[widget.id] || {}, widget);
-
- /**
- * Public methods of widget.
- */
-
- // Toggles the minimize() & maximize() methods.
- widget.toggleMinimize = function() {
- if (widget.minimized) {
- widget.maximize();
- }
- else {
- widget.minimize();
- }
-
- widget.hideSettings();
- };
- widget.minimize = function() {
- $('.widget-content', widget.element).slideUp(opts.animationSpeed);
- $(widget.controls.minimize.element)
- .addClass('fa-caret-right')
- .removeClass('fa-caret-down')
- .attr('title', ts('Expand'));
- widget.minimized = true;
- saveLocalCache();
- };
- widget.maximize = function() {
- $(widget.controls.minimize.element)
- .removeClass( 'fa-caret-right' )
- .addClass( 'fa-caret-down' )
- .attr('title', ts('Collapse'));
- widget.minimized = false;
- saveLocalCache();
- if (!widget.contentLoaded) {
- loadContent();
- }
- $('.widget-content', widget.element).slideDown(opts.animationSpeed);
- };
-
- // Toggles whether the widget is in settings-display mode or not.
- widget.toggleSettings = function() {
- if (widget.settings.displayed) {
- // Widgets always exit settings into maximized state.
- widget.maximize();
- widget.hideSettings();
- invokeCallback(opts.widgetCallbacks.hideSettings, widget);
- }
- else {
- widget.minimize();
- widget.showSettings();
- invokeCallback(opts.widgetCallbacks.showSettings, widget);
- }
- };
- widget.showSettings = function() {
- if (widget.settings.element) {
- widget.settings.element.show();
-
- // Settings are loaded via AJAX. Only execute the script if the settings have been loaded.
- if (widget.settings.ready) {
- getJavascript(widget.settings.script);
- }
- }
- else {
- // Settings have not been initialized. Do so now.
- initSettings();
- }
- widget.settings.displayed = true;
- };
- widget.hideSettings = function() {
- if (widget.settings.element) {
- widget.settings.element.hide();
- }
- widget.settings.displayed = false;
- };
- widget.saveSettings = function() {
- // Build list of parameters to POST to server.
- var params = {};
- // serializeArray() returns an array of objects. Process it.
- var fields = widget.settings.element.serializeArray();
- $.each(fields, function(i, field) {
- // Put the values into flat object properties that PHP will parse into an array server-side.
- // (Unfortunately jQuery doesn't do this)
- params['settings[' + field.name + ']'] = field.value;
- });
-
- // Things get messy here.
- // @todo Refactor to use currentState and targetedState properties to determine what needs
- // to be done to get to any desired state on any UI or AJAX event โ since these don't always
- // match.
- // E.g. When a user starts a new UI event before the Ajax event handler from a previous
- // UI event gets invoked.
-
- // Hide the settings first of all.
- widget.toggleSettings();
- // Save the real settings element so that we can restore the reference later.
- var settingsElement = widget.settings.element;
- // Empty the settings form.
- widget.settings.innerElement.empty();
- initThrobber();
- // So that showSettings() and hideSettings() can do SOMETHING, without showing the empty settings form.
- widget.settings.element = widget.throbber.hide();
- widget.settings.ready = false;
-
- // Save the settings to the server.
- $.extend(params, opts.ajaxCallbacks.widgetSettings.data, { id: widget.id });
- $.post(opts.ajaxCallbacks.widgetSettings.url, params, function(response, status) {
- // Merge the response into widget.settings.
- $.extend(widget.settings, response);
- // Restore the reference to the real settings element.
- widget.settings.element = settingsElement;
- // Make sure the settings form is empty and add the updated settings form.
- widget.settings.innerElement.empty().append(widget.settings.markup);
- widget.settings.ready = true;
-
- // Did the user already jump back into settings-display mode before we could finish reloading the settings form?
- if (widget.settings.displayed) {
- // Ooops! We had better take care of hiding the throbber and showing the settings form then.
- widget.throbber.hide();
- widget.showSettings();
- invokeCallback(opts.widgetCallbacks.saveSettings, dashboard);
- }
- }, 'json');
-
- // Don't let form submittal bubble up.
- return false;
- };
-
- widget.enterFullscreen = function() {
- // Make sure the widget actually supports full screen mode.
- if (widget.fullscreenUrl) {
- CRM.loadPage(widget.fullscreenUrl);
- }
- };
-
- // Adds controls to a widget. id is for internal use and image file name in images/dashboard/ (a .gif).
- widget.addControl = function(id, control) {
- var markup = '<a class="crm-i ' + control.icon + '" alt="' + control.description + '" title="' + control.description + '" aria-hidden="true"></a>';
- control.element = $(markup).prependTo($('.widget-controls', widget.element)).click(control.callback);
- };
-
- // Fetch remote content.
- widget.reloadContent = function() {
- // If minimized, we'll reload later
- if (widget.minimized) {
- widget.contentLoaded = false;
- widget.lastLoaded = 0;
- } else {
- CRM.loadPage(widget.url, {target: widget.contentElement});
- }
- };
-
- // Removes the widget from the dashboard, and saves columns.
- widget.remove = function() {
- invokeCallback(opts.widgetCallbacks.remove, widget);
- widget.element.fadeOut(opts.animationSpeed, function() {
- $(this).remove();
- delete(dashboard.widgets[widget.id]);
- dashboard.saveColumns(false);
- });
- CRM.alert(
- ts('You can re-add it by clicking the "Configure Your Dashboard" button.'),
- ts('"%1" Removed', {1: _.escape(widget.title)}),
- 'success'
- );
- };
-
- widget.cacheIsFresh = function() {
- return (((widget.cacheMinutes * 60000 + widget.lastLoaded) > $.now()) && widget.content);
- };
-
- /**
- * Public properties of widget.
- */
-
- // Default controls. External script can add more with widget.addControls()
- widget.controls = {
- settings: {
- description: ts('Configure this dashlet'),
- callback: widget.toggleSettings,
- icon: 'fa-wrench'
- },
- minimize: {
- description: widget.minimized ? ts('Expand') : ts('Collapse'),
- callback: widget.toggleMinimize,
- icon: widget.minimized ? 'fa-caret-right' : 'fa-caret-down'
- },
- fullscreen: {
- description: ts('View fullscreen'),
- callback: widget.enterFullscreen,
- icon: 'fa-expand'
- },
- close: {
- description: ts('Remove from dashboard'),
- callback: widget.remove,
- icon: 'fa-times'
- }
- };
- widget.contentLoaded = false;
-
- init();
- return widget;
-
- /**
- * Private methods of widget.
- */
-
- function loadContent() {
- var loadFromCache = widget.cacheIsFresh();
- if (loadFromCache) {
- widget.contentElement.html(widget.content).trigger('crmLoad', widget);
- }
- widget.contentElement.off('crmLoad').on('crmLoad', function(event, data) {
- if ($(event.target).is(widget.contentElement)) {
- widget.content = data.content;
- // Cache for one day
- widget.lastLoaded = $.now();
- saveLocalCache();
- invokeCallback(opts.widgetCallbacks.get, widget);
- }
- });
- if (!loadFromCache) {
- widget.reloadContent();
- }
- widget.contentLoaded = true;
- }
-
- // Build widget & load content.
- function init() {
- // Delete controls that don't apply to this widget.
- if (!widget.settings) {
- delete widget.controls.settings;
- widget.settings = {};
- }
- if (!widget.fullscreenUrl) {
- delete widget.controls.fullscreen;
- }
- var cssClass = 'widget-' + widget.name.replace('/', '-');
- widget.element.attr('id', 'widget-' + widget.id).addClass(cssClass);
- // Build and add the widget's DOM element.
- $(widget.element).append(widgetHTML());
- // Save the content element so that external scripts can reload it easily.
- widget.contentElement = $('.widget-content', widget.element);
- $.each(widget.controls, widget.addControl);
-
- if (widget.minimized) {
- widget.contentElement.hide();
- } else {
- loadContent();
- }
- }
-
- // Builds inner HTML for widgets.
- function widgetHTML() {
- var html = '';
- html += '<div class="widget-wrapper">';
- html += ' <div class="widget-controls"><h3 class="widget-header">' + _.escape(widget.title) + '</h3></div>';
- html += ' <div class="widget-content"></div>';
- html += '</div>';
- return html;
- }
-
- // Initializes a widgets settings pane.
- function initSettings() {
- // Overwrite widget.settings (boolean).
- initThrobber();
- widget.settings = {
- element: widget.throbber.show(),
- ready: false
- };
-
- // Get the settings markup and script executables for this widget.
- var params = $.extend({}, opts.ajaxCallbacks.widgetSettings.data, { id: widget.id });
- $.getJSON(opts.ajaxCallbacks.widgetSettings.url, params, function(response, status) {
- $.extend(widget.settings, response);
- // Build and add the settings form to the DOM. Bind the form's submit event handler/callback.
- widget.settings.element = $(widgetSettingsHTML()).appendTo($('.widget-wrapper', widget.element)).submit(widget.saveSettings);
- // Bind the cancel button's event handler too.
- widget.settings.cancelButton = $('.widget-settings-cancel', widget.settings.element).click(cancelEditSettings);
- // Build and add the inner form elements from the HTML markup provided in the AJAX data.
- widget.settings.innerElement = $('.widget-settings-inner', widget.settings.element).append(widget.settings.markup);
- widget.settings.ready = true;
-
- if (widget.settings.displayed) {
- // If the user hasn't clicked away from the settings pane, then display the form.
- widget.throbber.hide();
- widget.showSettings();
- }
-
- getJavascript(widget.settings.initScript);
- });
- }
-
- // Builds HTML for widget settings forms.
- function widgetSettingsHTML() {
- var html = '';
- html += '<form class="widget-settings">';
- html += ' <div class="widget-settings-inner"></div>';
- html += ' <div class="widget-settings-buttons">';
- html += ' <button id="' + widget.id + '-settings-save" class="widget-settings-save" type="submit">Save</button>';
- html += ' <button id="' + widget.id + '-settings-cancel" class="widget-settings-cancel" type="submit">Cancel</button>';
- html += ' </div>';
- html += '</form>';
- return html;
- }
-
- // Initializes a generic widget content throbber, for use by settings form and external scripts.
- function initThrobber() {
- if (!widget.throbber) {
- widget.throbber = $(opts.throbberMarkup).appendTo($('.widget-wrapper', widget.element));
- }
- }
-
- // Event handler/callback for cancel button clicks.
- // @todo test this gets caught by all browsers when the cancel button is 'clicked' via the keyboard.
- function cancelEditSettings() {
- widget.toggleSettings();
- return false;
- }
-
- // Helper function to execute external script on the server.
- // @todo It would be nice to provide some context to the script. How?
- function getJavascript(url) {
- if (url) {
- $.getScript(url);
- }
- }
- }
- };
-
- // Public static properties of dashboard. Default settings.
- $.fn.dashboard.defaults = {
- columns: 2,
- emptyPlaceholderInner: '',
- throbberMarkup: '',
- animationSpeed: 200,
- callbacks: {},
- widgetCallbacks: {}
- };
-
- // Default widget settings.
- $.fn.dashboard.widget = {
- defaults: {
- minimized: false,
- content: null,
- lastLoaded: 0,
- settings: false
- // id, url, fullscreenUrl, title, name, cacheMinutes
- }
- };
-})(jQuery, CRM._);
| and copyright information, see https://civicrm.org/licensing |
+--------------------------------------------------------------------+
*}
-{include file="CRM/common/dashboard.tpl"}
{include file="CRM/common/chart.tpl"}
{* Alerts for critical configuration settings. *}
{$communityMessages}
-<div class="crm-submit-buttons crm-dashboard-controls">
-<a href="#" id="crm-dashboard-configure" class="crm-hover-button show-add">
- <i class="crm-i fa-wrench" aria-hidden="true"></i> {ts}Configure Your Dashboard{/ts}
-</a>
-
-<a style="float:right;" href="#" class="crm-hover-button show-refresh" style="margin-left: 6px;">
- <i class="crm-i fa-refresh" aria-hidden="true"></i> {ts}Refresh Dashboard Data{/ts}
-</a>
-
-</div>
<div class="clear"></div>
<div class="crm-block crm-content-block">
-{* Welcome message appears when there are no active dashlets for the current user. *}
-<div id="empty-message" class='hiddenElement'>
- <div class="status">
- <div class="font-size12pt bold">{ts}Welcome to your Home Dashboard{/ts}</div>
- <div class="display-block">
- {ts}Your dashboard provides a one-screen view of the data that's most important to you. Graphical or tabular data is pulled from the reports you select, and is displayed in 'dashlets' (sections of the dashboard).{/ts} {help id="id-dash_welcome" file="CRM/Contact/Page/Dashboard.hlp"}
- </div>
- </div>
-</div>
-<div id="configure-dashlet" class='hiddenElement' style="min-height: 20em;"></div>
-<div id="civicrm-dashboard">
- {* You can put anything you like here. jQuery.dashboard() will remove it. *}
- <noscript>{ts}Javascript must be enabled in your browser in order to use the dashboard features.{/ts}</noscript>
-</div>
-<div class="clear"></div>
-{literal}
-<script type="text/javascript">
- CRM.$(function($) {
- $('#crm-dashboard-configure').click(function(e) {
- e.preventDefault();
- $(this).hide();
- if ($("#empty-message").is(':visible')) {
- $("#empty-message").fadeOut(400);
- }
- $("#civicrm-dashboard").fadeOut(400, function() {
- $(".crm-dashboard-controls").hide();
- $("#configure-dashlet").fadeIn(400);
- });
- CRM.loadPage(CRM.url('civicrm/dashlet', 'reset=1'), {target: $("#configure-dashlet")});
- });
- });
-</script>
-{/literal}
+ <div ng-app="crmDashboard">
+ <crm-dashboard></crm-dashboard>
+ </div>
+
</div>
| and copyright information, see https://civicrm.org/licensing |
+--------------------------------------------------------------------+
*}
-{htxt id="id-dash_welcome-title"}
- {ts}Dashboard{/ts}
-{/htxt}
-{htxt id="id-dash_welcome"}
-<p>
- {ts}To include a dashlet report click "Configure Dashboard" and drag the desired dashlet from the "Available Dashlets" block to one of the columns (use the right column for a better fit).{/ts}
-</p>
-<p>
- {ts}To add reports to your dashboard:{/ts}
- <ul>
- <li>{ts}Navigate to a report instance and verify the report filters and display type (Tabular, Bar or Pie Chart). OR create a new report instance from the Reports menu ยป Create Reports from Templates.{/ts}</li>
- <li>{ts}Expand the 'Report Settings' section, and click 'Include Report on Dashboard'.{/ts}</li>
- <li>{ts}Click 'Update Report'.{/ts}</li>
- <li>{ts}Repeat for each report you want to include on your dashboard.{/ts}</li>
- <li>{ts}Navigate back to the 'Home' dashboard and click 'Configure Dashboard' again.{/ts}</li>
- <li>{ts}You can now drag the reports onto the right or left columns and click 'Done' to save your layout.{/ts}</li>
- </ul>
-</p>
-{/htxt}
-{htxt id="id-dash_configure-title"}
- {ts}Configuration{/ts}
-{/htxt}
-{htxt id="id-dash_configure"}
+{htxt id="dash_configure"}
<p>
{ts}To include a dashlet report drag it from the "Available Dashlets" block to one of the two columns (use the right column for a better fit).{/ts}
</p>
{ts}To add reports to your dashboard:{/ts}
<ul>
<li>{ts}Navigate to a report instance and verify the report filters and display type (Tabular, Bar or Pie Chart). OR create a new report instance from the Reports menu » Create Reports from Templates.{/ts}</li>
- <li>{ts}Expand the 'Report Settings' section, and click 'Include Report on Dashboard'.{/ts}</li>
- <li>{ts}Click 'Update Report'.{/ts}</li>
- <li>{ts}Repeat for each report your want to include on your dashboard.{/ts}</li>
- <li>{ts}Navigate back to the 'Home' dashboard and click 'Configure Dashboard' again.{/ts}</li>
- <li>{ts}You can now drag the reports onto the right or left columns and click 'Done' to save your layout.{/ts}</li>
+ <li>{ts}Open the 'Access' tab, and click the 'Available for Dashboard' checkbox.{/ts}</li>
+ <li>{ts}Click 'Save' from the actions menu.{/ts}</li>
</ul>
</p>
-{if $params.admin}
- <p>
- {ts}If you want to remove dashlets from the set of 'Available Dashlets' you can click the 'x' in the right corner of any dashlet. This will also remove the dashlet from any users' dashboards that currently include it. You can make a dashlet available again by checking the 'Available for Dashboard' box in the Report Settings of the associated report instance.{/ts}
- </p>
-{/if}
{/htxt}
+++ /dev/null
-{*
- +--------------------------------------------------------------------+
- | Copyright CiviCRM LLC. All rights reserved. |
- | |
- | This work is published under the GNU AGPLv3 license with some |
- | permitted exceptions and without any warranty. For full license |
- | and copyright information, see https://civicrm.org/licensing |
- +--------------------------------------------------------------------+
-*}
- <div class="crm-submit-buttons">{crmButton p="civicrm/dashboard" q="reset=1" icon="check"}{ts}Done{/ts}{/crmButton}</div>
- <div id="help" style="padding: 1em;">
- {ts}Available dashboard elements - dashlets - are displayed in the dark gray top bar. Drag and drop dashlets onto the left or right columns below to add them to your dashboard. Changes are automatically saved. Click 'Done' to return to the normal dashboard view.{/ts}
- {help id="id-dash_configure" file="CRM/Contact/Page/Dashboard.hlp" admin=$admin}
- </div><br/>
- <div class="dashlets-header">{ts}Available Dashlets{/ts}</div>
- <div id="available-dashlets" class="dash-column">
- {foreach from=$availableDashlets item=row key=dashID}
- <div class="portlet">
- <div class="portlet-header" id="{$dashID}">{$row.label}{if $admin and !$row.is_reserved} <a class="crm-i fa-times delete-dashlet" aria-hidden="true"></a>{/if}</div>
- </div>
- {/foreach}
- </div>
- <br/>
- <div class="clear"></div>
- <div id="dashlets-header-col-0" class="dashlets-header">{ts}Left Column{/ts}</div>
- <div id="dashlets-header-col-1" class="dashlets-header">{ts}Right Column{/ts}</div>
- <div id="existing-dashlets-col-0" class="dash-column">
- {foreach from=$contactDashlets.0 item=row key=dashID}
- <div class="portlet">
- <div class="portlet-header" id="{$dashID}">{$row.label}{if $admin and !$row.is_reserved} <a class="crm-i fa-times delete-dashlet" aria-hidden="true"></a>{/if}</div>
- </div>
- {/foreach}
- </div>
-
- <div id="existing-dashlets-col-1" class="dash-column">
- {foreach from=$contactDashlets.1 item=row key=dashID}
- <div class="portlet">
- <div class="portlet-header" id="{$dashID}">{$row.label}{if $admin and !$row.is_reserved} <a class="crm-i fa-times delete-dashlet" aria-hidden="true"></a>{/if}</div>
- </div>
- {/foreach}
- </div>
-
- <div class="clear"></div>
-
-{literal}
-<script type="text/javascript">
- CRM.$(function($) {
- var currentReSortEvent;
- $(".dash-column").sortable({
- connectWith: '.dash-column',
- update: saveSorting
- });
-
- $(".portlet").addClass("ui-widget ui-widget-content ui-helper-clearfix ui-corner-all")
- .find(".portlet-header")
- .addClass("ui-widget-header ui-corner-all")
- .end()
- .find(".portlet-content");
-
- $(".dash-column").disableSelection();
-
- function saveSorting(e, ui) {
- // this is to prevent double post call
- if (!currentReSortEvent || e.originalEvent != currentReSortEvent) {
- currentReSortEvent = e.originalEvent;
-
- // Build a list of params to post to the server.
- var params = {};
-
- // post each columns
- dashletColumns = Array();
-
- // build post params
- $('div[id^=existing-dashlets-col-]').each( function( i ) {
- $(this).find('.portlet-header').each( function( j ) {
- var elementID = this.id;
- var idState = elementID.split('-');
- params['columns[' + i + '][' + idState[0] + ']'] = idState[1];
- });
- });
-
- // post to server
- var postUrl = {/literal}"{crmURL p='civicrm/ajax/dashboard' h=0 }"{literal};
- params['op'] = 'save_columns';
- params['key'] = {/literal}"{crmKey name='civicrm/ajax/dashboard'}"{literal};
- CRM.status({}, $.post(postUrl, params));
- }
- }
-
- $('.delete-dashlet').click( function( ) {
- var $dashlet = $(this).closest('.portlet-header');
- CRM.confirm({
- title: {/literal}'{ts escape="js"}Remove Permanently?{/ts}'{literal},
- message: {/literal}'{ts escape="js"}Do you want to remove this dashlet as an "Available Dashlet", AND delete it from all user dashboards?{/ts}'{literal}
- })
- .on('crmConfirm:yes', function() {
- var dashletID = $dashlet.attr('id');
- var idState = dashletID.split('-');
-
- // Build a list of params to post to the server.
- var params = {dashlet_id: idState[0]};
-
- // delete dashlet
- var postUrl = {/literal}"{crmURL p='civicrm/ajax/dashboard' h=0 }"{literal};
- params['op'] = 'delete_dashlet';
- params['key'] = {/literal}"{crmKey name='civicrm/ajax/dashboard'}"{literal};
- CRM.status({}, $.post(postUrl, params));
- $dashlet.parent().fadeOut('fast', function() {
- $(this).remove();
- });
- });
- });
- });
-</script>
-{/literal}
+++ /dev/null
-{*
- +--------------------------------------------------------------------+
- | Copyright CiviCRM LLC. All rights reserved. |
- | |
- | This work is published under the GNU AGPLv3 license with some |
- | permitted exceptions and without any warranty. For full license |
- | and copyright information, see https://civicrm.org/licensing |
- +--------------------------------------------------------------------+
-*}
-{literal}
-<script type="text/javascript">
-
-CRM.$(function($) {
- // The set of options we can use to initialize jQuery.dashboard().
- var options = {
-
- widgetsByColumn: {/literal}{$contactDashlets|@json_encode}{literal},
-
- // These define the urls and data objects used for all of the ajax requests to the server.
- ajaxCallbacks: {
-
- // jQuery.dashboard() POSTs the widget-to-column settings here.
- // The 'columns' property of data is reserved for the widget-to-columns settings:
- // An array (keyed by zero-indexed column ID), of arrays (keyed by widget ID)
- // of ints; 1 if the widget is minimized. 0 if not.
- saveColumns: {
- url: {/literal}'{crmURL p='civicrm/ajax/dashboard' h=0 }'{literal},
- data: {
- // columns: array(0 => array(widgetId => isMinimized, ...), ...),
- op: 'save_columns', key: {/literal}"{crmKey name='civicrm/ajax/dashboard'}"{literal}
- }
- },
-
- // jQuery.dashboard() GETs a widget's settings object and POST's a users submitted
- // settings back to the server. The return, in both cases, is an associative
- // array with the new settings markup and other info:
- //
- // Required properties:
- // * markup: HTML string. The inner HTML of the settings form. jQuery.dashboard()
- // provides the Save and Cancel buttons and wrapping <form> element. Can include
- // <input>s of any standard type and <select>s, nested in <div>s etc.
- //
- // Server-side executable script callbacks (See documentation for
- // ajaxCallbacks.getWidgets):
- // * initScript: Called when widget settings are initialising.
- // * script: Called when switching into settings mode. Executed every time
- // the widget goes into settings-edit mode.
- //
- // The 'id' property of data is reserved for the widget ID.
- // The 'settings' property of data is reserved for the user-submitted settings.
- // An array (keyed by the name="" attributes of <input>s), of <input> values.
- widgetSettings: {
- url: {/literal}'{crmURL p='civicrm/ajax/dashboard' h=0 }'{literal},
- data: {
- // id: widgetId,
- // settings: array(name => value, ...),
- op: 'widget_settings', key: {/literal}"{crmKey name='civicrm/ajax/dashboard'}"{literal}
- }
- }
- }
-
- };
-
- var dashboard = $('#civicrm-dashboard')
- .on('mouseover', '.widget-header', function() {
- $(this).closest('.widget-wrapper').addClass('db-hover-handle');
- })
- .on('mouseout', '.widget-header', function() {
- $(this).closest('.widget-wrapper').removeClass('db-hover-handle');
- })
- .dashboard(options);
-
-
- $('.crm-hover-button.show-refresh').click(function(e) {
- e.preventDefault();
- $.each(dashboard.widgets, function(id, widget) {
- widget.reloadContent();
- });
- });
-
-});
-
-</script>
-{/literal}
+++ /dev/null
-<?php
-/*
- +--------------------------------------------------------------------+
- | Copyright CiviCRM LLC. All rights reserved. |
- | |
- | This work is published under the GNU AGPLv3 license with some |
- | permitted exceptions and without any warranty. For full license |
- | and copyright information, see https://civicrm.org/licensing |
- +--------------------------------------------------------------------+
- */
-
-/**
- * Test class for Dashboard BAO
- *
- * @package CiviCRM
- * @group headless
- */
-class CRM_Core_BAO_DashboardTest extends CiviUnitTestCase {
-
- /**
- * Sets up the fixture, for example, opens a network connection.
- *
- * This method is called before a test is executed.
- */
- protected function setUp() {
- parent::setUp();
- }
-
- /**
- * @dataProvider parseUrlTestData
- * @param $input
- * @param $expectedResult
- */
- public function testParseUrl($input, $expectedResult) {
- $this->assertEquals($expectedResult, CRM_Core_BAO_Dashboard::parseUrl($input));
- }
-
- public function parseUrlTestData() {
- return [
- ['https://foo.bar', 'https://foo.bar'],
- ['civicrm/path?reset=1&unit=test', CRM_Utils_System::url('civicrm/path', 'reset=1&unit=test', FALSE, NULL, FALSE)],
- ];
- }
-
-}
<required>true</required>
<add>4.7</add>
</field>
+ <field>
+ <name>directive</name>
+ <type>varchar</type>
+ <title>Angular directive</title>
+ <length>255</length>
+ <comment>Element name of angular directive to invoke (lowercase hyphenated format)</comment>
+ <add>5.33</add>
+ </field>
</table>