Search ext: Add List display
authorColeman Watts <coleman@civicrm.org>
Thu, 19 Nov 2020 18:25:26 +0000 (13:25 -0500)
committerColeman Watts <coleman@civicrm.org>
Thu, 19 Nov 2020 18:25:26 +0000 (13:25 -0500)
Breaks search displays into their own modules, one per display type.
This gives finer-grained dependency management per display type.

24 files changed:
css/civicrm.css
ext/search/ang/crmSearchAdmin.ang.php
ext/search/ang/crmSearchAdmin.module.js
ext/search/ang/crmSearchAdmin/crmSearchAdmin.component.js
ext/search/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js
ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayList.component.js [new file with mode: 0644]
ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayList.html [new file with mode: 0644]
ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayTable.component.js
ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayTable.html
ext/search/ang/crmSearchDisplay.ang.php
ext/search/ang/crmSearchDisplay.module.js
ext/search/ang/crmSearchDisplay/Pager.html [new file with mode: 0644]
ext/search/ang/crmSearchDisplayList.ang.php [new file with mode: 0644]
ext/search/ang/crmSearchDisplayList.module.js [new file with mode: 0644]
ext/search/ang/crmSearchDisplayList/crmSearchDisplayList.component.js [new file with mode: 0644]
ext/search/ang/crmSearchDisplayList/crmSearchDisplayList.html [new file with mode: 0644]
ext/search/ang/crmSearchDisplayList/crmSearchDisplayListItems.html [new file with mode: 0644]
ext/search/ang/crmSearchDisplayTable.ang.php [new file with mode: 0644]
ext/search/ang/crmSearchDisplayTable.module.js [new file with mode: 0644]
ext/search/ang/crmSearchDisplayTable/crmSearchDisplayTable.component.js [moved from ext/search/ang/crmSearchDisplay/crmSearchDisplayTable.component.js with 65% similarity]
ext/search/ang/crmSearchDisplayTable/crmSearchDisplayTable.html [moved from ext/search/ang/crmSearchDisplay/crmSearchDisplayTable.html with 67% similarity]
ext/search/ang/crmSearchKit.ang.php
ext/search/ang/crmSearchPage.ang.php
ext/search/managed/SearchDisplayType.mgd.php

index 1a685a272970b4b5891a24f0334d692e7d52d492..997e77b8b0f94a7779d6f2ccf65785a5eaff9469 100644 (file)
   box-sizing: content-box;
 }
 
+.crm-container .crm-inline-block {
+  display: inline-block;
+}
+
 div.crm-container label {
   font-weight: normal;
   display: inline;
index 2a3b21d6b9e8899b625da273a67fc419ffac89b7..9176b14b29b8d805a81e2845e18f4073e3955d1b 100644 (file)
@@ -13,6 +13,6 @@ return [
     'ang/crmSearchAdmin',
   ],
   'basePages' => ['civicrm/admin/search'],
-  'requires' => ['crmUi', 'crmUtil', 'ngRoute', 'ui.sortable', 'ui.bootstrap', 'api4', 'crmSearchDisplay', 'crmSearchActions', 'crmSearchKit', 'crmRouteBinder'],
+  'requires' => ['crmUi', 'crmUtil', 'ngRoute', 'ui.sortable', 'ui.bootstrap', 'api4', 'crmSearchActions', 'crmSearchKit', 'crmRouteBinder'],
   'settingsFactory' => ['\Civi\Search\Admin', 'getAdminSettings'],
 ];
index c844282e26c5b73e7c5570218614da09f3815d10..d189b4865a6d24cc324e1206f74645038ba10105 100644 (file)
           return field;
         }
       }
+      function parseExpr(expr) {
+        var result = {fn: null, modifier: ''},
+          fieldName = expr,
+          bracketPos = expr.indexOf('(');
+        if (bracketPos >= 0) {
+          var parsed = expr.substr(bracketPos).match(/[ ]?([A-Z]+[ ]+)?([\w.:]+)/);
+          fieldName = parsed[2];
+          result.fn = _.find(CRM.crmSearchAdmin.functions, {name: expr.substring(0, bracketPos)});
+          result.modifier = _.trim(parsed[1]);
+        }
+        result.field = expr ? getField(fieldName, searchEntity) : undefined;
+        if (result.field) {
+          var split = fieldName.split(':'),
+            prefixPos = split[0].lastIndexOf(result.field.name);
+          result.path = split[0];
+          result.prefix = prefixPos > 0 ? result.path.substring(0, prefixPos) : '';
+          result.suffix = !split[1] ? '' : ':' + split[1];
+        }
+        return result;
+      }
       return {
         getEntity: getEntity,
         getField: getField,
-        parseExpr: function(expr) {
-          var result = {fn: null, modifier: ''},
-            fieldName = expr,
-            bracketPos = expr.indexOf('(');
-          if (bracketPos >= 0) {
-            var parsed = expr.substr(bracketPos).match(/[ ]?([A-Z]+[ ]+)?([\w.:]+)/);
-            fieldName = parsed[2];
-            result.fn = _.find(CRM.crmSearchAdmin.functions, {name: expr.substring(0, bracketPos)});
-            result.modifier = _.trim(parsed[1]);
-          }
-          result.field = expr ? getField(fieldName, searchEntity) : undefined;
-          if (result.field) {
-            var split = fieldName.split(':'),
-              prefixPos = split[0].lastIndexOf(result.field.name);
-            result.path = split[0];
-            result.prefix = prefixPos > 0 ? result.path.substring(0, prefixPos) : '';
-            result.suffix = !split[1] ? '' : ':' + split[1];
+        parseExpr: parseExpr,
+        getDefaultLabel: function(col) {
+          var info = parseExpr(col),
+            label = info.field.label;
+          if (info.fn) {
+            label = '(' + info.fn.title + ') ' + label;
           }
-          return result;
+          return label;
         },
         // Find all possible search columns that could serve as contact_id for a smart group
         getSmartGroupColumns: function(api_entity, api_params) {
index f9f72af6b82f7b45cad0ae89ee1aebd44eda79e4..61786f261d635c491a076914a2aa99dd52fd303c 100644 (file)
         return ctrl.allRowsSelected || _.includes(ctrl.selectedRows, row.id);
       };
 
-      this.getFieldLabel = function(col) {
-        var info = searchMeta.parseExpr(col),
-          label = info.field.label;
-        if (info.fn) {
-          label = '(' + info.fn.title + ') ' + label;
-        }
-        return label;
-      };
+      this.getFieldLabel = searchMeta.getDefaultLabel;
 
       // Is a column eligible to use an aggregate function?
       this.canAggregate = function(col) {
index 4bdf10f9db5250bc7062853834ad002e4988ec87..0def0e277027fddad4e809c69e51b0098f170cb3 100644 (file)
       html += '</div>';
       return html;
     },
-    controller: function($scope, $timeout) {
+    controller: function($scope, $timeout, searchMeta) {
       var ts = $scope.ts = CRM.ts(),
         ctrl = this;
 
+      function fieldToColumn(fieldExpr) {
+        var info = searchMeta.parseExpr(fieldExpr);
+        return {
+          expr: fieldExpr,
+          label: searchMeta.getDefaultLabel(fieldExpr),
+          dataType: (info.fn && info.fn.name === 'COUNT') ? 'Integer' : info.field.data_type
+        };
+      }
+
+      // Helper function to sort active from hidden columns and initialize each column with defaults
+      this.initColumns = function() {
+        if (!ctrl.display.settings.columns) {
+          ctrl.display.settings.columns = _.transform(ctrl.savedSearch.api_params.select, function(columns, fieldExpr) {
+            columns.push(fieldToColumn(fieldExpr));
+          });
+          return [];
+        } else {
+          var activeColumns = _.collect(ctrl.display.settings.columns, 'expr'),
+            hiddenColumns = _.transform(ctrl.savedSearch.api_params.select, function(hiddenColumns, fieldExpr) {
+            if (!_.includes(activeColumns, fieldExpr)) {
+              hiddenColumns.push(fieldToColumn(fieldExpr));
+            }
+          });
+          _.each(activeColumns, function(fieldExpr, index) {
+            if (!_.includes(ctrl.savedSearch.api_params.select, fieldExpr)) {
+              ctrl.display.settings.columns.splice(index, 1);
+            }
+          });
+          return hiddenColumns;
+        }
+      };
+
+      // Return all possible links to main entity or join entities
+      this.getLinks = function() {
+        var links = _.cloneDeep(searchMeta.getEntity(ctrl.savedSearch.api_entity).paths || []);
+        _.each(ctrl.savedSearch.api_params.join, function(join) {
+          var joinName = join[0].split(' AS '),
+            joinEntity = searchMeta.getEntity(joinName[0]);
+          _.each(joinEntity.paths, function(path) {
+            var link = _.cloneDeep(path);
+            link.path = link.path.replace(/\[/g, '[' + joinName[1] + '.');
+            links.push(link);
+          });
+        });
+        return links;
+      };
+
       this.preview = this.stale = false;
 
       this.previewDisplay = function() {
diff --git a/ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayList.component.js b/ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayList.component.js
new file mode 100644 (file)
index 0000000..9697a68
--- /dev/null
@@ -0,0 +1,65 @@
+(function(angular, $, _) {
+  "use strict";
+
+  angular.module('crmSearchAdmin').component('searchAdminDisplayList', {
+    bindings: {
+      display: '<',
+      apiEntity: '<',
+      apiParams: '<'
+    },
+    require: {
+      crmSearchAdminDisplay: '^crmSearchAdminDisplay'
+    },
+    templateUrl: '~/crmSearchAdmin/displays/searchAdminDisplayList.html',
+    controller: function($scope, searchMeta) {
+      var ts = $scope.ts = CRM.ts(),
+        ctrl = this;
+      this.getFieldLabel = searchMeta.getDefaultLabel;
+
+      this.sortableOptions = {
+        connectWith: '.crm-search-admin-edit-columns',
+        containment: '.crm-search-admin-edit-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.symbols = {
+        ul: [
+          {char: '', label: ts('Default')},
+          {char: 'none', label: ts('None ( )')},
+          {char: 'circle', label: ts('Circle')},
+          {char: 'square', label: ts('Square')},
+        ],
+        ol: [
+          {char: '', label: ts('Numbered (1. 2. 3.)')},
+          {char: 'none', label: ts('None ( )')},
+          {char: 'lower-latin', label: ts('Lowercase (a. b. c.)')},
+          {char: 'upper-latin', label: ts('Uppercase (A. B. C.)')},
+          {char: 'upper-roman', label: ts('Roman (I. II. III.)')},
+        ]
+      };
+
+      this.$onInit = function () {
+        if (!ctrl.display.settings) {
+          ctrl.display.settings = {
+            style: 'ul',
+            limit: 20,
+            pager: true
+          };
+        }
+        ctrl.hiddenColumns = ctrl.crmSearchAdminDisplay.initColumns();
+        ctrl.links = ctrl.crmSearchAdminDisplay.getLinks();
+      };
+
+    }
+  });
+
+})(angular, CRM.$, CRM._);
diff --git a/ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayList.html b/ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayList.html
new file mode 100644 (file)
index 0000000..926a595
--- /dev/null
@@ -0,0 +1,62 @@
+<fieldset>
+  <div class="form-inline">
+    <label for="crm-search-admin-display-style">{{:: ts('Style:') }}</label>
+    <select id="crm-search-admin-display-style" class="form-control" ng-model="$ctrl.display.settings.style" ng-change="$ctrl.display.settings.symbol = ''">
+      <option value="ul">{{:: ts('Bulleted') }}</option>
+      <option value="ol">{{:: ts('Numbered') }}</option>
+    </select>
+    <label for="crm-search-admin-display-symbol">{{:: ts('Symbol:') }}</label>
+    <select id="crm-search-admin-display-symbol" class="form-control" ng-model="$ctrl.display.settings.symbol">
+      <option ng-repeat="symbol in $ctrl.symbols[$ctrl.display.settings.style]" value="{{ symbol.char }}">
+        {{ symbol.label }}
+      </option>
+    </select>
+  <div class="form-inline">
+  </div>
+    <label for="crm-search-admin-display-limit">{{:: ts('Results to display (0 for no limit):') }}</label>
+    <input id="crm-search-admin-display-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-edit-columns-wrapper">
+  <fieldset class="crm-search-admin-edit-columns" ng-model="$ctrl.display.settings.columns" ui-sortable="$ctrl.sortableOptions">
+    <legend>{{:: ts('Fields') }}</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" >
+        <label ng-show="col.label.length" title="{{:: ts('Show label for every record even when this field is blank') }}"><input type="checkbox" ng-model="col.forceLabel"> {{:: ts('Always show') }}</label>
+        <button class="btn-xs pull-right" ng-click="$ctrl.removeCol($index)" title="{{:: ts('Hide') }}">
+          <i class="crm-i fa-ban"></i>
+        </button>
+      </div>
+      <div class="form-inline">
+        <label>{{:: ts('Prefix:') }}</label>
+        <input class="form-control" ng-model="col.prefix" size="4">
+        <label>{{:: ts('Suffix:') }}</label>
+        <input class="form-control" ng-model="col.suffix" size="4">
+        <label><input type="checkbox" ng-model="col.break"> {{:: ts('New line') }}</label>
+      </div>
+      <div class="form-inline">
+        <label>{{:: ts('Link:') }}</label>
+        <crm-search-admin-link-select column="col" links="$ctrl.links"></crm-search-admin-link-select>
+      </div>
+      <div class="form-inline">
+        <label>{{:: ts('Tooltip:') }}</label>
+        <input class="form-control" type="text" ng-model="col.title" />
+      </div>
+    </fieldset>
+  </fieldset>
+  <fieldset class="crm-search-admin-edit-columns" ng-model="$ctrl.hiddenColumns" ui-sortable="$ctrl.sortableOptions">
+    <legend>{{:: ts('Hidden Fields') }}</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>
index 78fb6450da2c74747f69a20f47a2f7ce6eaea8e9..3ef0cd702a451206f9296795a7114636d5426cad 100644 (file)
@@ -8,21 +8,13 @@
       apiParams: '<'
     },
     require: {
-      crmSearchAdmin: '^crmSearchAdmin'
+      crmSearchAdminDisplay: '^crmSearchAdminDisplay'
     },
     templateUrl: '~/crmSearchAdmin/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.getFieldLabel = searchMeta.getDefaultLabel;
 
       this.sortableOptions = {
         connectWith: '.crm-search-admin-edit-columns',
       };
 
       this.$onInit = function () {
-        ctrl.getFieldLabel = ctrl.crmSearchAdmin.getFieldLabel;
         if (!ctrl.display.settings) {
           ctrl.display.settings = {
             limit: 20,
             pager: true
           };
         }
-        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);
-            }
-          });
-        }
-        ctrl.links = _.cloneDeep(searchMeta.getEntity(ctrl.apiEntity).paths || []);
-        _.each(ctrl.apiParams.join, function(join) {
-          var joinName = join[0].split(' AS '),
-            joinEntity = searchMeta.getEntity(joinName[0]);
-          _.each(joinEntity.paths, function(path) {
-            var link = _.cloneDeep(path);
-            link.path = link.path.replace(/\[/g, '[' + joinName[1] + '.');
-            ctrl.links.push(link);
-          });
-        });
+        ctrl.hiddenColumns = ctrl.crmSearchAdminDisplay.initColumns();
+        ctrl.links = ctrl.crmSearchAdminDisplay.getLinks();
       };
 
     }
index 66a15a4286f569171ce9f80d393f1541847bfcbb..18e391633dc559900954abf5f6c5d4ea4418bf00 100644 (file)
@@ -1,9 +1,9 @@
 <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>
-    <label><input type="checkbox" ng-model="$ctrl.display.settings.actions"> {{ ts('Enable Actions') }}</label>
+    <label for="crm-search-admin-display-limit">{{:: ts('Results to display (0 for no limit):') }}</label>
+    <input id="crm-search-admin-display-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>
+    <label><input type="checkbox" ng-model="$ctrl.display.settings.actions"> {{:: ts('Enable Actions') }}</label>
   </div>
 </fieldset>
 <div class="crm-flex-box crm-search-admin-edit-columns-wrapper">
     <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" />
+        <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>
       <div class="form-inline">
-        <label>{{ ts('Link:') }}</label>
+        <label>{{:: ts('Alignment:') }}</label>
+        <select ng-model="col.alignment" class="form-control">
+          <option value="">{{:: ts('Left') }}</option>
+          <option value="text-center">{{:: ts('Center') }}</option>
+          <option value="text-right">{{:: ts('Right') }}</option>
+        </select>
+      </div>
+      <div class="form-inline">
+        <label>{{:: ts('Link:') }}</label>
         <crm-search-admin-link-select column="col" links="$ctrl.links"></crm-search-admin-link-select>
       </div>
       <div class="form-inline">
-        <label>{{ ts('Tooltip:') }}</label>
+        <label>{{:: ts('Tooltip:') }}</label>
         <input class="form-control" type="text" ng-model="col.title" />
       </div>
     </fieldset>
@@ -32,7 +40,7 @@
     <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" />
+        <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>
index 84d0b762b0bd030d77037baa14349e727b2618df..a70d2f1ec263be994446eab027cac2c0d48e3844 100644 (file)
@@ -1,5 +1,5 @@
 <?php
-// Search Display module - for rendering search displays.
+// Search Display base module - provides services used commonly by search display implementations.
 return [
   'js' => [
     'ang/crmSearchDisplay.module.js',
@@ -10,7 +10,7 @@ return [
     'ang/crmSearchDisplay',
   ],
   'basePages' => [],
-  'requires' => ['ngSanitize', 'crmUi', 'api4', 'crmSearchActions', 'ui.bootstrap'],
+  'requires' => ['api4', 'ngSanitize'],
   'exports' => [
     'crm-search-display-table' => 'E',
   ],
index beae3e52e1259333bc0e8c84091bce7a1b6079d3..9e2134e71341a0d06e6295990066bfa40f462734 100644 (file)
@@ -2,6 +2,62 @@
   "use strict";
 
   // Declare module
-  angular.module('crmSearchDisplay', CRM.angRequires('crmSearchDisplay'));
+  angular.module('crmSearchDisplay', CRM.angRequires('crmSearchDisplay'))
+
+    .factory('formatSearchValue', function() {
+      function getUrl(link, row) {
+        var url = replaceTokens(link, row);
+        if (url.slice(0, 1) !== '/' && url.slice(0, 4) !== 'http') {
+          url = CRM.url(url);
+        }
+        return _.escape(url);
+      }
+
+      function replaceTokens(str, data) {
+        _.each(data, function(value, key) {
+          str = str.replace('[' + key + ']', value);
+        });
+        return str;
+      }
+
+      return function formatSearchValue(row, col, value) {
+        var type = col.dataType,
+          result = value;
+        if (_.isArray(value)) {
+          return _.map(value, function(val) {
+            return formatSearchValue(col, val);
+          }).join(', ');
+        }
+        if (value && (type === 'Date' || type === 'Timestamp') && /^\d{4}-\d{2}-\d{2}/.test(value)) {
+          result = CRM.utils.formatDate(value, null, type === 'Timestamp');
+        }
+        else if (type === 'Boolean' && typeof value === 'boolean') {
+          result = value ? ts('Yes') : ts('No');
+        }
+        else if (type === 'Money' && typeof value === 'number') {
+          result = CRM.formatMoney(value);
+        }
+        result = _.escape(result);
+        if (col.link) {
+          result = '<a href="' + getUrl(col.link, row) + '">' + result + '</a>';
+        }
+        return result;
+      };
+    })
+
+    .factory('searchDisplayFieldCanAggregate', function() {
+      return function searchDisplayFieldCanAggregate(fieldName, prefix, apiParams) {
+        // If the query does not use grouping, never
+        if (!apiParams.groupBy.length) {
+          return false;
+        }
+        // If the column is used for a groupBy, no
+        if (apiParams.groupBy.indexOf(prefix + fieldName) > -1) {
+          return false;
+        }
+        // If the entity this column belongs to is being grouped by id, then also no
+        return apiParams.groupBy.indexOf(prefix + 'id') < 0;
+      };
+    });
 
 })(angular, CRM.$, CRM._);
diff --git a/ext/search/ang/crmSearchDisplay/Pager.html b/ext/search/ang/crmSearchDisplay/Pager.html
new file mode 100644 (file)
index 0000000..84c2f86
--- /dev/null
@@ -0,0 +1,16 @@
+<div class="text-center" ng-if="$ctrl.rowCount && $ctrl.settings.pager">
+  <ul uib-pagination
+      class="pagination"
+      boundary-links="true"
+      total-items="$ctrl.rowCount"
+      ng-model="$ctrl.page"
+      ng-change="$ctrl.getResults()"
+      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/crmSearchDisplayList.ang.php b/ext/search/ang/crmSearchDisplayList.ang.php
new file mode 100644 (file)
index 0000000..0538ee8
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+// Module for rendering List Search Displays.
+return [
+  'js' => [
+    'ang/crmSearchDisplayList.module.js',
+    'ang/crmSearchDisplayList/*.js',
+  ],
+  'partials' => [
+    'ang/crmSearchDisplayList',
+  ],
+  'basePages' => ['civicrm/search', 'civicrm/admin/search'],
+  'requires' => ['crmSearchDisplay', 'crmUi', 'ui.bootstrap'],
+  'exports' => [
+    'crm-search-display-list' => 'E',
+  ],
+];
diff --git a/ext/search/ang/crmSearchDisplayList.module.js b/ext/search/ang/crmSearchDisplayList.module.js
new file mode 100644 (file)
index 0000000..ccc0274
--- /dev/null
@@ -0,0 +1,7 @@
+(function(angular, $, _) {
+  "use strict";
+
+  // Declare module
+  angular.module('crmSearchDisplayList', CRM.angRequires('crmSearchDisplayList'));
+
+})(angular, CRM.$, CRM._);
diff --git a/ext/search/ang/crmSearchDisplayList/crmSearchDisplayList.component.js b/ext/search/ang/crmSearchDisplayList/crmSearchDisplayList.component.js
new file mode 100644 (file)
index 0000000..d5e9fa8
--- /dev/null
@@ -0,0 +1,77 @@
+(function(angular, $, _) {
+  "use strict";
+
+  angular.module('crmSearchDisplayList').component('crmSearchDisplayList', {
+    bindings: {
+      apiEntity: '<',
+      apiParams: '<',
+      settings: '<',
+      filters: '<'
+    },
+    templateUrl: '~/crmSearchDisplayList/crmSearchDisplayList.html',
+    controller: function($scope, crmApi4, formatSearchValue, searchDisplayFieldCanAggregate) {
+      var ts = $scope.ts = CRM.ts(),
+        ctrl = this;
+      this.page = 1;
+
+      this.$onInit = function() {
+        this.limit = parseInt(ctrl.settings.limit || 0, 10);
+        ctrl.columns = _.cloneDeep(ctrl.settings.columns);
+        _.each(ctrl.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 '));
+        });
+      };
+
+      this.getResults = function() {
+        var params = _.merge(_.cloneDeep(ctrl.apiParams), {limit: ctrl.limit, offset: (ctrl.page - 1) * ctrl.limit});
+        if (_.isEmpty(params.where)) {
+          params.where = [];
+        }
+        // Select the ids of joined entities (helps with displaying links)
+        _.each(params.join, function(join) {
+          var joinEntity = join[0].split(' AS ')[1],
+            idField = joinEntity + '.id';
+          if (!_.includes(params.select, idField) && !searchDisplayFieldCanAggregate('id', joinEntity + '.', params)) {
+            params.select.push(idField);
+          }
+        });
+        _.each(ctrl.filters, function(value, key) {
+          if (value) {
+            params.where.push([key, 'CONTAINS', value]);
+          }
+        });
+        if (ctrl.settings.pager) {
+          params.select.push('row_count');
+        }
+        crmApi4(ctrl.apiEntity, 'get', params).then(function(results) {
+          ctrl.results = results;
+          ctrl.rowCount = results.count;
+        });
+      };
+
+      $scope.$watch('$ctrl.filters', ctrl.getResults, true);
+
+      $scope.formatResult = function(row, col) {
+        var value = row[col.key],
+          formatted = formatSearchValue(row, col, value),
+          output = '';
+        if (formatted.length || (col.label && col.forceLabel)) {
+          if (col.label && (formatted.length || col.forceLabel)) {
+            output += '<label>' + _.escape(col.label) + '</label> ';
+          }
+          if (formatted.length) {
+            output += (col.prefix || '') + formatted + (col.suffix || '');
+          }
+        }
+        return output;
+      };
+
+    }
+  });
+
+})(angular, CRM.$, CRM._);
diff --git a/ext/search/ang/crmSearchDisplayList/crmSearchDisplayList.html b/ext/search/ang/crmSearchDisplayList/crmSearchDisplayList.html
new file mode 100644 (file)
index 0000000..00c0f55
--- /dev/null
@@ -0,0 +1,3 @@
+<ol ng-if=":: $ctrl.settings.style === 'ol'" ng-include="'~/crmSearchDisplayList/crmSearchDisplayListItems.html'" ng-style="{'list-style': $ctrl.settings.symbol}"></ol>
+<ul ng-if=":: $ctrl.settings.style !== 'ol'" ng-include="'~/crmSearchDisplayList/crmSearchDisplayListItems.html'" ng-style="{'list-style': $ctrl.settings.symbol}"></ul>
+<div ng-include="'~/crmSearchDisplay/Pager.html'"></div>
diff --git a/ext/search/ang/crmSearchDisplayList/crmSearchDisplayListItems.html b/ext/search/ang/crmSearchDisplayList/crmSearchDisplayListItems.html
new file mode 100644 (file)
index 0000000..15b8b16
--- /dev/null
@@ -0,0 +1,4 @@
+<li ng-repeat="row in $ctrl.results">
+  <div ng-repeat="col in $ctrl.columns" ng-bind-html="formatResult(row, col)" title="{{:: col.title }}" class="{{:: col.break ? '' : 'crm-inline-block' }}">
+  </div>
+</li>
diff --git a/ext/search/ang/crmSearchDisplayTable.ang.php b/ext/search/ang/crmSearchDisplayTable.ang.php
new file mode 100644 (file)
index 0000000..ea24b14
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+// Module for rendering Table Search Displays.
+return [
+  'js' => [
+    'ang/crmSearchDisplayTable.module.js',
+    'ang/crmSearchDisplayTable/*.js',
+  ],
+  'partials' => [
+    'ang/crmSearchDisplayTable',
+  ],
+  'basePages' => ['civicrm/search', 'civicrm/admin/search'],
+  'requires' => ['crmSearchDisplay', 'crmUi', 'crmSearchActions', 'ui.bootstrap'],
+  'exports' => [
+    'crm-search-display-table' => 'E',
+  ],
+];
diff --git a/ext/search/ang/crmSearchDisplayTable.module.js b/ext/search/ang/crmSearchDisplayTable.module.js
new file mode 100644 (file)
index 0000000..dab6001
--- /dev/null
@@ -0,0 +1,7 @@
+(function(angular, $, _) {
+  "use strict";
+
+  // Declare module
+  angular.module('crmSearchDisplayTable', CRM.angRequires('crmSearchDisplayTable'));
+
+})(angular, CRM.$, CRM._);
similarity index 65%
rename from ext/search/ang/crmSearchDisplay/crmSearchDisplayTable.component.js
rename to ext/search/ang/crmSearchDisplayTable/crmSearchDisplayTable.component.js
index c3d4e5e884bfc2c0ab2c106a274dd83ae1b261bb..c9ebaeeda9e3cb0c2faccf02cd47cd7cd15a8aef 100644 (file)
@@ -1,15 +1,15 @@
 (function(angular, $, _) {
   "use strict";
 
-  angular.module('crmSearchDisplay').component('crmSearchDisplayTable', {
+  angular.module('crmSearchDisplayTable').component('crmSearchDisplayTable', {
     bindings: {
       apiEntity: '<',
       apiParams: '<',
       settings: '<',
       filters: '<'
     },
-    templateUrl: '~/crmSearchDisplay/crmSearchDisplayTable.html',
-    controller: function($scope, crmApi4) {
+    templateUrl: '~/crmSearchDisplayTable/crmSearchDisplayTable.html',
+    controller: function($scope, crmApi4, formatSearchValue, searchDisplayFieldCanAggregate) {
       var ts = $scope.ts = CRM.ts(),
         ctrl = this;
 
@@ -40,7 +40,7 @@
         _.each(params.join, function(join) {
           var joinEntity = join[0].split(' AS ')[1],
             idField = joinEntity + '.id';
-          if (!_.includes(params.select, idField) && !canAggregate('id', joinEntity + '.')) {
+          if (!_.includes(params.select, idField) && !searchDisplayFieldCanAggregate('id', joinEntity + '.', params)) {
             params.select.push(idField);
           }
         });
 
       $scope.formatResult = function(row, col) {
         var value = row[col.key];
-        return formatFieldValue(row, col, value);
+        return formatSearchValue(row, col, value);
       };
 
-      function formatFieldValue(row, col, value) {
-        var type = col.dataType,
-          result = value;
-        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)) {
-          result = CRM.utils.formatDate(value, null, type === 'Timestamp');
-        }
-        else if (type === 'Boolean' && typeof value === 'boolean') {
-          result = value ? ts('Yes') : ts('No');
-        }
-        else if (type === 'Money' && typeof value === 'number') {
-          result = CRM.formatMoney(value);
-        }
-        result = _.escape(result);
-        if (col.link) {
-          result = '<a href="' + getUrl(col.link, row) + '">' + result + '</a>';
-        }
-        return result;
-      }
-
-      function getUrl(link, row) {
-        var url = replaceTokens(link, row);
-        if (url.slice(0, 1) !== '/' && url.slice(0, 4) !== 'http') {
-          url = CRM.url(url);
-        }
-        return _.escape(url);
-      }
-
-      function replaceTokens(str, data) {
-        _.each(data, function(value, key) {
-          str = str.replace('[' + key + ']', value);
-        });
-        return str;
-      }
-
-      function canAggregate(fieldName, prefix) {
-        // If the query does not use grouping, never
-        if (!ctrl.apiParams.groupBy.length) {
-          return false;
-        }
-        // If the column is used for a groupBy, no
-        if (ctrl.apiParams.groupBy.indexOf(prefix + fieldName) > -1) {
-          return false;
-        }
-        // If the entity this column belongs to is being grouped by id, then also no
-        return ctrl.apiParams.groupBy.indexOf(prefix + 'id') < 0;
-      }
-
       $scope.selectAllRows = function() {
         // Deselect all
         if (ctrl.allRowsSelected) {
similarity index 67%
rename from ext/search/ang/crmSearchDisplay/crmSearchDisplayTable.html
rename to ext/search/ang/crmSearchDisplayTable/crmSearchDisplayTable.html
index 6b086334fe9a37d5bf2b826bf7a7a77c9a22e6a0..59d55f2d5ff9dae9998c7d57510f720d7b10d41a 100644 (file)
       <td ng-if="$ctrl.settings.actions">
         <input type="checkbox" ng-checked="isRowSelected(row)" ng-click="selectRow(row)" ng-disabled="!(!loadingAllRows && row.id)">
       </td>
-      <td ng-repeat="col in $ctrl.columns" ng-bind-html="formatResult(row, col)" title="{{:: col.title }}">
+      <td ng-repeat="col in $ctrl.columns" ng-bind-html="formatResult(row, col)" title="{{:: col.title }}" class="{{:: col.alignment }}">
       </td>
       <td></td>
     </tr>
   </tbody>
 </table>
-<div class="text-center" ng-if="$ctrl.rowCount && $ctrl.settings.pager">
-  <ul uib-pagination
-      class="pagination"
-      boundary-links="true"
-      total-items="$ctrl.rowCount"
-      ng-model="$ctrl.page"
-      ng-change="$ctrl.getResults()"
-      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>
+<div ng-include="'~/crmSearchDisplay/Pager.html'"></div>
index e48bd14ce09c9d5c562e685bac54df139214be35..1827d07ce898a6a1a4c7d825eff2689b5adaf53d 100644 (file)
@@ -10,5 +10,5 @@ return [
     'ang/crmSearchKit',
   ],
   'basePages' => [],
-  'requires' => ['api4'],
+  'requires' => [],
 ];
index b806777464eb52a3385cd381cda82cb105442743..12508ec61b9536959414ba1f45ac82daf1076fe0 100644 (file)
@@ -10,6 +10,6 @@ return [
     'ang/crmSearchPage',
   ],
   'basePages' => ['civicrm/search'],
-  'requires' => ['ngRoute', 'api4', 'crmUi', 'crmSearchDisplay'],
+  'requires' => ['ngRoute', 'api4', 'crmUi'],
   'settingsFactory' => ['\Civi\Search\Display', 'getPageSettings'],
 ];
index b8e3fa99b89448975796492065532fabc2430dce..0e7930adf5c978d451b82a4aec21bd84f7a2d01b 100644 (file)
@@ -21,4 +21,15 @@ return [
       'icon' => 'fa-table',
     ],
   ],
+  [
+    'name' => 'SearchDisplayType:list',
+    'entity' => 'OptionValue',
+    'params' => [
+      'option_group_id' => 'search_display_type',
+      'name' => 'list',
+      'value' => 'list',
+      'label' => 'List',
+      'icon' => 'fa-list',
+    ],
+  ],
 ];