SearchKit - Use a searchDisplay instead of ad-hoc table to display main list of saved...
authorColeman Watts <coleman@civicrm.org>
Thu, 26 Aug 2021 15:29:12 +0000 (11:29 -0400)
committerColeman Watts <coleman@civicrm.org>
Tue, 31 Aug 2021 20:12:21 +0000 (16:12 -0400)
12 files changed:
CRM/Contact/DAO/SavedSearch.php
Civi/Api4/SavedSearch.php
ext/search_kit/ang/crmSearchAdmin.module.js
ext/search_kit/ang/crmSearchAdmin/searchList.controller.js [deleted file]
ext/search_kit/ang/crmSearchAdmin/searchList.html [deleted file]
ext/search_kit/ang/crmSearchAdmin/searchListing/afforms.html [new file with mode: 0644]
ext/search_kit/ang/crmSearchAdmin/searchListing/buttons.html [new file with mode: 0644]
ext/search_kit/ang/crmSearchAdmin/searchListing/crmSearchAdminSearchListing.component.js [new file with mode: 0644]
ext/search_kit/ang/crmSearchAdmin/searchListing/crmSearchAdminSearchListing.html [new file with mode: 0644]
ext/search_kit/ang/crmSearchAdmin/searchListing/displays.html [new file with mode: 0644]
ext/search_kit/css/crmSearchAdmin.css
xml/schema/Contact/SavedSearch.xml

index 84f3e161e6e8921e96c4acf70bae3f004b2a3376..c8fff17301053c06aa3792464d66ef5416506eda 100644 (file)
@@ -6,7 +6,7 @@
  *
  * Generated from xml/schema/CRM/Contact/SavedSearch.xml
  * DO NOT EDIT.  Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:5e5799b7755c435363f2c8cdafd13055)
+ * (GenCodeChecksum:bca384578ea59c0cf4f0a80617c788fe)
  */
 
 /**
@@ -219,6 +219,7 @@ class CRM_Contact_DAO_SavedSearch extends CRM_Core_DAO {
           'localizable' => 0,
           'html' => [
             'type' => 'Text',
+            'label' => ts("Label"),
           ],
           'add' => '5.32',
         ],
index ddf6704add3d725c016631d9c46e2d6194d286cc..74dd21cef1731aa9207ca67cb56ca2e57e708a6a 100644 (file)
@@ -16,7 +16,7 @@ namespace Civi\Api4;
  * Stores search parameters for populating smart groups with live results.
  *
  * @see https://docs.civicrm.org/user/en/latest/organising-your-data/smart-groups/
- * @searchable none
+ * @searchable secondary
  * @since 5.24
  * @package Civi\Api4
  */
index 52bad76c33757e849f50050ff0eb530ab62b9714..265d5708ac7544f2b05077d1443aec4ce91c6a9f 100644 (file)
 
     .config(function($routeProvider) {
       $routeProvider.when('/list', {
-        controller: 'searchList',
-        templateUrl: '~/crmSearchAdmin/searchList.html',
-        resolve: {
-          // Load data for lists
-          savedSearches: function(crmApi4) {
-            return crmApi4('SavedSearch', 'get', {
-              select: [
-                'id',
-                'name',
-                'label',
-                'api_entity',
-                'api_params',
-                'created_id.display_name',
-                'modified_id.display_name',
-                'created_date',
-                'modified_date',
-                'GROUP_CONCAT(display.name ORDER BY display.id) AS display_name',
-                'GROUP_CONCAT(display.label ORDER BY display.id) AS display_label',
-                'GROUP_CONCAT(display.type:icon ORDER BY display.id) AS display_icon',
-                'GROUP_CONCAT(display.acl_bypass ORDER BY display.id) AS display_acl_bypass',
-                'GROUP_CONCAT(DISTINCT group.title) AS groups'
-              ],
-              join: [['SearchDisplay AS display'], ['Group AS group']],
-              where: [['api_entity', 'IS NOT NULL']],
-              groupBy: ['id']
-            });
-          }
-        }
+        controller: function() {
+          searchEntity = 'SavedSearch';
+        },
+        template: '<crm-search-admin-search-listing></crm-search-admin-search-listing>',
       });
       $routeProvider.when('/create/:entity', {
         controller: 'searchCreate',
             key: info.alias,
             dataType: (info.fn && info.fn.dataType) || (info.field && info.field.data_type)
           }, defaults);
-        if (defaults.label) {
+        if (defaults.label === true) {
           values.label = getDefaultLabel(fieldExpr);
         }
         return values;
diff --git a/ext/search_kit/ang/crmSearchAdmin/searchList.controller.js b/ext/search_kit/ang/crmSearchAdmin/searchList.controller.js
deleted file mode 100644 (file)
index 2266b65..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-(function(angular, $, _) {
-  "use strict";
-
-  angular.module('crmSearchAdmin').controller('searchList', function($scope, savedSearches, crmApi4, searchMeta) {
-    var ts = $scope.ts = CRM.ts('org.civicrm.search_kit'),
-      ctrl = $scope.$ctrl = this;
-    $scope.formatDate = CRM.utils.formatDate;
-    this.savedSearches = savedSearches;
-    this.sortField = 'modified_date';
-    this.sortDir = true;
-    this.afformEnabled = CRM.crmSearchAdmin.afformEnabled;
-    this.afformAdminEnabled = CRM.crmSearchAdmin.afformAdminEnabled;
-
-    _.each(savedSearches, function(search) {
-      search.entity_title = searchMeta.getEntity(search.api_entity).title_plural;
-      search.permissionToEdit = CRM.checkPerm('all CiviCRM permissions and ACLs') || !_.includes(search.display_acl_bypass, true);
-      search.afform_count = 0;
-    });
-
-    this.searchPath = CRM.url('civicrm/search');
-    this.afformPath = CRM.url('civicrm/admin/afform');
-
-    this.encode = function(params) {
-      return encodeURI(angular.toJson(params));
-    };
-
-    // Change sort field/direction when clicking a column header
-    this.sortBy = function(col) {
-      ctrl.sortDir = ctrl.sortField === col ? !ctrl.sortDir : false;
-      ctrl.sortField = col;
-    };
-
-    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);
-      }
-    };
-
-    this.loadAfforms = function() {
-      if (ctrl.afforms || ctrl.afforms === null) {
-        return;
-      }
-      ctrl.afforms = null;
-      crmApi4('Afform', 'get', {
-        select: ['layout', 'name', 'title', 'server_route'],
-        where: [['type', '=', 'search']],
-        layoutFormat: 'html'
-      }).then(function(afforms) {
-        ctrl.afforms = {};
-        _.each(afforms, function(afform) {
-          var searchName = afform.layout.match(/<crm-search-display-[^>]+search-name[ ]*=[ ]*['"]([^"']+)/);
-          if (searchName) {
-            var search = _.find(ctrl.savedSearches, {name: searchName[1]});
-            if (search) {
-              search.afform_count++;
-              ctrl.afforms[searchName[1]] = ctrl.afforms[searchName[1]] || [];
-              ctrl.afforms[searchName[1]].push({
-                title: afform.title,
-                name: afform.name,
-                // FIXME: This is the view url, currently not exposed to the UI, as BS3 doesn't support submenus.
-                url: afform.server_route ? CRM.url(afform.server_route) : null
-              });
-            }
-          }
-        });
-      });
-    };
-
-  });
-
-})(angular, CRM.$, CRM._);
diff --git a/ext/search_kit/ang/crmSearchAdmin/searchList.html b/ext/search_kit/ang/crmSearchAdmin/searchList.html
deleted file mode 100644 (file)
index 9d27013..0000000
+++ /dev/null
@@ -1,124 +0,0 @@
-<div id="bootstrap-theme" class="crm-search crm-search-admin-list">
-  <h1 crm-page-title>{{:: ts('Saved Searches') }}</h1>
-  <div class="form-inline">
-    <label for="search-list-filter">{{:: ts('Filter') }}</label>
-    <input class="form-control" type="search" id="search-list-filter" ng-model="$ctrl.searchFilter" placeholder="&#xf002">
-    <a class="btn btn-primary pull-right" href="#/create/Contact/">
-      <i class="crm-i fa-plus"></i>
-      {{:: ts('New Search') }}
-    </a>
-  </div>
-  <table>
-    <thead>
-      <tr>
-        <th title="{{:: ts('Click to sort') }}" ng-click="$ctrl.sortBy('label')">
-          <i class="crm-i fa-sort disabled" ng-if="$ctrl.sortField !== 'label'"></i>
-          <i class="crm-i fa-sort-{{ $ctrl.sortDir ? 'asc' : 'desc' }}" ng-if="$ctrl.sortField === 'label'"></i>
-          {{:: ts('Label') }}
-        </th>
-        <th title="{{:: ts('Click to sort') }}" ng-click="$ctrl.sortBy('entity_title')">
-          <i class="crm-i fa-sort disabled" ng-if="$ctrl.sortField !== 'entity_title'"></i>
-          <i class="crm-i fa-sort-{{ $ctrl.sortDir ? 'asc' : 'desc' }}" ng-if="$ctrl.sortField === 'entity_title'"></i>
-          {{:: ts('For') }}
-        </th>
-        <th title="{{:: ts('Click to sort') }}" ng-click="$ctrl.sortBy('display_name.length')">
-          <i class="crm-i fa-sort disabled" ng-if="$ctrl.sortField !== 'display_name.length'"></i>
-          <i class="crm-i fa-sort-{{ $ctrl.sortDir ? 'asc' : 'desc' }}" ng-if="$ctrl.sortField === 'display_name.length'"></i>
-          {{:: ts('Displays') }}
-        </th>
-        <th title="{{:: ts('Click to sort') }}" ng-click="$ctrl.sortBy('groups[0]')">
-          <i class="crm-i fa-sort disabled" ng-if="$ctrl.sortField !== 'groups[0]'"></i>
-          <i class="crm-i fa-sort-{{ $ctrl.sortDir ? 'asc' : 'desc' }}" ng-if="$ctrl.sortField === 'groups[0]'"></i>
-          {{:: ts('Smart Group') }}
-        </th>
-        <th ng-if="$ctrl.afformEnabled" ng-click="$ctrl.sortBy('afform_count')">
-          <i class="crm-i fa-sort disabled" ng-if="$ctrl.sortField !== 'afform_count'"></i>
-          <i class="crm-i fa-sort-{{ $ctrl.sortDir ? 'asc' : 'desc' }}" ng-if="$ctrl.sortField === 'afform_count'"></i>
-          {{:: ts('Forms') }}
-        </th>
-        <th title="{{:: ts('Click to sort') }}" ng-click="$ctrl.sortBy('created_date')">
-          <i class="crm-i fa-sort disabled" ng-if="$ctrl.sortField !== 'created_date'"></i>
-          <i class="crm-i fa-sort-{{ $ctrl.sortDir ? 'asc' : 'desc' }}" ng-if="$ctrl.sortField === 'created_date'"></i>
-          {{:: ts('Created') }}
-        </th>
-        <th title="{{:: ts('Click to sort') }}" ng-click="$ctrl.sortBy('modified_date')">
-          <i class="crm-i fa-sort disabled" ng-if="$ctrl.sortField !== 'modified_date'"></i>
-          <i class="crm-i fa-sort-{{ $ctrl.sortDir ? 'asc' : 'desc' }}" ng-if="$ctrl.sortField === 'modified_date'"></i>
-          {{:: ts('Last Modified') }}
-        </th>
-        <th></th>
-      </tr>
-    </thead>
-    <tbody>
-      <tr ng-repeat="search in $ctrl.savedSearches | filter:$ctrl.searchFilter | orderBy:$ctrl.sortField:$ctrl.sortDir">
-        <td>{{:: search.label }}</td>
-        <td>{{:: search.entity_title }}</td>
-        <td>
-          <div class="btn-group">
-            <button type="button" disabled ng-if="!search.display_name" class="btn btn-xs dropdown-toggle btn-primary-outline">
-              {{:: ts('0 Displays') }}
-            </button>
-            <button type="button" ng-if="search.display_name" class="btn btn-xs dropdown-toggle btn-primary-outline" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-              {{:: search.display_name.length === 1 ? ts('1 Display') : ts('%1 Displays', {1: search.display_name.length}) }} <span class="caret"></span>
-            </button>
-            <ul class="dropdown-menu" ng-if=":: search.display_name.length">
-              <li ng-repeat="display_name in search.display_name" ng-class="{disabled: search.display_acl_bypass[$index]}" title="{{:: search.display_acl_bypass[$index] ? ts('Display has permissions disabled') : ts('View display') }}">
-                <a ng-href="{{:: search.display_acl_bypass[$index] ? '' : $ctrl.searchPath + '#/display/' + search.name + '/' + display_name }}" target="_blank">
-                  <i class="fa {{:: search.display_icon[$index] }}"></i>
-                  {{:: search.display_label[$index] }}
-                </a>
-              </li>
-            </ul>
-          </div>
-        </td>
-        <td>{{:: search.groups.join(', ') }}</td>
-        <td ng-if="::$ctrl.afformEnabled">
-          <div class="btn-group">
-            <button type="button" ng-click="$ctrl.loadAfforms()" ng-if="search.display_name" class="btn btn-xs dropdown-toggle btn-primary-outline" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-              {{ $ctrl.afforms ? (search.afform_count === 1 ? ts('1 Form') : ts('%1 Forms', {1: search.afform_count})) : ts('Forms...') }}
-              <span class="caret"></span>
-            </button>
-            <ul class="dropdown-menu">
-              <li ng-repeat="display_name in search.display_name" ng-if="::$ctrl.afformAdminEnabled">
-                <a href="{{:: $ctrl.afformPath + '#/create/search/' + search.name + '.' + display_name }}">
-                  <i class="fa fa-plus"></i> {{:: ts('Create form for %1', {1: search.display_label[$index]}) }}
-                </a>
-              </li>
-              <li class="divider" role="separator" ng-if="::$ctrl.afformAdminEnabled"></li>
-              <li ng-if="!search.afform_count" class="disabled">
-                <a href>
-                  <i ng-if="!$ctrl.afforms" class="crm-i fa-spinner fa-spin"></i>
-                  <em ng-if="$ctrl.afforms && !$ctrl.afforms[search.name]">{{:: ts('None Found') }}</em>
-                </a>
-              </li>
-              <li ng-if="$ctrl.afforms" ng-repeat="afform in $ctrl.afforms[search.name]" title="{{:: ts('Edit form') }}">
-                <a href="{{:: $ctrl.afformPath + '#/edit/' + afform.name }}">
-                  <i class="crm-i fa-pencil-square-o"></i>
-                  {{:: afform.title }}
-                </a>
-              </li>
-            </ul>
-          </div>
-        </td>
-        <td title="{{:: formatDate(search.created_date, null, true) }}">
-          {{:: search['created_id.display_name'] ? ts('%1 by %2', {1: formatDate(search.created_date), 2: search['created_id.display_name']}) : formatDate(search.created_date) }}
-        </td>
-        <td title="{{:: formatDate(search.modified_date, null, true) }}">
-          {{:: search['modified_id.display_name'] ? ts('%1 by %2', {1: formatDate(search.modified_date), 2: search['modified_id.display_name']}) : formatDate(search.modified_date) }}
-        </td>
-        <td class="text-right">
-          <a class="btn btn-xs btn-default" href="#/edit/{{:: search.id }}" ng-if="search.permissionToEdit">{{:: ts('Edit') }}</a>
-          <a class="btn btn-xs btn-default" href="#/create/{{:: search.api_entity + '?params=' + $ctrl.encode(search.api_params) }}">{{:: ts('Clone') }}</a>
-          <a href class="btn btn-xs btn-danger" 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.')}}
-          </p>
-        </td>
-      </tr>
-    </tbody>
-  </table>
-</div>
diff --git a/ext/search_kit/ang/crmSearchAdmin/searchListing/afforms.html b/ext/search_kit/ang/crmSearchAdmin/searchListing/afforms.html
new file mode 100644 (file)
index 0000000..12f6a40
--- /dev/null
@@ -0,0 +1,26 @@
+<div class="btn-group" ng-if=":: row.display_name.raw">
+  <button type="button" ng-click="$ctrl.loadAfforms(); row.openAfformMenu = true;" class="btn btn-xs dropdown-toggle btn-primary-outline" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+    {{ $ctrl.afforms ? (row.afform_count === 1 ? ts('1 Form') : ts('%1 Forms', {1: row.afform_count})) : ts('Forms...') }}
+    <span class="caret"></span>
+  </button>
+  <ul class="dropdown-menu" ng-if=":: row.openAfformMenu">
+    <li ng-repeat="display_name in row.display_name.raw" ng-if="::$ctrl.afformAdminEnabled">
+      <a target="_blank" href="{{:: $ctrl.afformPath + '#/create/search/' + row.name.raw + '.' + display_name }}">
+        <i class="fa fa-plus"></i> {{:: ts('Create form for %1', {1: row.display_label.raw[$index]}) }}
+      </a>
+    </li>
+    <li class="divider" role="separator" ng-if="::$ctrl.afformAdminEnabled"></li>
+    <li ng-if="!row.afform_count" class="disabled">
+      <a href>
+        <i ng-if="!$ctrl.afforms" class="crm-i fa-spinner fa-spin"></i>
+        <em ng-if="$ctrl.afforms && !$ctrl.afforms[row.name.raw]">{{:: ts('None Found') }}</em>
+      </a>
+    </li>
+    <li ng-if="$ctrl.afforms" ng-repeat="afform in $ctrl.afforms[row.name.raw]" title="{{:: ts('Edit form') }}">
+      <a target="_blank" href="{{:: $ctrl.afformPath + '#/edit/' + afform.name }}">
+        <i class="crm-i fa-pencil-square-o"></i>
+        {{:: afform.title }}
+      </a>
+    </li>
+  </ul>
+</div>
diff --git a/ext/search_kit/ang/crmSearchAdmin/searchListing/buttons.html b/ext/search_kit/ang/crmSearchAdmin/searchListing/buttons.html
new file mode 100644 (file)
index 0000000..ee5eff3
--- /dev/null
@@ -0,0 +1,9 @@
+<a class="btn btn-xs btn-default" href="#/edit/{{:: row.id.raw }}" ng-if="row.permissionToEdit">
+  {{:: ts('Edit') }}
+</a>
+<a class="btn btn-xs btn-default" href="#/create/{{:: row.api_entity.raw + '?params=' + $ctrl.encode(row.api_params.raw) }}">
+  {{:: ts('Clone') }}
+</a>
+<a href class="btn btn-xs btn-danger" crm-confirm="{type: 'delete', obj: row}" on-yes="$ctrl.deleteSearch(row)">
+  {{:: ts('Delete') }}
+</a>
diff --git a/ext/search_kit/ang/crmSearchAdmin/searchListing/crmSearchAdminSearchListing.component.js b/ext/search_kit/ang/crmSearchAdmin/searchListing/crmSearchAdminSearchListing.component.js
new file mode 100644 (file)
index 0000000..038ec00
--- /dev/null
@@ -0,0 +1,161 @@
+(function(angular, $, _) {
+  "use strict";
+
+  // Specialized searchDisplay, only used by Admins
+  angular.module('crmSearchAdmin').component('crmSearchAdminSearchListing', {
+    templateUrl: '~/crmSearchAdmin/searchListing/crmSearchAdminSearchListing.html',
+    controller: function($scope, crmApi4, crmStatus, searchMeta, searchDisplayBaseTrait, searchDisplaySortableTrait) {
+      var ts = $scope.ts = CRM.ts('org.civicrm.search_kit'),
+        // Mix in traits to this controller
+        ctrl = angular.extend(this, searchDisplayBaseTrait, searchDisplaySortableTrait);
+
+      this.searchDisplayPath = CRM.url('civicrm/search');
+      this.afformPath = CRM.url('civicrm/admin/afform');
+      this.afformEnabled = CRM.crmSearchAdmin.afformEnabled;
+      this.afformAdminEnabled = CRM.crmSearchAdmin.afformAdminEnabled;
+
+      this.apiEntity = 'SavedSearch';
+      this.search = {
+        api_entity: 'SavedSearch',
+        api_params: {
+          version: 4,
+          select: [
+            'id',
+            'name',
+            'label',
+            'api_entity',
+            'api_entity:label',
+            'api_params',
+            'created_date',
+            'modified_date',
+            'GROUP_CONCAT(display.name ORDER BY display.id) AS display_name',
+            'GROUP_CONCAT(display.label ORDER BY display.id) AS display_label',
+            'GROUP_CONCAT(display.type:icon ORDER BY display.id) AS display_icon',
+            'GROUP_CONCAT(display.acl_bypass ORDER BY display.id) AS display_acl_bypass',
+            'GROUP_CONCAT(DISTINCT group.title) AS groups'
+          ],
+          join: [['SearchDisplay AS display'], ['Group AS group']],
+          where: [['api_entity', 'IS NOT NULL']],
+          groupBy: ['id']
+        }
+      };
+
+      this.$onInit = function() {
+        buildDisplaySettings();
+        this.initializeDisplay($scope, $());
+      };
+
+      this.onPostRun.push(function(result) {
+        _.each(result, function(row) {
+          row.permissionToEdit = CRM.checkPerm('all CiviCRM permissions and ACLs') || !_.includes(row.display_acl_bypass.raw, true);
+          // Saves rendering cycles to not show an empty menu of search displays
+          if (!row.display_name.raw) {
+            row.openDisplayMenu = false;
+          }
+        });
+        updateAfformCounts();
+      });
+
+      this.encode = function(params) {
+        return encodeURI(angular.toJson(params));
+      };
+
+      this.deleteSearch = function(search) {
+        crmStatus({start: ts('Deleting...'), success: ts('Search Deleted')},
+          crmApi4('SavedSearch', 'delete', {where: [['id', '=', search.id.raw]]}).then(function() {
+            ctrl.rowCount = null;
+            ctrl.runSearch();
+          })
+        );
+      };
+
+      function buildDisplaySettings() {
+        ctrl.display = {
+          type: 'table',
+          settings: {
+            limit: CRM.crmSearchAdmin.defaultPagerSize,
+            pager: {show_count: true, expose_limit: true},
+            actions: false,
+            sort: [['modified_date', 'DESC']],
+            columns: [
+              searchMeta.fieldToColumn('label', {
+                label: true,
+                title: ts('Edit Label'),
+                editable: {entity: 'SavedSearch', id: 'id', name: 'label', value: 'label'}
+              }),
+              searchMeta.fieldToColumn('api_entity:label', {
+                label: ts('For'),
+              }),
+              {
+                type: 'include',
+                label: ts('Displays'),
+                path: '~/crmSearchAdmin/searchListing/displays.html'
+              },
+              searchMeta.fieldToColumn('GROUP_CONCAT(DISTINCT group.title) AS groups', {
+                label: ts('Smart Group')
+              }),
+              searchMeta.fieldToColumn('created_date', {
+                label: ts('Created'),
+                dataType: 'Date',
+                rewrite: ts('%1 by %2', {1: '[created_date]', 2: '[created_id.display_name]'})
+              }),
+              searchMeta.fieldToColumn('modified_date', {
+                label: ts('Last Modified'),
+                dataType: 'Date',
+                rewrite: ts('%1 by %2', {1: '[modified_date]', 2: '[modified_id.display_name]'})
+              }),
+              {
+                type: 'include',
+                alignment: 'text-right',
+                path: '~/crmSearchAdmin/searchListing/buttons.html'
+              }
+            ]
+          }
+        };
+        if (ctrl.afformEnabled) {
+          ctrl.display.settings.columns.splice(3, 0, {
+            type: 'include',
+            label: ts('Forms'),
+            path: '~/crmSearchAdmin/searchListing/afforms.html'
+          });
+        }
+        ctrl.settings = ctrl.display.settings;
+      }
+
+      this.loadAfforms = function() {
+        if (ctrl.afforms || ctrl.afforms === null) {
+          return;
+        }
+        ctrl.afforms = null;
+        crmApi4('Afform', 'get', {
+          select: ['layout', 'name', 'title', 'server_route'],
+          where: [['type', '=', 'search']],
+          layoutFormat: 'html'
+        }).then(function(afforms) {
+          ctrl.afforms = {};
+          _.each(afforms, function(afform) {
+            var searchName = afform.layout.match(/<crm-search-display-[^>]+search-name[ ]*=[ ]*['"]([^"']+)/);
+            if (searchName) {
+              ctrl.afforms[searchName[1]] = ctrl.afforms[searchName[1]] || [];
+              ctrl.afforms[searchName[1]].push({
+                title: afform.title,
+                name: afform.name,
+                // FIXME: This is the view url, currently not exposed to the UI, as BS3 doesn't support submenus.
+                url: afform.server_route ? CRM.url(afform.server_route) : null
+              });
+            }
+          });
+          updateAfformCounts();
+        });
+      };
+
+      function updateAfformCounts() {
+        _.each(ctrl.results, function(row) {
+          row.afform_count = ctrl.afforms && ctrl.afforms[row.name.raw] && ctrl.afforms[row.name.raw].length || 0;
+        });
+      }
+
+    }
+  });
+
+})(angular, CRM.$, CRM._);
diff --git a/ext/search_kit/ang/crmSearchAdmin/searchListing/crmSearchAdminSearchListing.html b/ext/search_kit/ang/crmSearchAdmin/searchListing/crmSearchAdminSearchListing.html
new file mode 100644 (file)
index 0000000..b2e0db8
--- /dev/null
@@ -0,0 +1,12 @@
+<div id="bootstrap-theme" class="crm-search">
+  <h1 crm-page-title>{{:: ts('Saved Searches') }}</h1>
+  <div class="form-inline">
+    <label for="search-list-filter">{{:: ts('Filter') }}</label>
+    <input class="form-control" type="search" id="search-list-filter" ng-model="$ctrl.filters.label" placeholder="&#xf002">
+    <a class="btn btn-primary pull-right" href="#/create/Contact/">
+      <i class="crm-i fa-plus"></i>
+      {{:: ts('New Search') }}
+    </a>
+  </div>
+  <div ng-include="'~/crmSearchDisplayTable/crmSearchDisplayTable.html'"></div>
+</div>
diff --git a/ext/search_kit/ang/crmSearchAdmin/searchListing/displays.html b/ext/search_kit/ang/crmSearchAdmin/searchListing/displays.html
new file mode 100644 (file)
index 0000000..c16c66e
--- /dev/null
@@ -0,0 +1,16 @@
+<div class="btn-group">
+  <button type="button" disabled ng-if="!row.display_name.raw" class="btn btn-xs dropdown-toggle btn-primary-outline">
+    {{:: ts('0 Displays') }}
+  </button>
+  <button type="button" ng-if=":: row.display_name.raw" ng-click="row.openDisplayMenu = true" class="btn btn-xs dropdown-toggle btn-primary-outline" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+    {{:: row.display_name.raw.length === 1 ? ts('1 Display') : ts('%1 Displays', {1: row.display_name.raw.length}) }} <span class="caret"></span>
+  </button>
+  <ul class="dropdown-menu" ng-if=":: row.openDisplayMenu">
+    <li ng-repeat="display_name in row.display_name.raw" ng-class="{disabled: row.display_acl_bypass.raw[$index]}" title="{{:: row.display_acl_bypass.raw[$index] ? ts('Display has permissions disabled') : ts('View display') }}">
+      <a ng-href="{{:: row.display_acl_bypass.raw[$index] ? '' : $ctrl.searchDisplayPath + '#/display/' + row.name.raw + '/' + display_name }}" target="_blank">
+        <i class="fa {{:: row.display_icon.rw[$index] }}"></i>
+        {{:: row.display_label.raw[$index] }}
+      </a>
+    </li>
+  </ul>
+</div>
index cf3b0743c9c1122aa9f5716f3c6b59f705b90332..bdcf4123c5c3068fd4106062c46490910af3b159 100644 (file)
@@ -1,13 +1,3 @@
-#bootstrap-theme.crm-search-admin-list th[ng-click] {
-  cursor: pointer;
-}
-#bootstrap-theme.crm-search-admin-list th i.fa-sort-desc,
-#bootstrap-theme.crm-search-admin-list th i.fa-sort-asc {
-  color: #1a5a82;
-}
-#bootstrap-theme.crm-search-admin-list th:not(:hover) i.fa-sort {
-  opacity: .5;
-}
 
 #bootstrap-theme .crm-search-criteria-column {
   min-width: 500px;
index d0146e6709e86ac979af167b6292b6b80299534a..77f5b37229671741105f1270d3a126b5951b2cd5 100644 (file)
@@ -49,6 +49,7 @@
     <default>NULL</default>
     <comment>Administrative label for search</comment>
     <html>
+      <label>Label</label>
       <type>Text</type>
     </html>
     <add>5.32</add>