Rewrite jquery.dashboard.js as Angular app
authorColeman Watts <coleman@civicrm.org>
Fri, 13 Nov 2020 18:23:41 +0000 (13:23 -0500)
committerColeman Watts <coleman@civicrm.org>
Mon, 16 Nov 2020 12:11:22 +0000 (07:11 -0500)
New home dashboard written with Angular + APIv4.
Functionality is mostly unchanged.
The motivation for this is to support afforms embedded within dashboard dashlets.

29 files changed:
CRM/Contact/Page/AJAX.php
CRM/Contact/Page/DashBoard.php
CRM/Contact/Page/Dashlet.php [deleted file]
CRM/Core/BAO/Dashboard.php
CRM/Core/DAO/Dashboard.php
CRM/Core/xml/Menu/Contact.xml
CRM/Upgrade/Incremental/php/FiveThirtyThree.php
CRM/Utils/Hook.php
Civi/Angular/Manager.php
ang/crmDashboard.ang.php [new file with mode: 0644]
ang/crmDashboard.js [new file with mode: 0644]
ang/crmDashboard/Dashboard.html [new file with mode: 0644]
ang/crmDashboard/Dashlet.html [new file with mode: 0644]
ang/crmDashboard/FullscreenDialog.html [new file with mode: 0644]
ang/crmDashboard/InactiveDashlet.html [new file with mode: 0644]
ang/crmDashboard/crmDashboard.component.js [new file with mode: 0644]
ang/crmDashboard/crmDashlet.component.js [new file with mode: 0644]
ang/crmDashboard/crmInactiveDashlet.component.js [new file with mode: 0644]
api/v3/DashboardContact.php
css/civicrm.css
css/dashboard.css
ext/search/css/search.css
js/jquery/jquery.dashboard.js [deleted file]
templates/CRM/Contact/Page/DashBoardDashlet.tpl
templates/CRM/Contact/Page/Dashboard.hlp
templates/CRM/Contact/Page/Dashlet.tpl [deleted file]
templates/CRM/common/dashboard.tpl [deleted file]
tests/phpunit/CRM/Core/BAO/DashboardTest.php [deleted file]
xml/schema/Core/Dashboard.xml

index be78f303a9d7b9732c5a4718f6a112a6045e78a6..74aa9b6e2df96f5138908ed73c8ac443fde43c0b 100644 (file)
@@ -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.
    */
index dd2bda097f177d6019425e24cbc3ac0a330b1596..07f6c78ea3f1177b03ff1ffecd626e8b72acb8d3 100644 (file)
@@ -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 (file)
index 016c178..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-<?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();
-  }
-
-}
index eb9d0b00633b2bc16ab8fb2f9f72de52e620505e..6246590ca972f084b247227b46519880cba4a748 100644 (file)
 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;
-  }
-
 }
index 45282f473195d01a741097279e5156d2502e7a9e..5644b1e62daea63425aeb91e46887110385ae478 100644 (file)
@@ -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']);
     }
index e49b57236dda52787551c60eb90b626bdb330f2f..361bcb969578c54de910e5f046765ca3840883dc 100644 (file)
      <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>
index f7bc1ab8095e4d421109fed7f7c411a60a26f3ff..8d67bd4c0ff59baf17c7444b01916c3c995b7458 100644 (file)
@@ -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)'");
+  }
 
 }
index bc905d21baa3f7d259071d8e8bcf72140d037dba..1833851b91b7680c0b37e307b37d65e42960a9ac 100644 (file)
@@ -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
index ab9600175a885e120fe515f22d32e0439102b057..5724bdab68d0c30cebdbf941d7d670f9f6569e13 100644 (file)
@@ -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 (file)
index 0000000..65f312a
--- /dev/null
@@ -0,0 +1,17 @@
+<?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'],
+];
diff --git a/ang/crmDashboard.js b/ang/crmDashboard.js
new file mode 100644 (file)
index 0000000..43233a2
--- /dev/null
@@ -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 (file)
index 0000000..8b500c1
--- /dev/null
@@ -0,0 +1,26 @@
+<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>
diff --git a/ang/crmDashboard/Dashlet.html b/ang/crmDashboard/Dashlet.html
new file mode 100644 (file)
index 0000000..738474b
--- /dev/null
@@ -0,0 +1,10 @@
+<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>
diff --git a/ang/crmDashboard/FullscreenDialog.html b/ang/crmDashboard/FullscreenDialog.html
new file mode 100644 (file)
index 0000000..f96fd82
--- /dev/null
@@ -0,0 +1 @@
+<crm-dashlet dashlet="model" is-fullscreen="true"></crm-dashlet>
diff --git a/ang/crmDashboard/InactiveDashlet.html b/ang/crmDashboard/InactiveDashlet.html
new file mode 100644 (file)
index 0000000..37f5f37
--- /dev/null
@@ -0,0 +1,4 @@
+<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>
diff --git a/ang/crmDashboard/crmDashboard.component.js b/ang/crmDashboard/crmDashboard.component.js
new file mode 100644 (file)
index 0000000..eb229b3
--- /dev/null
@@ -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 (file)
index 0000000..5568f4d
--- /dev/null
@@ -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 (file)
index 0000000..1961f55
--- /dev/null
@@ -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._);
index 1467d24f226247d959a5145c8dec3c302b7f693b..1662b934d2b52309a0892e2a757dcd5116da8e51 100644 (file)
  * @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.
  *
index 1a685a272970b4b5891a24f0334d692e7d52d492..6de2e31d9a90264c3b68ba038db3e20aaabd1b4b 100644 (file)
    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;
 }
index 7f783555fd5344f244c1f67dd602bd1cd116b641..88296b1be7a8e05443fe7cae60de1047ec1421c0 100644 (file)
-/**
-* @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;
-  }
-}
index 2353aa2bd666bb6b777ac27f2e974358fadd3982..9ade04fef8740393f25146ab74a7cfe6b817674e 100644 (file)
@@ -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 (file)
index 00c6526..0000000
+++ /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 = '<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._);
index 54b26340775944a40c81332c29216753b6984278..e16653417ecab88c8aecd4501205a9b83fc6a42d 100644 (file)
@@ -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}
-<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>
index 446c5f3d7318ece4f75972c459070f1ea47352f4..f72c9b27c618bd6d1624a2a306175315f00bb9cc 100644 (file)
@@ -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"}
-<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 &raquo; 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}
diff --git a/templates/CRM/Contact/Page/Dashlet.tpl b/templates/CRM/Contact/Page/Dashlet.tpl
deleted file mode 100644 (file)
index fa1dcfa..0000000
+++ /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       |
- +--------------------------------------------------------------------+
-*}
-    <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}&nbsp;<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}&nbsp;<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}&nbsp;<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}
diff --git a/templates/CRM/common/dashboard.tpl b/templates/CRM/common/dashboard.tpl
deleted file mode 100644 (file)
index 75de0a5..0000000
+++ /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}
-<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}
diff --git a/tests/phpunit/CRM/Core/BAO/DashboardTest.php b/tests/phpunit/CRM/Core/BAO/DashboardTest.php
deleted file mode 100644 (file)
index dddc514..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-<?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)],
-    ];
-  }
-
-}
index 6b34614dbcc23802de5095f988a386ae314b628b..39373d714b21d2b9b1095ee53f3b3e0dcac644cc 100644 (file)
     <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>