SearchKit - add option to rewrite field output in displays
authorColeman Watts <coleman@civicrm.org>
Wed, 24 Feb 2021 01:38:39 +0000 (20:38 -0500)
committerColeman Watts <coleman@civicrm.org>
Wed, 24 Feb 2021 01:49:06 +0000 (20:49 -0500)
13 files changed:
ext/search/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js
ext/search/ang/crmSearchAdmin/displays/common/fieldOptions.html
ext/search/ang/crmSearchAdmin/displays/common/unusedColumns.html
ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayList.component.js
ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayList.html
ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayTable.component.js
ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayTable.html
ext/search/ang/crmSearchDisplay.module.js
ext/search/ang/crmSearchDisplayList/crmSearchDisplayList.component.js
ext/search/ang/crmSearchDisplayList/crmSearchDisplayListItems.html
ext/search/ang/crmSearchDisplayTable/crmSearchDisplayTable.component.js
ext/search/ang/crmSearchDisplayTable/crmSearchDisplayTable.html
ext/search/css/crmSearchAdmin.css

index 3627c41a51d6154510816dac08baa39f0d8fc14e..afea8e8b60a901f29ed94e702f3719fdf5f4f039 100644 (file)
         return searchMeta.getDefaultLabel(expr);
       };
 
-      function fieldToColumn(fieldExpr) {
-        var info = searchMeta.parseExpr(fieldExpr);
-        return {
-          key: info.alias,
-          label: searchMeta.getDefaultLabel(fieldExpr),
-          dataType: (info.fn && info.fn.name === 'COUNT') ? 'Integer' : info.field.data_type
-        };
+      function fieldToColumn(fieldExpr, defaults) {
+        var info = searchMeta.parseExpr(fieldExpr),
+          values = _.cloneDeep(defaults);
+        if (defaults.key) {
+          values.key = info.alias;
+        }
+        if (defaults.label) {
+          values.label = searchMeta.getDefaultLabel(fieldExpr);
+        }
+        if (defaults.dataType) {
+          values.dataType = (info.fn && info.fn.name === 'COUNT') ? 'Integer' : info.field && info.field.data_type;
+        }
+        return values;
       }
 
       // Helper function to sort active from hidden columns and initialize each column with defaults
-      this.initColumns = function() {
+      this.initColumns = function(defaults) {
         if (!ctrl.display.settings.columns) {
           ctrl.display.settings.columns = _.transform(ctrl.savedSearch.api_params.select, function(columns, fieldExpr) {
-            columns.push(fieldToColumn(fieldExpr));
+            columns.push(fieldToColumn(fieldExpr, defaults));
           });
           ctrl.hiddenColumns = [];
         } else {
           ctrl.hiddenColumns = _.transform(ctrl.savedSearch.api_params.select, function(hiddenColumns, fieldExpr) {
             var key = _.last(fieldExpr.split(' AS '));
             if (!_.includes(activeColumns, key)) {
-              hiddenColumns.push(fieldToColumn(fieldExpr));
+              hiddenColumns.push(fieldToColumn(fieldExpr, defaults));
             }
           });
           _.eachRight(activeColumns, function(key, index) {
index 81f1b302f63f1abf1eefcd4b3b827fe57b2b6a03..d05271bd3641a0a818af8b72b439a8ad5b83195e 100644 (file)
@@ -7,3 +7,11 @@
   <input class="form-control" type="text" ng-model="col.title" ng-if="col.title" ng-model-options="{updateOn: 'blur'}" />
   <crm-search-admin-token-select ng-if="col.title" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" model="col" field="title"></crm-search-admin-token-select>
 </div>
+<div class="form-inline crm-search-admin-flex-row">
+  <label title="{{ ts('Change the contents of this field, or combine multiple field values.') }}">
+    <input type="checkbox" ng-checked="col.rewrite" ng-click="col.rewrite = col.rewrite ? null : '['+col.key+']'" >
+    {{ col.rewrite ? ts('Rewrite:') : ts('Rewrite') }}
+  </label>
+  <input type="text" class="form-control" ng-if="col.rewrite" ng-model="col.rewrite" ng-model-options="{updateOn: 'blur'}">
+  <crm-search-admin-token-select ng-if="col.rewrite" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" model="col" field="rewrite"></crm-search-admin-token-select>
+</div>
index 08132e97c60c6aeab00aecf2d0b4829662a3242d..ed093f54e6a81f19e1c893f4ccc5dd65c25fd4e8 100644 (file)
@@ -1,9 +1,8 @@
-<fieldset class="crm-search-admin-edit-columns" ng-model="$ctrl.parent.hiddenColumns" ui-sortable="$ctrl.parent.sortableOptions">
+<fieldset class="crm-search-admin-edit-columns crm-search-admin-unused-columns" ng-model="$ctrl.parent.hiddenColumns" ui-sortable="$ctrl.parent.sortableOptions">
   <legend>{{:: ts('Unused') }}</legend>
   <fieldset ng-repeat="col in $ctrl.parent.hiddenColumns" class="crm-draggable">
     <legend>{{ $ctrl.parent.getFieldLabel(col.key) }}</legend>
     <div class="form-inline">
-      <label>{{:: ts('Label:') }}</label> <input disabled class="form-control" type="text" ng-model="col.label" />
       <button type="button" class="btn-xs pull-right" ng-click="$ctrl.parent.restoreCol($index)" title="{{:: ts('Show') }}">
         <i class="crm-i fa-undo"></i>
       </button>
index 777191ef1a352a2fa8e44726938da10f4fde2863..54dd7008c75de78636589bbc3e0639024592460e 100644 (file)
@@ -38,7 +38,7 @@
             pager: true
           };
         }
-        ctrl.parent.initColumns();
+        ctrl.parent.initColumns({key: true, dataType: true});
       };
 
     }
index fb02a78279fa1bb54a6cd4da0d2f0b9f01594fad..10b1a60296fc51c89cdb47e21e5369cc1f02ed4d 100644 (file)
     <legend>{{:: ts('Fields') }}</legend>
     <fieldset ng-repeat="col in $ctrl.display.settings.columns" class="crm-draggable">
       <legend>{{ $ctrl.parent.getFieldLabel(col.key) }}</legend>
-      <div class="form-inline">
-        <label>{{:: ts('Label:') }}</label> <input class="form-control" type="text" ng-model="col.label" >
-        <div class="form-control checkbox-inline" ng-show="col.label.length" title="{{:: ts('Show label for every record even when this field is blank') }}">
-          <label><input type="checkbox" ng-model="col.forceLabel"> <span>{{:: ts('Always show') }}</span></label>
-        </div>
+      <div class="form-inline" title="{{ ts('Should this item display on its own line or inline with other items?') }}">
+        <label><input type="checkbox" ng-model="col.break"> {{:: ts('Display on new line') }}</label>
         <button type="button" class="btn-xs pull-right" ng-click="$ctrl.parent.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">
-        <div class="form-control checkbox-inline">
-          <label><input type="checkbox" ng-model="col.break"> <span>{{:: ts('New line') }}</span></label>
+      <div class="form-inline crm-search-admin-flex-row">
+        <label>
+          <input type="checkbox" ng-checked="col.label" ng-click="col.label = col.label ? null : $ctrl.parent.getFieldLabel(col.key)" >
+          {{ col.label ? ts('Label:') : ts('Label') }}
+        </label>
+        <input ng-if="col.label" class="form-control" type="text" ng-model="col.label" ng-model-options="{updateOn: 'blur'}">
+        <crm-search-admin-token-select ng-if="col.label" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" model="col" field="label"></crm-search-admin-token-select>
+      </div>
+      <div class="form-inline" ng-if="col.label">
+        <label style="visibility: hidden"><input type="checkbox" disabled></label>
+        <div class="checkbox">
+          <label><input type="checkbox" ng-model="col.forceLabel"> {{:: ts('Show label even when field is blank') }}</label>
         </div>
       </div>
       <div ng-include="'~/crmSearchAdmin/displays/common/fieldOptions.html'"></div>
index ccb4fd5c8c22a62e7c999975e742c2973e22dac1..bd958c37bf6facaf8c0d8046ab697a714f5a8f70 100644 (file)
@@ -22,7 +22,7 @@
             pager: true
           };
         }
-        ctrl.parent.initColumns();
+        ctrl.parent.initColumns({key: true, label: true, dataType: true});
       };
 
     }
index 118148062584bc73032601ceefe7b5e4e96a1ac4..4836045d9bb43ef3f12431a6b68f5bdbce7920ca 100644 (file)
     <legend>{{:: ts('Columns') }}</legend>
     <fieldset ng-repeat="col in $ctrl.display.settings.columns" class="crm-draggable">
       <legend>{{ $ctrl.parent.getFieldLabel(col.key) }}</legend>
-      <div class="form-inline">
-        <label>{{:: ts('Label:') }}</label> <input class="form-control" type="text" ng-model="col.label" />
-        <button type="button" class="btn-xs pull-right" ng-click="$ctrl.parent.removeCol($index)" title="{{:: ts('Hide') }}">
+      <div class="form-inline crm-search-admin-flex-row">
+        <label for="crm-search-admin-edit-col-{{ $index }}">{{:: ts('Header:') }}</label>
+        <input id="crm-search-admin-edit-col-{{ $index }}" class="form-control" type="text" ng-model="col.label" >
+        &nbsp;&nbsp;&nbsp;&nbsp;
+        <button type="button" class="btn-xs" ng-click="$ctrl.parent.removeCol($index)" title="{{:: ts('Hide') }}">
           <i class="crm-i fa-ban"></i>
         </button>
       </div>
index f77de83380b081b5a2d2981505b086335b3137a2..55c18c7a55c36d7a331ffd609af8408b6e152d69 100644 (file)
@@ -6,30 +6,48 @@
 
     .factory('searchDisplayUtils', function(crmApi4) {
 
-      function replaceTokens(str, data) {
+      // Replace tokens keyed to rowData.
+      // If rowMeta is provided, values will be formatted; if omiited, raw values will be provided.
+      function replaceTokens(str, rowData, rowMeta) {
         if (!str) {
           return '';
         }
-        _.each(data, function(value, key) {
-          str = str.replace('[' + key + ']', value);
+        _.each(rowData, function(value, key) {
+          if (str.indexOf('[' + key + ']') >= 0) {
+            var column = rowMeta && _.findWhere(rowMeta, {key: key}),
+              replacement = column ? formatRawValue(column, value) : value;
+            str = str.replace(new RegExp(_.escapeRegExp('[' + key + ']', 'g')), replacement);
+          }
         });
         return str;
       }
 
-      function getUrl(link, row) {
-        var url = replaceTokens(link, row);
+      function getUrl(link, rowData) {
+        var url = replaceTokens(link, rowData);
         if (url.slice(0, 1) !== '/' && url.slice(0, 4) !== 'http') {
           url = CRM.url(url);
         }
         return _.escape(url);
       }
 
-      function formatSearchValue(row, col, value) {
-        var type = col.dataType,
+      // Returns html-escaped display value for a single column in a row
+      function formatDisplayValue(rowData, key, rowMeta) {
+        var column = _.findWhere(rowMeta, {key: key}),
+          displayValue = column.rewrite ? replaceTokens(column.rewrite, rowData, rowMeta) : formatRawValue(column, rowData[key]),
+          result = _.escape(displayValue);
+        if (column.link) {
+          result = '<a href="' + getUrl(column.link, rowData) + '">' + result + '</a>';
+        }
+        return result;
+      }
+
+      // Formats raw field value according to data type
+      function formatRawValue(column, value) {
+        var type = column && column.dataType,
           result = value;
         if (_.isArray(value)) {
           return _.map(value, function(val) {
-            return formatSearchValue(row, col, val);
+            return formatRawValue(column, val);
           }).join(', ');
         }
         if (value && (type === 'Date' || type === 'Timestamp') && /^\d{4}-\d{2}-\d{2}/.test(value)) {
         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;
       }
 
@@ -76,7 +90,7 @@
       }
 
       return {
-        formatSearchValue: formatSearchValue,
+        formatDisplayValue: formatDisplayValue,
         getApiParams: getApiParams,
         getResults: getResults,
         replaceTokens: replaceTokens
index 129d267c7f1a424d89235fbd458c884655c489d0..d439fb6e8cbbc9c1c16224d5aa2f742960745d23 100644 (file)
         ctrl.getResults();
       }
 
-      $scope.formatResult = function(row, col) {
-        var value = row[col.key],
-          formatted = searchDisplayUtils.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 || '');
-          }
+      $scope.formatResult = function(rowData, col) {
+        var formatted = searchDisplayUtils.formatDisplayValue(rowData, col.key, ctrl.settings.columns);
+        if (col.label && (formatted.length || col.forceLabel)) {
+          var label = searchDisplayUtils.replaceTokens(col.label, rowData, ctrl.settings.columns);
+          formatted = '<label>' + _.escape(label) + '</label> ' + formatted;
         }
-        return output;
+        return formatted;
       };
 
     }
index e6a29dd9cbd870770a685473f7739faf609905c0..5d4d0deb4872fa742bd84d5d809320954eacd7cb 100644 (file)
@@ -1,4 +1,4 @@
 <li ng-repeat="row in $ctrl.results">
-  <div ng-repeat="col in $ctrl.settings.columns" ng-bind-html="formatResult(row, col)" title="{{:: displayUtils.replaceTokens(col.title, row) }}" class="{{:: col.break ? '' : 'crm-inline-block' }}">
+  <div ng-repeat="col in $ctrl.settings.columns" ng-bind-html="formatResult(row, col)" title="{{:: displayUtils.replaceTokens(col.title, row, $ctrl.settings.columns) }}" class="{{:: col.break ? '' : 'crm-inline-block' }}">
   </div>
 </li>
index 6a3eca26f835244f3624201ceb0b72bf7c55c123..fb5999f2bdbdbea2ce0dcf4ad45e36017100f7c6 100644 (file)
@@ -76,9 +76,8 @@
         ctrl.getResults();
       };
 
-      $scope.formatResult = function(row, col) {
-        var value = row[col.key];
-        return searchDisplayUtils.formatSearchValue(row, col, value);
+      $scope.formatResult = function(rowData, col) {
+        return searchDisplayUtils.formatDisplayValue(rowData, col.key, ctrl.settings.columns);
       };
 
       $scope.selectAllRows = function() {
index 9ad18f1fc6216abfd5488ce84fabf6710900c186..f1737a82e1d6a1cda091210cbf5616689f9e4c32 100644 (file)
@@ -18,7 +18,7 @@
       <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.settings.columns" ng-bind-html="formatResult(row, col)" title="{{:: displayUtils.replaceTokens(col.title, row) }}" class="{{:: col.alignment }}">
+      <td ng-repeat="col in $ctrl.settings.columns" ng-bind-html="formatResult(row, col)" title="{{:: displayUtils.replaceTokens(col.title, row, $ctrl.settings.columns) }}" class="{{:: col.alignment }}">
       </td>
       <td></td>
     </tr>
index 2121cd507abd341ead339158d7fbf8ed089b5365..29de32e6429a3aa037ecbf5aef7fbe26ad22c8cf 100644 (file)
   flex: 1;
 }
 
+#bootstrap-theme .crm-search-admin-unused-columns fieldset {
+  min-height: 60px;
+}
+
 #bootstrap-theme input[type=search]::placeholder {
   font-family: FontAwesome;
   text-align: right;