Search ext: Add searchDisplay and searchPage modules
authorColeman Watts <coleman@civicrm.org>
Tue, 20 Oct 2020 19:55:00 +0000 (15:55 -0400)
committerColeman Watts <coleman@civicrm.org>
Thu, 29 Oct 2020 23:48:28 +0000 (19:48 -0400)
40 files changed:
CRM/Contact/BAO/SavedSearch.php
ext/search/CRM/Search/Page/Admin.php
ext/search/CRM/Search/Page/Search.php [new file with mode: 0644]
ext/search/CRM/Search/Upgrader.php
ext/search/Civi/Search/Actions.php
ext/search/ang/crmSearchActions.ang.php [moved from ext/search/ang/searchActions.ang.php with 70% similarity]
ext/search/ang/crmSearchActions.module.js [moved from ext/search/ang/searchActions.module.js with 55% similarity]
ext/search/ang/crmSearchActions/SaveSmartGroup.ctrl.js [moved from ext/search/ang/searchActions/SaveSmartGroup.ctrl.js with 92% similarity]
ext/search/ang/crmSearchActions/crmSearchActionDelete.ctrl.js [moved from ext/search/ang/searchActions/crmSearchActionDelete.ctrl.js with 82% similarity]
ext/search/ang/crmSearchActions/crmSearchActionDelete.html [moved from ext/search/ang/searchActions/crmSearchActionDelete.html with 100% similarity]
ext/search/ang/crmSearchActions/crmSearchActionUpdate.ctrl.js [moved from ext/search/ang/searchActions/crmSearchActionUpdate.ctrl.js with 92% similarity]
ext/search/ang/crmSearchActions/crmSearchActionUpdate.html [moved from ext/search/ang/searchActions/crmSearchActionUpdate.html with 100% similarity]
ext/search/ang/crmSearchActions/crmSearchActions.component.js [moved from ext/search/ang/searchActions/crmSearchActions.component.js with 87% similarity]
ext/search/ang/crmSearchActions/crmSearchActions.html [moved from ext/search/ang/searchActions/crmSearchActions.html with 100% similarity]
ext/search/ang/crmSearchActions/saveSmartGroup.directive.js [moved from ext/search/ang/searchActions/saveSmartGroup.directive.js with 84% similarity]
ext/search/ang/crmSearchActions/saveSmartGroup.html [moved from ext/search/ang/searchActions/saveSmartGroup.html with 100% similarity]
ext/search/ang/crmSearchDisplay.ang.php [new file with mode: 0644]
ext/search/ang/crmSearchDisplay.module.js [new file with mode: 0644]
ext/search/ang/crmSearchDisplay/crmSearchDisplayTable.component.js [new file with mode: 0644]
ext/search/ang/crmSearchDisplay/crmSearchDisplayTable.html [new file with mode: 0644]
ext/search/ang/crmSearchPage.ang.php [new file with mode: 0644]
ext/search/ang/crmSearchPage.module.js [new file with mode: 0644]
ext/search/ang/crmSearchPage/display.html [new file with mode: 0644]
ext/search/ang/searchAdmin.ang.php
ext/search/ang/searchAdmin.module.js
ext/search/ang/searchAdmin/compose/criteria.html
ext/search/ang/searchAdmin/compose/results.html
ext/search/ang/searchAdmin/crmSearchAdmin.component.js
ext/search/ang/searchAdmin/crmSearchAdmin.html
ext/search/ang/searchAdmin/display.html [new file with mode: 0644]
ext/search/ang/searchAdmin/displays/searchAdminDisplayTable.component.js [new file with mode: 0644]
ext/search/ang/searchAdmin/displays/searchAdminDisplayTable.html [new file with mode: 0644]
ext/search/ang/searchAdmin/displays/table.html [new file with mode: 0644]
ext/search/ang/searchAdmin/group.html
ext/search/ang/searchAdmin/searchList.controller.js
ext/search/ang/searchAdmin/searchList.html
ext/search/ang/searchAdmin/tabs.html
ext/search/css/search.css
ext/search/templates/CRM/Search/Page/Search.tpl [new file with mode: 0644]
ext/search/xml/Menu/search.xml

index 0b5881513b5e74ed081569eb4bcbb439e1bd3d27..d5e4456720c3d1e230e943ddc5c3d03f8b7917a7 100644 (file)
@@ -443,7 +443,7 @@ LEFT JOIN civicrm_email ON (contact_a.id = civicrm_email.contact_id AND civicrm_
     $savedSearch = self::retrieve(['id' => $id]);
     // APIv4 search
     if (!empty($savedSearch->api_entity)) {
-      return CRM_Utils_System::url('civicrm/search', NULL, FALSE, "/edit/$id");
+      return CRM_Utils_System::url('civicrm/admin/search', NULL, FALSE, "/edit/$id");
     }
     // Classic search builder
     if (!empty($savedSearch->mapping_id)) {
index e98f208a15e4d6e5a7e2f32afa6421fd275dc4ed..ccd4cf6b247308a81d268fcb6db7d569b0c5cffd 100644 (file)
@@ -17,7 +17,7 @@ class CRM_Search_Page_Admin extends CRM_Core_Page {
   public function run() {
     $breadCrumb = [
       'title' => ts('Search Kit'),
-      'url' => CRM_Utils_System::url('civicrm/search', NULL, FALSE, '/list'),
+      'url' => CRM_Utils_System::url('civicrm/admin/search', NULL, FALSE, '/list'),
     ];
     CRM_Utils_System::appendBreadCrumb([$breadCrumb]);
 
@@ -40,8 +40,7 @@ class CRM_Search_Page_Admin extends CRM_Core_Page {
 
     // Load angular module
     $loader = new Civi\Angular\AngularLoader();
-    $loader->setModules(['searchAdmin']);
-    $loader->setPageName('civicrm/search');
+    $loader->setPageName('civicrm/admin/search');
     $loader->useApp([
       'defaultRoute' => '/list',
     ]);
diff --git a/ext/search/CRM/Search/Page/Search.php b/ext/search/CRM/Search/Page/Search.php
new file mode 100644 (file)
index 0000000..053a8ee
--- /dev/null
@@ -0,0 +1,30 @@
+<?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       |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ * Angular base page for search admin
+ */
+class CRM_Search_Page_Search extends CRM_Core_Page {
+
+  public function run() {
+
+    Civi::resources()->addBundle('bootstrap3');
+
+    // Load angular module
+    $loader = new Civi\Angular\AngularLoader();
+    $loader->setPageName('civicrm/search');
+    $loader->useApp();
+    $loader->load();
+
+    parent::run();
+  }
+
+}
index 667460ecba06408145f47d036d1bf696c8112e26..87dfb5f03dd69938f784c53ad27d24184f359f35 100644 (file)
@@ -14,7 +14,7 @@ class CRM_Search_Upgrader extends CRM_Search_Upgrader_Base {
       ->addValue('parent_id:name', 'Search')
       ->addValue('label', E::ts('Search Kit'))
       ->addValue('name', 'search_kit')
-      ->addValue('url', 'civicrm/search')
+      ->addValue('url', 'civicrm/admin/search')
       ->addValue('icon', 'crm-i fa-search-plus')
       ->addValue('has_separator', 2)
       ->addValue('weight', 99)
index 47711011830b7d931346cd558f366d3e67e34774..5918d757dda1d6b7067e40be810447bb4078af78 100644 (file)
@@ -58,13 +58,13 @@ class Actions {
         'title' => ts('Update %1'),
         'icon' => 'fa-save',
         'entities' => [],
-        'uiDialog' => ['templateUrl' => '~/searchActions/crmSearchActionUpdate.html'],
+        'uiDialog' => ['templateUrl' => '~/crmSearchActions/crmSearchActionUpdate.html'],
       ],
       'delete' => [
         'title' => ts('Delete %1'),
         'icon' => 'fa-trash',
         'entities' => [],
-        'uiDialog' => ['templateUrl' => '~/searchActions/crmSearchActionDelete.html'],
+        'uiDialog' => ['templateUrl' => '~/crmSearchActions/crmSearchActionDelete.html'],
       ],
     ];
 
similarity index 70%
rename from ext/search/ang/searchActions.ang.php
rename to ext/search/ang/crmSearchActions.ang.php
index 6699110540f8ddfba0753411425dd42d20042eb8..4b0841c4707c21c50d566a02553eca83ec9b85aa 100644 (file)
@@ -2,12 +2,12 @@
 // Autoloader data for search actions.
 return [
   'js' => [
-    'ang/searchActions.module.js',
-    'ang/searchActions/*.js',
-    'ang/searchActions/*/*.js',
+    'ang/crmSearchActions.module.js',
+    'ang/crmSearchActions/*.js',
+    'ang/crmSearchActions/*/*.js',
   ],
   'partials' => [
-    'ang/searchActions',
+    'ang/crmSearchActions',
   ],
   'basePages' => [],
   'requires' => ['crmUi', 'crmUtil', 'dialogService', 'api4'],
similarity index 55%
rename from ext/search/ang/searchActions.module.js
rename to ext/search/ang/crmSearchActions.module.js
index 9f4204c0bf77a3f5358fe09957db00226537493c..912d2e54ff26c5e32dab9618ebc2e098174cad8d 100644 (file)
@@ -2,6 +2,6 @@
   "use strict";
 
   // Declare module
-  angular.module('searchActions', CRM.angRequires('searchActions'));
+  angular.module('crmSearchActions', CRM.angRequires('crmSearchActions'));
 
 })(angular, CRM.$, CRM._);
similarity index 92%
rename from ext/search/ang/searchActions/SaveSmartGroup.ctrl.js
rename to ext/search/ang/crmSearchActions/SaveSmartGroup.ctrl.js
index b7d8c1f1d09ac2e7b36bb40acc7107fc72bbe5d7..7b89a9dc2852f62db175b422a09066336caabca2 100644 (file)
@@ -1,7 +1,7 @@
 (function(angular, $, _) {
   "use strict";
 
-  angular.module('searchActions').controller('SaveSmartGroup', function ($scope, $element, $timeout, crmApi4, dialogService, searchMeta) {
+  angular.module('crmSearchActions').controller('SaveSmartGroup', function ($scope, $element, $timeout, crmApi4, dialogService, searchMeta) {
     var ts = $scope.ts = CRM.ts(),
       model = $scope.model;
     $scope.groupEntityRefParams = {
@@ -35,7 +35,7 @@
     $scope.perm = {
       administerReservedGroups: CRM.checkPerm('administer reserved groups')
     };
-    $scope.groupOptions = CRM.searchActions.groupOptions;
+    $scope.groupOptions = CRM.crmSearchActions.groupOptions;
     $element.on('change', '#api-save-search-select-group', function() {
       if ($(this).val()) {
         $scope.$apply(function() {
similarity index 82%
rename from ext/search/ang/searchActions/crmSearchActionDelete.ctrl.js
rename to ext/search/ang/crmSearchActions/crmSearchActionDelete.ctrl.js
index 4f0cf17a655a2a100be7c13bbb2268c921b2f7da..b842e96a397b28731ee83eaa8700599eb4c90755 100644 (file)
@@ -1,7 +1,7 @@
 (function(angular, $, _) {
   "use strict";
 
-  angular.module('searchActions').controller('crmSearchActionDelete', function($scope, crmApi4, dialogService, searchMeta) {
+  angular.module('crmSearchActions').controller('crmSearchActionDelete', function($scope, crmApi4, dialogService, searchMeta) {
     var ts = $scope.ts = CRM.ts(),
       model = $scope.model,
       ctrl = $scope.$ctrl = this;
similarity index 92%
rename from ext/search/ang/searchActions/crmSearchActionUpdate.ctrl.js
rename to ext/search/ang/crmSearchActions/crmSearchActionUpdate.ctrl.js
index 18bd6fd08235c091918b25588d72d3f736cc98d9..229d75c73536a095df2c4c20d1ecbd2155356608 100644 (file)
@@ -1,7 +1,7 @@
 (function(angular, $, _) {
   "use strict";
 
-  angular.module('searchActions').controller('crmSearchActionUpdate', function ($scope, $timeout, crmApi4, dialogService, searchMeta) {
+  angular.module('crmSearchActions').controller('crmSearchActionUpdate', function ($scope, $timeout, crmApi4, dialogService, searchMeta) {
     var ts = $scope.ts = CRM.ts(),
       model = $scope.model,
       ctrl = $scope.$ctrl = this;
similarity index 87%
rename from ext/search/ang/searchActions/crmSearchActions.component.js
rename to ext/search/ang/crmSearchActions/crmSearchActions.component.js
index 2638d5c2bbbaef3178cd72424128a8bedfbd3f9b..1958e5da637b46e677dbcb9fca91d49744bbccb1 100644 (file)
@@ -1,13 +1,13 @@
 (function(angular, $, _) {
   "use strict";
 
-  angular.module('searchActions').component('crmSearchActions', {
+  angular.module('crmSearchActions').component('crmSearchActions', {
     bindings: {
       entity: '<',
       refresh: '&',
       ids: '<'
     },
-    templateUrl: '~/searchActions/crmSearchActions.html',
+    templateUrl: '~/crmSearchActions/crmSearchActions.html',
     controller: function($scope, crmApi4, dialogService, searchMeta) {
       var ts = $scope.ts = CRM.ts(),
         ctrl = this,
@@ -28,9 +28,9 @@
           where: [['name', 'IN', ['update', 'delete']]],
         }, ['name']).then(function(allowed) {
           _.each(allowed, function(action) {
-            CRM.searchActions.tasks[action].entities.push(ctrl.entity);
+            CRM.crmSearchActions.tasks[action].entities.push(ctrl.entity);
           });
-          var actions = _.transform(_.cloneDeep(CRM.searchActions.tasks), function(actions, action) {
+          var actions = _.transform(_.cloneDeep(CRM.crmSearchActions.tasks), function(actions, action) {
             if (_.includes(action.entities, ctrl.entity)) {
               action.title = action.title.replace('%1', entityTitle);
               actions.push(action);
similarity index 84%
rename from ext/search/ang/searchActions/saveSmartGroup.directive.js
rename to ext/search/ang/crmSearchActions/saveSmartGroup.directive.js
index 9f59a7d668b55d497a2dc0cf209de05076bf02d0..bd68699d857754b4a382f858969a6a1e3df4bc97 100644 (file)
@@ -1,13 +1,14 @@
 (function(angular, $, _) {
   "use strict";
 
-  angular.module('searchActions').directive('saveSmartGroup', function() {
+  angular.module('crmSearchActions').directive('saveSmartGroup', function() {
     return {
       bindToController: {
         load: '<',
         entity: '<',
         params: '<'
       },
+      restrict: 'A',
       controller: function ($scope, $element, dialogService) {
         var ts = $scope.ts = CRM.ts(),
           ctrl = this;
@@ -30,7 +31,7 @@
             autoOpen: false,
             title: ts('Save smart group')
           });
-          dialogService.open('saveSearchDialog', '~/searchActions/saveSmartGroup.html', model, options);
+          dialogService.open('saveSearchDialog', '~/crmSearchActions/saveSmartGroup.html', model, options);
         };
       }
     };
diff --git a/ext/search/ang/crmSearchDisplay.ang.php b/ext/search/ang/crmSearchDisplay.ang.php
new file mode 100644 (file)
index 0000000..a2524b3
--- /dev/null
@@ -0,0 +1,17 @@
+<?php
+// Autoloader data for SearchDisplay module.
+return [
+  'js' => [
+    'ang/crmSearchDisplay.module.js',
+    'ang/crmSearchDisplay/*.js',
+    'ang/crmSearchDisplay/*/*.js',
+  ],
+  'partials' => [
+    'ang/crmSearchDisplay',
+  ],
+  'basePages' => [],
+  'requires' => ['crmUi', 'api4', 'crmSearchActions', 'ui.bootstrap'],
+  'exports' => [
+    'crm-search-display-table' => 'E',
+  ],
+];
diff --git a/ext/search/ang/crmSearchDisplay.module.js b/ext/search/ang/crmSearchDisplay.module.js
new file mode 100644 (file)
index 0000000..beae3e5
--- /dev/null
@@ -0,0 +1,7 @@
+(function(angular, $, _) {
+  "use strict";
+
+  // Declare module
+  angular.module('crmSearchDisplay', CRM.angRequires('crmSearchDisplay'));
+
+})(angular, CRM.$, CRM._);
diff --git a/ext/search/ang/crmSearchDisplay/crmSearchDisplayTable.component.js b/ext/search/ang/crmSearchDisplay/crmSearchDisplayTable.component.js
new file mode 100644 (file)
index 0000000..fe3de21
--- /dev/null
@@ -0,0 +1,100 @@
+(function(angular, $, _) {
+  "use strict";
+
+  angular.module('crmSearchDisplay').component('crmSearchDisplayTable', {
+    bindings: {
+      apiEntity: '<',
+      apiParams: '<',
+      settings: '<'
+    },
+    templateUrl: '~/crmSearchDisplay/crmSearchDisplayTable.html',
+    controller: function($scope, crmApi4) {
+      var ts = $scope.ts = CRM.ts(),
+        ctrl = this;
+
+      this.page = 1;
+
+      this.$onInit = function() {
+        this.orderBy = this.apiParams.orderBy || {};
+        this.limit = parseInt(ctrl.settings.limit || 0, 10);
+        _.each(ctrl.settings.columns, function(col, num) {
+          var index = ctrl.apiParams.select.indexOf(col.expr);
+          if (_.includes(col.expr, '(') && !_.includes(col.expr, ' AS ')) {
+            col.expr += ' AS column_' + num;
+            ctrl.apiParams.select[index] += ' AS column_' + num;
+          }
+          col.key = _.last(col.expr.split(' AS '));
+        });
+        getResults();
+      };
+
+      function getResults() {
+        var params = _.merge(_.cloneDeep(ctrl.apiParams), {limit: ctrl.limit, offset: (ctrl.page - 1) * ctrl.limit, orderBy: ctrl.orderBy});
+        if (ctrl.settings.pager) {
+          params.select.push('row_count');
+        }
+        crmApi4(ctrl.apiEntity, 'get', params).then(function(results) {
+          ctrl.results = results;
+          ctrl.rowCount = results.count;
+        });
+      }
+
+      this.changePage = function() {
+        getResults();
+      };
+
+      /**
+       * Returns crm-i icon class for a sortable column
+       * @param col
+       * @returns {string}
+       */
+      $scope.getOrderBy = function(col) {
+        var dir = ctrl.orderBy && ctrl.orderBy[col.key];
+        if (dir) {
+          return 'fa-sort-' + dir.toLowerCase();
+        }
+        return 'fa-sort disabled';
+      };
+
+      /**
+       * Called when clicking on a column header
+       * @param col
+       * @param $event
+       */
+      $scope.setOrderBy = function(col, $event) {
+        var dir = $scope.getOrderBy(col) === 'fa-sort-asc' ? 'DESC' : 'ASC';
+        if (!$event.shiftKey) {
+          ctrl.orderBy = {};
+        }
+        ctrl.orderBy[col.key] = dir;
+        getResults();
+      };
+
+      $scope.formatResult = function(row, col) {
+        var value = row[col.key];
+        return formatFieldValue(col, value);
+      };
+
+      function formatFieldValue(col, value) {
+        var type = col.dataType;
+        if (_.isArray(value)) {
+          return _.map(value, function(val) {
+            return formatFieldValue(col, val);
+          }).join(', ');
+        }
+        if (value && (type === 'Date' || type === 'Timestamp') && /^\d{4}-\d{2}-\d{2}/.test(value)) {
+          return CRM.utils.formatDate(value, null, type === 'Timestamp');
+        }
+        else if (type === 'Boolean' && typeof value === 'boolean') {
+          return value ? ts('Yes') : ts('No');
+        }
+        else if (type === 'Money' && typeof value === 'number') {
+          return CRM.formatMoney(value);
+        }
+        return value;
+      }
+
+    }
+  });
+
+})(angular, CRM.$, CRM._);
diff --git a/ext/search/ang/crmSearchDisplay/crmSearchDisplayTable.html b/ext/search/ang/crmSearchDisplay/crmSearchDisplayTable.html
new file mode 100644 (file)
index 0000000..a980eb5
--- /dev/null
@@ -0,0 +1,40 @@
+<table>
+  <thead>
+    <tr>
+      <th class="crm-search-result-select" ng-if="$ctrl.settings.actions">
+        <input type="checkbox" ng-checked="$ctrl.allRowsSelected" ng-click="selectAllRows()" >
+      </th>
+      <th ng-repeat="col in $ctrl.settings.columns" ng-click="setOrderBy(col, $event)" title="{{:: ts('Click to sort results (shift-click to sort by multiple).') }}">
+        <i class="crm-i {{ getOrderBy(col) }}"></i>
+        <span>{{ col.label }}</span>
+      </th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr ng-repeat="row in $ctrl.results">
+      <td ng-if="$ctrl.settings.actions">
+        <input type="checkbox" ng-checked="isRowSelected(row)" ng-click="selectRow(row)" ng-disabled="!(loading === false && !loadingAllRows && row.id)">
+      </td>
+      <td ng-repeat="col in $ctrl.settings.columns">
+        {{ formatResult(row, col) }}
+      </td>
+      <td></td>
+    </tr>
+  </tbody>
+</table>
+<div class="text-center" ng-if="$ctrl.rowCount">
+  <ul uib-pagination
+      class="pagination"
+      boundary-links="true"
+      total-items="$ctrl.rowCount"
+      ng-model="$ctrl.page"
+      ng-change="$ctrl.changePage()"
+      items-per-page="$ctrl.limit"
+      max-size="6"
+      force-ellipses="true"
+      previous-text="&lsaquo;"
+      next-text="&rsaquo;"
+      first-text="&laquo;"
+      last-text="&raquo;"
+  ></ul>
+</div>
diff --git a/ext/search/ang/crmSearchPage.ang.php b/ext/search/ang/crmSearchPage.ang.php
new file mode 100644 (file)
index 0000000..54442bd
--- /dev/null
@@ -0,0 +1,14 @@
+<?php
+// Autoloader data for SearchDisplay module.
+return [
+  'js' => [
+    'ang/crmSearchPage.module.js',
+    'ang/crmSearchPage/*.js',
+    'ang/crmSearchPage/*/*.js',
+  ],
+  'partials' => [
+    'ang/crmSearchPage',
+  ],
+  'basePages' => ['civicrm/search'],
+  'requires' => ['ngRoute', 'api4', 'crmUi', 'crmSearchDisplay'],
+];
diff --git a/ext/search/ang/crmSearchPage.module.js b/ext/search/ang/crmSearchPage.module.js
new file mode 100644 (file)
index 0000000..0c8862c
--- /dev/null
@@ -0,0 +1,31 @@
+(function(angular, $, _) {
+  "use strict";
+
+  // Declare module
+  angular.module('crmSearchPage', CRM.angRequires('crmSearchPage'))
+
+
+    .config(function($routeProvider) {
+      $routeProvider.when('/display/:savedSearchName/:displayName', {
+        controller: 'crmSearchPageDisplay',
+        templateUrl: '~/crmSearchPage/display.html',
+        resolve: {
+          // Load saved search display
+          display: function($route, crmApi4) {
+            var params = $route.current.params;
+            return crmApi4('SearchDisplay', 'get', {
+              where: [['name', '=', params.displayName], ['saved_search.name', '=', params.savedSearchName]],
+              select: ['*', 'saved_search.api_entity', 'saved_search.api_params']
+            }, 0);
+          }
+        }
+      });
+    })
+
+    // Controller for displaying a search
+    .controller('crmSearchPageDisplay', function($scope, $routeParams, $location, display) {
+      this.display = display;
+      $scope.$ctrl = this;
+    });
+
+})(angular, CRM.$, CRM._);
diff --git a/ext/search/ang/crmSearchPage/display.html b/ext/search/ang/crmSearchPage/display.html
new file mode 100644 (file)
index 0000000..789963a
--- /dev/null
@@ -0,0 +1,7 @@
+<h1 crm-page-title>{{:: $ctrl.display.label }}</h1>
+
+<div ng-switch="$ctrl.display.type" id="bootstrap-theme">
+  <div ng-switch-when="table">
+    <crm-search-display-table api-entity="$ctrl.display['saved_search.api_entity']" api-params="$ctrl.display['saved_search.api_params']" settings="$ctrl.display.settings"></crm-search-display-table>
+  </div>
+</div>
index 31a85141bff549c879bc86848bd0605dcb47459a..4b944adcd692a31b30354128231bc911be5a806d 100644 (file)
@@ -12,7 +12,7 @@ return [
   'partials' => [
     'ang/searchAdmin',
   ],
-  'basePages' => [],
-  'requires' => ['crmUi', 'crmUtil', 'ngRoute', 'ui.sortable', 'ui.bootstrap', 'api4', 'searchActions'],
+  'basePages' => ['civicrm/admin/search'],
+  'requires' => ['crmUi', 'crmUtil', 'ngRoute', 'ui.sortable', 'ui.bootstrap', 'api4', 'crmSearchDisplay', 'crmSearchActions'],
   'settingsFactory' => ['\Civi\Search\Admin', 'getAdminSettings'],
 ];
index 486298bca61cbd39157e9ca9dd22f3b7a07cc788..168aba26b4021747405e1c7036641293794bdffd 100644 (file)
@@ -16,7 +16,7 @@
           // Load data for lists
           savedSearches: function(crmApi4) {
             return crmApi4('SavedSearch', 'get', {
-              select: ['id', 'api_entity', 'form_values', 'COUNT(search_display.id) AS displays', 'GROUP_CONCAT(group.title) AS groups'],
+              select: ['id', 'label', 'api_entity', 'form_values', 'COUNT(search_display.id) AS displays', 'GROUP_CONCAT(group.title) AS groups'],
               join: [['SearchDisplay AS search_display'], ['Group AS group']],
               where: [['api_entity', 'IS NOT NULL']],
               groupBy: ['id']
@@ -38,7 +38,7 @@
             return crmApi4('SavedSearch', 'get', {
               where: [['id', '=', params.id]],
               chain: {
-                group: ['Group', 'get', {where: [['saved_search_id', '=', '$id']]}, 0],
+                groups: ['Group', 'get', {where: [['saved_search_id', '=', '$id']]}],
                 displays: ['SearchDisplay', 'get', {where: [['saved_search_id', '=', '$id']]}]
               }
             }, 0);
index 1cfa81ba708d05c38823a1b7a8d9847f10b44697..6b5eb4b3af88db2ae3450de3fed941767c8343af 100644 (file)
@@ -1,9 +1,5 @@
 <div class="crm-flex-box">
   <div>
-    <div class="form-inline">
-      <label for="crm-search-main-entity">{{:: ts('Search for') }}</label>
-      <input id="crm-search-main-entity" class="form-control" ng-model="$ctrl.savedSearch.api_entity" crm-ui-select="::{allowClear: false, data: entities}" />
-    </div>
     <div ng-if=":: $ctrl.paramExists('join')">
       <fieldset ng-repeat="join in $ctrl.savedSearch.api_params.join">
         <div class="form-inline">
index 9bd8f25686de376a66e21a8782decdc82ac3140d..049d037bb4da6d32b2363d84119819342514227d 100644 (file)
@@ -4,9 +4,9 @@
       <th class="crm-search-result-select">
         <input type="checkbox" ng-checked="$ctrl.allRowsSelected" ng-click="selectAllRows()" ng-disabled="!(loading === false && !loadingAllRows && $ctrl.results[$ctrl.page] && $ctrl.results[$ctrl.page][0].id)">
       </th>
-      <th ng-repeat="col in $ctrl.savedSearch.api_params.select" ng-click="setOrderBy(col, $event)" title="{{:: ts('Drag to reorder columns, click to sort results (shift-click to sort by multiple).')}}">
+      <th ng-repeat="col in $ctrl.savedSearch.api_params.select" ng-click="setOrderBy(col, $event)" title="{{:: ts('Drag to reorder columns, click to sort results (shift-click to sort by multiple).') }}">
         <i class="crm-i {{ getOrderBy(col) }}"></i>
-        <span ng-class="{'crm-sortable': $index || !$ctrl.groupExists}">{{ $ctrl.getFieldLabel(col) }}</span>
+        <span ng-class="{'crm-draggable': $index || !$ctrl.groupExists}">{{ $ctrl.getFieldLabel(col) }}</span>
         <span ng-switch="$index || !$ctrl.groupExists ? 'sortable' : 'locked'">
           <i ng-switch-when="locked" class="crm-i fa-lock" aria-hidden="true"></i>
           <a href ng-switch-default class="crm-hover-button" title="{{:: ts('Clear') }}" ng-click="$ctrl.clearParam('select', $index)"><i class="crm-i fa-times" aria-hidden="true"></i></a>
index 57867876d3c7488ad96dd7646f9ad64dec834c71..0e98c31d9d3c86b91d6edbfbd98e403351623ef3 100644 (file)
@@ -26,7 +26,7 @@
 
       $scope.controls = {tab: 'compose'};
       $scope.joinTypes = [{k: false, v: ts('Optional')}, {k: true, v: ts('Required')}];
-      $scope.groupOptions = CRM.searchActions.groupOptions;
+      $scope.groupOptions = CRM.crmSearchActions.groupOptions;
       $scope.entities = formatForSelect2(CRM.vars.search.schema, 'name', 'title_plural', ['description', 'icon']);
       this.perm = {
         editGroups: CRM.checkPerm('edit groups')
         this.entityTitle = searchMeta.getEntity(this.savedSearch.api_entity).title_plural;
 
         this.savedSearch.displays = this.savedSearch.displays || [];
-        this.groupExists = !!this.savedSearch.group;
-
-        this.original = _.indexBy(_.cloneDeep(this.savedSearch.displays), 'id');
-        if (this.savedSearch.group) {
-          this.original.group = _.cloneDeep(this.savedSearch.group);
-        }
+        this.savedSearch.groups = this.savedSearch.groups || [];
+        this.groupExists = !!this.savedSearch.groups.length;
 
         if (!this.savedSearch.api_params) {
           this.savedSearch.api_params = {
+            version: 4,
             select: getDefaultSelect(),
             orderBy: {},
             where: [],
           $scope.$watch('$ctrl.savedSearch.api_params.having', onChangeFilters, true);
         }
 
+        $scope.$watch('$ctrl.savedSearch', onChangeAnything, true);
+
+        // Is this savedSearch record saved, unsaved or saving
+        $scope.status = this.savedSearch && this.savedSearch.id ? 'saved' : 'unsaved';
+
         loadFieldOptions();
       };
 
+      function onChangeAnything() {
+        $scope.status = 'unsaved';
+      }
+
+      this.save = function() {
+        $scope.status = 'saving';
+        var params = _.cloneDeep(ctrl.savedSearch),
+          apiCalls = {},
+          chain = {};
+        if (ctrl.groupExists) {
+          chain.groups = ['Group', 'save', {defaults: {saved_search_id: '$id'}, records: params.groups}];
+          delete params.groups;
+        } else if (params.id) {
+          apiCalls.deleteGroup = ['Group', 'delete', {where: [['saved_search_id', '=', params.id]]}];
+        }
+        if (params.displays && params.displays.length) {
+          chain.displays = ['SearchDisplay', 'replace', {where: [['saved_search_id', '=', '$id']], records: params.displays}];
+        } else if (params.id) {
+          apiCalls.deleteDisplays = ['SearchDisplay', 'delete', {where: [['saved_search_id', '=', params.id]]}];
+        }
+        delete params.displays;
+        apiCalls.saved = ['SavedSearch', 'save', {records: [params], chain: chain}, 0];
+        crmApi4(apiCalls).then(function(results) {
+          ctrl.savedSearch.id = results.saved.id;
+          ctrl.savedSearch.groups = results.saved.groups || [];
+          ctrl.savedSearch.displays = results.saved.displays || [];
+          if ($scope.status === 'saving') {
+            $scope.status = 'saved';
+          }
+        });
+      };
+
       this.paramExists = function(param) {
         return _.includes(searchMeta.getEntity(ctrl.savedSearch.api_entity).params, param);
       };
 
       this.addDisplay = function(type) {
         ctrl.savedSearch.displays.push({
-          type: type
+          type: type,
+          label: '',
+          settings: {}
         });
         $scope.selectTab('display_' + (ctrl.savedSearch.displays.length - 1));
       };
       };
 
       this.addGroup = function() {
-        ctrl.savedSearch.group = {
+        ctrl.savedSearch.groups.push({
           title: '',
           description: '',
           visibility: 'User and User Admin Only',
           group_type: []
-        };
+        });
         ctrl.groupExists = true;
         $scope.selectTab('group');
       };
 
       this.removeGroup = function() {
         ctrl.groupExists = !ctrl.groupExists;
-        if (!ctrl.groupExists && (!ctrl.savedSearch.group || !ctrl.savedSearch.group.id)) {
-          ctrl.savedSearch.group = null;
+        if (!ctrl.groupExists && (!ctrl.savedSearch.groups.length || !ctrl.savedSearch.groups[0].id)) {
+          ctrl.savedSearch.groups.length = 0;
         }
         $scope.selectTab('compose');
       };
 
       $scope.sortableColumnOptions = {
         axis: 'x',
-        handle: '.crm-sortable',
+        handle: '.crm-draggable',
         update: function(e, ui) {
           // Don't allow items to be moved to position 0 if locked
           if (!ui.item.sortable.dropindex && ctrl.groupExists) {
index b8fcb1820b6aa970c8762a2f8e4cccadcc6257ad..a4b184b6ec136914cd1115651efba0285d2dcc1f 100644 (file)
 
   <form>
     <div class="navbar-form clearfix">
-      <div class="form-group pull-right">
-        <button class="btn btn-default" ng-disabled="$ctrl.saved" ng-click="$ctrl.save()">
-          {{ $ctrl.saved ? ts('Saved') : ts('Save') }}
+      <label for="crm-saved-search-label">{{:: ts('Label:') }}</label>
+      <input id="crm-saved-search-label" class="form-control" ng-model="$ctrl.savedSearch.label" type="text" />
+      <label for="crm-search-main-entity">{{:: ts('Search for:') }}</label>
+      <input id="crm-search-main-entity" class="form-control" ng-model="$ctrl.savedSearch.api_entity" crm-ui-select="::{allowClear: false, data: entities}" ng-disabled="$ctrl.savedSearch.id" />
+      <div class="btn-group btn-group-md pull-right">
+        <button type="submit" class="btn" ng-class="{'btn-primary': status === 'unsaved', 'btn-warning': status === 'saving', 'btn-success': status === 'saved'}" ng-disabled="status !== 'unsaved'" ng-click="$ctrl.save()">
+          <i class="crm-i" ng-class="{'fa-check': status !== 'saving', 'fa-spin fa-spinner': status === 'saving'}"></i>
+          <span ng-if="status === 'saved'">{{ ts('Saved') }}</span>
+          <span ng-if="status === 'unsaved'">{{ ts('Save') }}</span>
+          <span ng-if="status === 'saving'">{{ ts('Saving...') }}</span>
         </button>
       </div>
     </div>
@@ -33,6 +40,9 @@
           <fieldset ng-include="'~/searchAdmin/group.html'"></fieldset>
         </div>
         <div ng-switch-default>
+          <div ng-repeat="display in $ctrl.savedSearch.displays" ng-if="controls.tab === ('display_' + $index)">
+            <div ng-include="'~/searchAdmin/display.html'"></div>
+          </div>
         </div>
       </div>
     </div>
diff --git a/ext/search/ang/searchAdmin/display.html b/ext/search/ang/searchAdmin/display.html
new file mode 100644 (file)
index 0000000..7e954ed
--- /dev/null
@@ -0,0 +1,9 @@
+<fieldset>
+  <div class="form-inline">
+    <label for="search_display_label">{{:: ts('Name:') }} <span class="crm-marker">*</span></label>
+    <input id="search_display_label" type="text" class="form-control" ng-model="display.label" required />
+    <label class="pull-right">{{:: $ctrl.displayTypes[display.type].label }}</label>
+  </div>
+</fieldset>
+<!-- Load template with the name of this display type, e.g. 'table.html' -->
+<div ng-if="display.type" ng-include="'~/searchAdmin/displays/' + display.type + '.html'"></div>
diff --git a/ext/search/ang/searchAdmin/displays/searchAdminDisplayTable.component.js b/ext/search/ang/searchAdmin/displays/searchAdminDisplayTable.component.js
new file mode 100644 (file)
index 0000000..5d3b6b9
--- /dev/null
@@ -0,0 +1,68 @@
+(function(angular, $, _) {
+  "use strict";
+
+  angular.module('searchAdmin').component('searchAdminDisplayTable', {
+    bindings: {
+      display: '<',
+      apiEntity: '<',
+      apiParams: '<'
+    },
+    require: {
+      crmSearchAdmin: '^crmSearchAdmin'
+    },
+    templateUrl: '~/searchAdmin/displays/searchAdminDisplayTable.html',
+    controller: function($scope, searchMeta) {
+      var ts = $scope.ts = CRM.ts(),
+        ctrl = this;
+
+      function fieldToColumn(fieldExpr) {
+        var info = searchMeta.parseExpr(fieldExpr);
+        return {
+          expr: fieldExpr,
+          label: ctrl.getFieldLabel(fieldExpr),
+          dataType: (info.fn && info.fn.name === 'COUNT') ? 'Integer' : info.field.data_type
+        };
+      }
+
+      this.sortableOptions = {
+        connectWith: '.crm-search-admin-table-columns',
+        containment: '.crm-search-admin-table-columns-wrapper'
+      };
+
+      this.removeCol = function(index) {
+        ctrl.hiddenColumns.push(ctrl.display.settings.columns[index]);
+        ctrl.display.settings.columns.splice(index, 1);
+      };
+
+      this.restoreCol = function(index) {
+        ctrl.display.settings.columns.push(ctrl.hiddenColumns[index]);
+        ctrl.hiddenColumns.splice(index, 1);
+      };
+
+      this.$onInit = function () {
+        ctrl.display.settings.limit = parseInt(ctrl.display.settings.limit || 0, 10);
+        ctrl.getFieldLabel = ctrl.crmSearchAdmin.getFieldLabel;
+        if (!ctrl.display.settings.columns) {
+          ctrl.display.settings.columns = _.transform(ctrl.apiParams.select, function(columns, fieldExpr) {
+            columns.push(fieldToColumn(fieldExpr));
+          });
+          ctrl.hiddenColumns = [];
+        } else {
+          var activeColumns = _.collect(ctrl.display.settings.columns, 'expr');
+          ctrl.hiddenColumns = _.transform(ctrl.apiParams.select, function(hiddenColumns, fieldExpr) {
+            if (!_.includes(activeColumns, fieldExpr)) {
+              hiddenColumns.push(fieldToColumn(fieldExpr));
+            }
+          });
+          _.each(activeColumns, function(fieldExpr, index) {
+            if (!_.includes(ctrl.apiParams.select, fieldExpr)) {
+              ctrl.display.settings.columns.splice(index, 1);
+            }
+          });
+        }
+      };
+
+    }
+  });
+
+})(angular, CRM.$, CRM._);
diff --git a/ext/search/ang/searchAdmin/displays/searchAdminDisplayTable.html b/ext/search/ang/searchAdmin/displays/searchAdminDisplayTable.html
new file mode 100644 (file)
index 0000000..4367c4c
--- /dev/null
@@ -0,0 +1,33 @@
+<fieldset>
+  <div class="form-inline">
+    <label for="crm-search-admin-table-limit">{{ ts('Results to display (0 for no limit):') }}</label>
+    <input id="crm-search-admin-table-limit" type="number" min="0" step="1" class="form-control" ng-model="$ctrl.display.settings.limit">
+    <label><input type="checkbox" ng-model="$ctrl.display.settings.pager"> {{ ts('Use Pager') }}</label>
+  </div>
+</fieldset>
+<div class="crm-flex-box crm-search-admin-table-columns-wrapper">
+  <fieldset class="crm-search-admin-table-columns" ng-model="$ctrl.display.settings.columns" ui-sortable="$ctrl.sortableOptions">
+    <legend>{{:: ts('Columns') }}</legend>
+    <fieldset ng-repeat="col in $ctrl.display.settings.columns" class="crm-draggable">
+      <legend>{{ $ctrl.getFieldLabel(col.expr) }}</legend>
+      <div class="form-inline">
+        <label>{{ ts('Label:') }}</label> <input class="form-control" type="text" ng-model="col.label" />
+        <button class="btn-xs pull-right" ng-click="$ctrl.removeCol($index)" title="{{:: ts('Hide') }}">
+          <i class="crm-i fa-ban"></i>
+        </button>
+      </div>
+    </fieldset>
+  </fieldset>
+  <fieldset class="crm-search-admin-table-columns" ng-model="$ctrl.hiddenColumns" ui-sortable="$ctrl.sortableOptions">
+    <legend>{{:: ts('Hidden Columns') }}</legend>
+    <fieldset ng-repeat="col in $ctrl.hiddenColumns" class="crm-draggable">
+      <legend>{{ $ctrl.getFieldLabel(col.expr) }}</legend>
+      <div class="form-inline">
+        <label>{{ ts('Label:') }}</label> <input disabled class="form-control" type="text" ng-model="col.label" />
+        <button class="btn-xs pull-right" ng-click="$ctrl.restoreCol($index)" title="{{:: ts('Show') }}">
+          <i class="crm-i fa-undo"></i>
+        </button>
+      </div>
+    </fieldset>
+  </fieldset>
+</div>
diff --git a/ext/search/ang/searchAdmin/displays/table.html b/ext/search/ang/searchAdmin/displays/table.html
new file mode 100644 (file)
index 0000000..fb5bf38
--- /dev/null
@@ -0,0 +1,3 @@
+<search-admin-display-table api-entity="$ctrl.savedSearch.api_entity" api-params="$ctrl.savedSearch.api_params" display="display"></search-admin-display-table>
+<hr>
+<!--<crm-search-display-table api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" settings="display.settings"></crm-search-display-table>-->
index 6a4674f2b7556cb539b2f35a96963b05d3f9d8be..8eb14919c3157e04b18395f8ee36e668c3177ee8 100644 (file)
@@ -1,13 +1,13 @@
 <div ng-if="!$ctrl.groupExists">
   <div class="alert alert-warning">
-    {{:: ts('Smart group "%1" will be deleted.', {1: $ctrl.original.group.title}) }}
+    {{:: ts('Smart group "%1" will be deleted.', {1: $ctrl.savedSearch.groups[0].title}) }}
   </div>
 </div>
 <div ng-if="$ctrl.groupExists">
   <div class="alert alert-warning" ng-show="!smartGroupColumns.length">
     {{:: ts('Unable to create smart group because this search does not include any contacts.') }}
   </div>
-  <input class="form-control" placeholder="{{:: ts('Group Title') }}" ng-model="$ctrl.savedSearch.group.title" ng-disabled="!smartGroupColumns.length">
+  <input class="form-control" placeholder="{{:: ts('Group Title') }}" ng-model="$ctrl.savedSearch.groups[0].title" ng-disabled="!smartGroupColumns.length">
   <hr />
   <div class="form-inline">
    <label for="api-save-search-select-column">{{:: ts('Contact Column:') }} <span class="crm-marker">*</span></label>
   </div>
   <fieldset ng-show="smartGroupColumns.length">
     <label>{{:: ts('Description:') }}</label>
-    <textarea class="form-control" ng-model="$ctrl.savedSearch.group.description"></textarea>
+    <textarea class="form-control" ng-model="$ctrl.savedSearch.groups[0].description"></textarea>
     <div class="form-inline">
       <label>{{:: ts('Group Type:') }} </label>
       <div class="checkbox" ng-repeat="option in groupOptions.group_type track by option.id">&nbsp;
         <label>
-          <input type="checkbox" checklist-model="$ctrl.savedSearch.group.group_type" checklist-value="option.id">
+          <input type="checkbox" checklist-model="$ctrl.savedSearch.groups[0].group_type" checklist-value="option.id">
           {{ option.label }}
         </label>&nbsp;
       </div>
     </div>
     <div class="form-inline">
       <label>{{:: ts('Visibility:') }}</label>
-      <select class="form-control" ng-model="$ctrl.savedSearch.group.visibility" ng-options="item.id as item.label for item in groupOptions.visibility track by item.id" crm-ui-select></select>
+      <select class="form-control" ng-model="$ctrl.savedSearch.groups[0].visibility" ng-options="item.id as item.label for item in groupOptions.visibility track by item.id" crm-ui-select></select>
     </div>
   </fieldset>
 </div>
index 6372ae23227cbe7c7eed5bf8e7d93a8fc29c3c1c..56cc6c3d67a4f5f2f502c112ebc23464a47a00c2 100644 (file)
@@ -1,13 +1,24 @@
 (function(angular, $, _) {
   "use strict";
 
-  angular.module('searchAdmin').controller('searchList', function($scope, savedSearches) {
+  angular.module('searchAdmin').controller('searchList', function($scope, savedSearches, crmApi4) {
     var ts = $scope.ts = CRM.ts(),
       ctrl = $scope.$ctrl = this;
     this.savedSearches = savedSearches;
     this.entityTitles = _.transform(CRM.vars.search.schema, function(titles, entity) {
       titles[entity.name] = entity.titlePlural;
     }, {});
+
+    this.deleteSearch = function(search) {
+      var index = _.findIndex(savedSearches, {id: search.id});
+      if (index > -1) {
+        crmApi4([
+          ['Group', 'delete', {where: [['saved_search_id', '=', search.id]]}],
+          ['SavedSearch', 'delete', {where: [['id', '=', search.id]]}]
+        ]);
+        savedSearches.splice(index, 1);
+      }
+    };
   });
 
 })(angular, CRM.$, CRM._);
index e64daef51ad2f43613d8b6f0ee49dfabb32f3b60..3f246f0c5496d2792113df16fdc79f901939cee8 100644 (file)
@@ -10,6 +10,7 @@
     <thead>
       <tr>
         <th>{{:: ts('ID') }}</th>
+        <th>{{:: ts('Label') }}</th>
         <th>{{:: ts('For') }}</th>
         <th>{{:: ts('Displays') }}</th>
         <th>{{:: ts('Smart Group') }}</th>
     <tbody>
       <tr ng-repeat="search in $ctrl.savedSearches">
         <td>{{ search.id }}</td>
+        <td>{{ search.label }}</td>
         <td>{{ $ctrl.entityTitles[search.api_entity] }}</td>
         <td>{{ search.displays }}</td>
         <td>{{ search.groups.join(', ') }}</td>
         <td>
-          <a href="#/load/SavedSearch/{{ search.id }}">{{:: ts('Edit') }}</a>
+          <a href="#/edit/{{ search.id }}">{{:: ts('Edit') }}</a>
+          <a href crm-confirm="{type: 'delete', obj: search}" on-yes="$ctrl.deleteSearch(search)">{{:: ts('Delete') }}</a>
         </td>
       </tr>
       <tr ng-if="$ctrl.savedSearches.length === 0">
         <td colspan="9">
           <p class="messages status no-popup text-center">
             {{:: ts('No saved searches.')}}
-            <a href="#/create/Contact/">{{:: ts('New Search') }}</a>
           </p>
         </td>
       </tr>
index a9d6591c017680eb2882c9f471287eb492fd7af9..6f581bc714e53a8249c7cad5966ad052d1d852ca 100644 (file)
@@ -4,19 +4,19 @@
     {{ ts('Compose Search') }}
   </a>
 </li>
-<li role="presentation" ng-class="{active: controls.tab === 'group'}" ng-if="$ctrl.savedSearch.group">
+<li role="presentation" ng-class="{active: controls.tab === 'group'}" ng-if="$ctrl.savedSearch.groups.length">
   <a href ng-click="selectTab('group')" ng-class="{strikethrough: !$ctrl.groupExists}">
     <i class="crm-i fa-users"></i>
-    {{:: ts('Smart Group:') }} {{ $ctrl.savedSearch.group.title }}
+    {{:: ts('Smart Group:') }} {{ $ctrl.savedSearch.groups[0].title }}
   </a>
   <button class="btn-xs crm-search-delete-display" ng-click="$ctrl.removeGroup()">
     <i class="crm-i fa-trash"></i>
   </button>
 </li>
 <li role="presentation" ng-repeat="display in $ctrl.savedSearch.displays" ng-class="{active: controls.tab === ('display_' + $index)}">
-  <a href ng-click="selectTab('display_' + $index)">
+  <a href ng-click="selectTab('display_' + $index)" ng-class="{strikethrough: display.trashed}">
     <i class="crm-i {{ $ctrl.displayTypes[display.type].icon }}"></i>
-    {{ $ctrl.displayTypes[display.type].label }}
+    {{ display.label || ts('Untitled') }}
   </a>
   <button class="btn-xs crm-search-delete-display" ng-click="$ctrl.removeDisplay($index)">
     <i class="crm-i fa-trash"></i>
@@ -27,7 +27,7 @@
     <i class="crm-i fa-plus"></i> {{:: ts('Add...') }} <span class="caret"></span>
   </button>
   <ul class="dropdown-menu">
-    <li ng-if="!$ctrl.savedSearch.group">
+    <li ng-if="!$ctrl.savedSearch.groups.length">
       <a href ng-click="$ctrl.addGroup()">
         <i class="crm-i fa-users"></i>
         {{:: ts('Smart Group') }}
index e792d5ceb95b9c278781a7e48453fe95f630ed74..af5733e31e210266b628345dd4fbc79d3c2bcee0 100644 (file)
@@ -13,7 +13,7 @@
 .crm-flex-box > .crm-flex-4 {
   flex: 4;
 }
-.crm-sortable {
+.crm-draggable {
   cursor: move;
 }
 
diff --git a/ext/search/templates/CRM/Search/Page/Search.tpl b/ext/search/templates/CRM/Search/Page/Search.tpl
new file mode 100644 (file)
index 0000000..e69de29
index b5657b94fc66b9dc742d07cf3e418cb8959af7ae..168defc411e816ece36869c80062e17f00c2051a 100644 (file)
@@ -2,7 +2,12 @@
 <menu>
   <item>
     <path>civicrm/search</path>
-    <page_callback>CRM_Search_Page_Admin</page_callback>
+    <page_callback>CRM_Search_Page_Search</page_callback>
     <access_arguments>access CiviCRM</access_arguments>
   </item>
+  <item>
+    <path>civicrm/admin/search</path>
+    <page_callback>CRM_Search_Page_Admin</page_callback>
+    <access_arguments>administer CiviCRM</access_arguments>
+  </item>
 </menu>