From: Coleman Watts
Date: Fri, 13 Nov 2020 18:23:41 +0000 (-0500)
Subject: Rewrite jquery.dashboard.js as Angular app
X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=f263929f90c38c9c4b1e1c02c0b7c4ed56ea68d8;p=civicrm-core.git
Rewrite jquery.dashboard.js as Angular app
New home dashboard written with Angular + APIv4.
Functionality is mostly unchanged.
The motivation for this is to support afforms embedded within dashboard dashlets.
---
diff --git a/CRM/Contact/Page/AJAX.php b/CRM/Contact/Page/AJAX.php
index be78f303a9..74aa9b6e2d 100644
--- a/CRM/Contact/Page/AJAX.php
+++ b/CRM/Contact/Page/AJAX.php
@@ -499,23 +499,6 @@ LIMIT {$offset}, {$rowCount}
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.
*/
diff --git a/CRM/Contact/Page/DashBoard.php b/CRM/Contact/Page/DashBoard.php
index dd2bda097f..07f6c78ea3 100644
--- a/CRM/Contact/Page/DashBoard.php
+++ b/CRM/Contact/Page/DashBoard.php
@@ -24,12 +24,6 @@ class CRM_Contact_Page_DashBoard extends CRM_Core_Page {
* 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();
@@ -50,7 +44,56 @@ class CRM_Contact_Page_DashBoard extends CRM_Core_Page {
}
}
+ $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(),
+ ];
+ }
+
}
diff --git a/CRM/Contact/Page/Dashlet.php b/CRM/Contact/Page/Dashlet.php
deleted file mode 100644
index 016c178ae2..0000000000
--- a/CRM/Contact/Page/Dashlet.php
+++ /dev/null
@@ -1,61 +0,0 @@
-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();
- }
-
-}
diff --git a/CRM/Core/BAO/Dashboard.php b/CRM/Core/BAO/Dashboard.php
index eb9d0b0063..6246590ca9 100644
--- a/CRM/Core/BAO/Dashboard.php
+++ b/CRM/Core/BAO/Dashboard.php
@@ -21,13 +21,11 @@
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';
@@ -38,140 +36,52 @@ class CRM_Core_BAO_Dashboard extends CRM_Core_DAO_Dashboard {
}
/**
- * 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) {
@@ -180,63 +90,29 @@ class CRM_Core_BAO_Dashboard extends CRM_Core_DAO_Dashboard {
'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;
@@ -301,81 +177,6 @@ class CRM_Core_BAO_Dashboard extends CRM_Core_DAO_Dashboard {
}
}
- /**
- * 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.
*
@@ -472,38 +273,4 @@ class CRM_Core_BAO_Dashboard extends CRM_Core_DAO_Dashboard {
}
}
- /**
- * @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;
- }
-
}
diff --git a/CRM/Core/DAO/Dashboard.php b/CRM/Core/DAO/Dashboard.php
index 45282f4731..5644b1e62d 100644
--- a/CRM/Core/DAO/Dashboard.php
+++ b/CRM/Core/DAO/Dashboard.php
@@ -6,7 +6,7 @@
*
* Generated from xml/schema/CRM/Core/Dashboard.xml
* DO NOT EDIT. Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:d1da8f66f209f70a5e8a1fd440bb4694)
+ * (GenCodeChecksum:1f32e4ff906a25d432cc28598e388f60)
*/
/**
@@ -105,6 +105,13 @@ class CRM_Core_DAO_Dashboard extends CRM_Core_DAO {
*/
public $cache_minutes;
+ /**
+ * Element name of angular directive to invoke (lowercase hyphenated format)
+ *
+ * @var string
+ */
+ public $directive;
+
/**
* Class constructor.
*/
@@ -302,6 +309,20 @@ class CRM_Core_DAO_Dashboard extends CRM_Core_DAO {
'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']);
}
diff --git a/CRM/Core/xml/Menu/Contact.xml b/CRM/Core/xml/Menu/Contact.xml
index e49b57236d..361bcb9695 100644
--- a/CRM/Core/xml/Menu/Contact.xml
+++ b/CRM/Core/xml/Menu/Contact.xml
@@ -19,14 +19,6 @@
access CiviCRM
0
- -
-
civicrm/dashlet
- CiviCRM Dashlets
- 1
- CRM_Contact_Page_Dashlet
- access CiviCRM
- 1
-
-
civicrm/contact/search
Find Contacts
@@ -294,12 +286,6 @@
CRM_Contact_Page_AJAX::buildSubTypes
access CiviCRM
--
-
civicrm/ajax/dashboard
- CRM_Contact_Page_AJAX::dashboard
- access CiviCRM
- 3
-
-
civicrm/ajax/signature
CRM_Contact_Page_AJAX::getSignature
diff --git a/CRM/Upgrade/Incremental/php/FiveThirtyThree.php b/CRM/Upgrade/Incremental/php/FiveThirtyThree.php
index f7bc1ab809..8d67bd4c0f 100644
--- a/CRM/Upgrade/Incremental/php/FiveThirtyThree.php
+++ b/CRM/Upgrade/Incremental/php/FiveThirtyThree.php
@@ -46,27 +46,14 @@ class CRM_Upgrade_Incremental_php_FiveThirtyThree extends CRM_Upgrade_Incrementa
// }
}
- /*
- * 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)'");
+ }
}
diff --git a/CRM/Utils/Hook.php b/CRM/Utils/Hook.php
index bc905d21ba..1833851b91 100644
--- a/CRM/Utils/Hook.php
+++ b/CRM/Utils/Hook.php
@@ -2208,7 +2208,7 @@ abstract class CRM_Utils_Hook {
}
/**
- * 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
diff --git a/Civi/Angular/Manager.php b/Civi/Angular/Manager.php
index ab9600175a..5724bdab68 100644
--- a/Civi/Angular/Manager.php
+++ b/Civi/Angular/Manager.php
@@ -121,6 +121,7 @@ class Manager {
$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());
diff --git a/ang/crmDashboard.ang.php b/ang/crmDashboard.ang.php
new file mode 100644
index 0000000000..65f312a7e2
--- /dev/null
+++ b/ang/crmDashboard.ang.php
@@ -0,0 +1,17 @@
+ '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'],
+];
diff --git a/ang/crmDashboard.js b/ang/crmDashboard.js
new file mode 100644
index 0000000000..43233a222f
--- /dev/null
+++ b/ang/crmDashboard.js
@@ -0,0 +1,6 @@
+(function(angular, $, _) {
+ angular.module('crmDashboard', CRM.angRequires('crmDashboard'));
+
+ angular.module('crmDashboard', CRM.angular.modules);
+
+})(angular, CRM.$, CRM._);
diff --git a/ang/crmDashboard/Dashboard.html b/ang/crmDashboard/Dashboard.html
new file mode 100644
index 0000000000..8b500c1d4a
--- /dev/null
+++ b/ang/crmDashboard/Dashboard.html
@@ -0,0 +1,26 @@
+
diff --git a/ang/crmDashboard/Dashlet.html b/ang/crmDashboard/Dashlet.html
new file mode 100644
index 0000000000..738474bcea
--- /dev/null
+++ b/ang/crmDashboard/Dashlet.html
@@ -0,0 +1,10 @@
+
+
diff --git a/ang/crmDashboard/FullscreenDialog.html b/ang/crmDashboard/FullscreenDialog.html
new file mode 100644
index 0000000000..f96fd82e59
--- /dev/null
+++ b/ang/crmDashboard/FullscreenDialog.html
@@ -0,0 +1 @@
+
diff --git a/ang/crmDashboard/InactiveDashlet.html b/ang/crmDashboard/InactiveDashlet.html
new file mode 100644
index 0000000000..37f5f37f89
--- /dev/null
+++ b/ang/crmDashboard/InactiveDashlet.html
@@ -0,0 +1,4 @@
+
diff --git a/ang/crmDashboard/crmDashboard.component.js b/ang/crmDashboard/crmDashboard.component.js
new file mode 100644
index 0000000000..eb229b3f33
--- /dev/null
+++ b/ang/crmDashboard/crmDashboard.component.js
@@ -0,0 +1,109 @@
+(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._);
diff --git a/ang/crmDashboard/crmDashlet.component.js b/ang/crmDashboard/crmDashlet.component.js
new file mode 100644
index 0000000000..5568f4d26f
--- /dev/null
+++ b/ang/crmDashboard/crmDashlet.component.js
@@ -0,0 +1,115 @@
+(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._);
diff --git a/ang/crmDashboard/crmInactiveDashlet.component.js b/ang/crmDashboard/crmInactiveDashlet.component.js
new file mode 100644
index 0000000000..1961f55447
--- /dev/null
+++ b/ang/crmDashboard/crmInactiveDashlet.component.js
@@ -0,0 +1,25 @@
+(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._);
diff --git a/api/v3/DashboardContact.php b/api/v3/DashboardContact.php
index 1467d24f22..1662b934d2 100644
--- a/api/v3/DashboardContact.php
+++ b/api/v3/DashboardContact.php
@@ -23,10 +23,6 @@
* @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');
}
@@ -54,24 +50,6 @@ function _civicrm_api3_dashboard_contact_create_spec(&$fields) {
$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.
*
diff --git a/css/civicrm.css b/css/civicrm.css
index 1a685a2729..6de2e31d9a 100644
--- a/css/civicrm.css
+++ b/css/civicrm.css
@@ -19,6 +19,27 @@
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;
}
diff --git a/css/dashboard.css b/css/dashboard.css
index 7f783555fd..88296b1be7 100644
--- a/css/dashboard.css
+++ b/css/dashboard.css
@@ -1,210 +1,92 @@
-/**
-* @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 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;
- }
-}
diff --git a/ext/search/css/search.css b/ext/search/css/search.css
index 2353aa2bd6..9ade04fef8 100644
--- a/ext/search/css/search.css
+++ b/ext/search/css/search.css
@@ -1,22 +1,3 @@
-.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;
}
diff --git a/js/jquery/jquery.dashboard.js b/js/jquery/jquery.dashboard.js
deleted file mode 100644
index 00c65260ec..0000000000
--- a/js/jquery/jquery.dashboard.js
+++ /dev/null
@@ -1,580 +0,0 @@
-// 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 = '' + opts.emptyPlaceholderInner + ' ';
-
- // 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: $('').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: $(' ').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 = ' ';
- 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 += '';
- 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 += '';
- 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._);
diff --git a/templates/CRM/Contact/Page/DashBoardDashlet.tpl b/templates/CRM/Contact/Page/DashBoardDashlet.tpl
index 54b2634077..e16653417e 100644
--- a/templates/CRM/Contact/Page/DashBoardDashlet.tpl
+++ b/templates/CRM/Contact/Page/DashBoardDashlet.tpl
@@ -7,54 +7,14 @@
| 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}
-
-{* Welcome message appears when there are no active dashlets for the current user. *}
-
-
-
{ts}Welcome to your Home Dashboard{/ts}
-
- {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"}
-
-
-
-
-
- {* You can put anything you like here. jQuery.dashboard() will remove it. *}
- {ts}Javascript must be enabled in your browser in order to use the dashboard features.{/ts}
-
-
-{literal}
-
-{/literal}
+
+
+
+
diff --git a/templates/CRM/Contact/Page/Dashboard.hlp b/templates/CRM/Contact/Page/Dashboard.hlp
index 446c5f3d73..f72c9b27c6 100644
--- a/templates/CRM/Contact/Page/Dashboard.hlp
+++ b/templates/CRM/Contact/Page/Dashboard.hlp
@@ -7,30 +7,8 @@
| and copyright information, see https://civicrm.org/licensing |
+--------------------------------------------------------------------+
*}
-{htxt id="id-dash_welcome-title"}
- {ts}Dashboard{/ts}
-{/htxt}
-{htxt id="id-dash_welcome"}
-
- {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}
-
-
- {ts}To add reports to your dashboard:{/ts}
-
- {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}
- {ts}Expand the 'Report Settings' section, and click 'Include Report on Dashboard'.{/ts}
- {ts}Click 'Update Report'.{/ts}
- {ts}Repeat for each report you want to include on your dashboard.{/ts}
- {ts}Navigate back to the 'Home' dashboard and click 'Configure Dashboard' again.{/ts}
- {ts}You can now drag the reports onto the right or left columns and click 'Done' to save your layout.{/ts}
-
-
-{/htxt}
-{htxt id="id-dash_configure-title"}
- {ts}Configuration{/ts}
-{/htxt}
-{htxt id="id-dash_configure"}
+{htxt id="dash_configure"}
{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}
@@ -38,16 +16,8 @@
{ts}To add reports to your dashboard:{/ts}
{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}
- {ts}Expand the 'Report Settings' section, and click 'Include Report on Dashboard'.{/ts}
- {ts}Click 'Update Report'.{/ts}
- {ts}Repeat for each report your want to include on your dashboard.{/ts}
- {ts}Navigate back to the 'Home' dashboard and click 'Configure Dashboard' again.{/ts}
- {ts}You can now drag the reports onto the right or left columns and click 'Done' to save your layout.{/ts}
+ {ts}Open the 'Access' tab, and click the 'Available for Dashboard' checkbox.{/ts}
+ {ts}Click 'Save' from the actions menu.{/ts}
-{if $params.admin}
-
- {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}
-
-{/if}
{/htxt}
diff --git a/templates/CRM/Contact/Page/Dashlet.tpl b/templates/CRM/Contact/Page/Dashlet.tpl
deleted file mode 100644
index fa1dcfa47c..0000000000
--- a/templates/CRM/Contact/Page/Dashlet.tpl
+++ /dev/null
@@ -1,115 +0,0 @@
-{*
- +--------------------------------------------------------------------+
- | 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 |
- +--------------------------------------------------------------------+
-*}
- {crmButton p="civicrm/dashboard" q="reset=1" icon="check"}{ts}Done{/ts}{/crmButton}
-
- {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}
-
-
-
- {foreach from=$availableDashlets item=row key=dashID}
-
-
-
- {/foreach}
-
-
-
-
-
-
- {foreach from=$contactDashlets.0 item=row key=dashID}
-
-
-
- {/foreach}
-
-
-
- {foreach from=$contactDashlets.1 item=row key=dashID}
-
-
-
- {/foreach}
-
-
-
-
-{literal}
-
-{/literal}
diff --git a/templates/CRM/common/dashboard.tpl b/templates/CRM/common/dashboard.tpl
deleted file mode 100644
index 75de0a5e81..0000000000
--- a/templates/CRM/common/dashboard.tpl
+++ /dev/null
@@ -1,84 +0,0 @@
-{*
- +--------------------------------------------------------------------+
- | 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}
-
-{/literal}
diff --git a/tests/phpunit/CRM/Core/BAO/DashboardTest.php b/tests/phpunit/CRM/Core/BAO/DashboardTest.php
deleted file mode 100644
index dddc5146b6..0000000000
--- a/tests/phpunit/CRM/Core/BAO/DashboardTest.php
+++ /dev/null
@@ -1,45 +0,0 @@
-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)],
- ];
- }
-
-}
diff --git a/xml/schema/Core/Dashboard.xml b/xml/schema/Core/Dashboard.xml
index 6b34614dbc..39373d714b 100644
--- a/xml/schema/Core/Dashboard.xml
+++ b/xml/schema/Core/Dashboard.xml
@@ -148,4 +148,12 @@
true
4.7
+
+ directive
+ varchar
+ Angular directive
+ 255
+ Element name of angular directive to invoke (lowercase hyphenated format)
+ 5.33
+