Search ext: group tab
authorColeman Watts <coleman@civicrm.org>
Sun, 18 Oct 2020 23:52:28 +0000 (19:52 -0400)
committerColeman Watts <coleman@civicrm.org>
Thu, 29 Oct 2020 23:48:28 +0000 (19:48 -0400)
ext/search/ang/searchActions/SaveSmartGroup.ctrl.js
ext/search/ang/searchAdmin.ang.php
ext/search/ang/searchAdmin.module.js
ext/search/ang/searchAdmin/compose/results.html
ext/search/ang/searchAdmin/crmSearchAdmin.component.js
ext/search/ang/searchAdmin/crmSearchAdmin.html
ext/search/ang/searchAdmin/group.html [new file with mode: 0644]
ext/search/ang/searchAdmin/tabs.html
ext/search/css/search.css

index 180099cd863f7add49bbc81649684d79391e5883..b7d8c1f1d09ac2e7b36bb40acc7107fc72bbe5d7 100644 (file)
@@ -3,9 +3,7 @@
 
   angular.module('searchActions').controller('SaveSmartGroup', function ($scope, $element, $timeout, crmApi4, dialogService, searchMeta) {
     var ts = $scope.ts = CRM.ts(),
-      model = $scope.model,
-      joins = _.pluck((model.api_params.join || []), 0),
-      entityCount = {};
+      model = $scope.model;
     $scope.groupEntityRefParams = {
       entity: 'Group',
       api: {
         placeholder: ts('Select existing group')
       }
     };
-    // Find all possible search columns that could serve as contact_id for the smart group
-    $scope.columns = _.transform([model.api_entity].concat(joins), function(columns, joinExpr) {
-      var joinName = joinExpr.split(' AS '),
-        entityName = joinName[0],
-        entity = searchMeta.getEntity(entityName),
-        prefix = joinName[1] ? joinName[1] + '.' : '';
-      _.each(entity.fields, function(field) {
-        if ((entityName === 'Contact' && field.name === 'id') || field.fk_entity === 'Contact') {
-          columns.push({
-            id: prefix + field.name,
-            text: entity.title_plural + (entityCount[entityName] ? ' ' + entityCount[entityName] : '') + ': ' + field.label,
-            icon: entity.icon
-          });
-        }
-      });
-      entityCount[entityName] = 1 + (entityCount[entityName] || 1);
-    });
+    $scope.columns = searchMeta.getSmartGroupColumns(model.api_entity, model.api_params);
 
     if (!$scope.columns.length) {
       CRM.alert(ts('Cannot create smart group; search does not include any contacts.'), ts('Error'));
index 6c72a9ebe69e87950fc041fbe407aad5e93539f1..31a85141bff549c879bc86848bd0605dcb47459a 100644 (file)
@@ -13,6 +13,6 @@ return [
     'ang/searchAdmin',
   ],
   'basePages' => [],
-  'requires' => ['crmUi', 'crmUtil', 'ngRoute', 'ui.sortable', 'ui.bootstrap', 'dialogService', 'api4', 'searchActions'],
+  'requires' => ['crmUi', 'crmUtil', 'ngRoute', 'ui.sortable', 'ui.bootstrap', 'api4', 'searchActions'],
   'settingsFactory' => ['\Civi\Search\Admin', 'getAdminSettings'],
 ];
index 913f9ac5e3e2c4f833fafef48774116aeb0eb27c..486298bca61cbd39157e9ca9dd22f3b7a07cc788 100644 (file)
             result.suffix = !split[1] ? '' : ':' + split[1];
           }
           return result;
+        },
+        // Find all possible search columns that could serve as contact_id for a smart group
+        getSmartGroupColumns: function(api_entity, api_params) {
+          var joins = _.pluck((api_params.join || []), 0),
+            entityCount = {};
+          return _.transform([api_entity].concat(joins), function(columns, joinExpr) {
+            var joinName = joinExpr.split(' AS '),
+              entityName = joinName[0],
+              entity = getEntity(entityName),
+              prefix = joinName[1] ? joinName[1] + '.' : '';
+            _.each(entity.fields, function(field) {
+              if ((entityName === 'Contact' && field.name === 'id') || field.fk_entity === 'Contact') {
+                columns.push({
+                  id: prefix + field.name,
+                  text: entity.titlePlural + (entityCount[entityName] ? ' ' + entityCount[entityName] : '') + ': ' + field.label,
+                  icon: entity.icon
+                });
+              }
+            });
+            entityCount[entityName] = 1 + (entityCount[entityName] || 1);
+          });
         }
       };
     })
index cb0fe7566ffa7d8ecaab49bb0be2bc81ef31f13b..9bd8f25686de376a66e21a8782decdc82ac3140d 100644 (file)
@@ -1,13 +1,16 @@
 <table>
   <thead>
-    <tr ng-model="$ctrl.savedSearch.api_params.select" ui-sortable="{axis: 'x'}">
+    <tr ng-model="$ctrl.savedSearch.api_params.select" ui-sortable="sortableColumnOptions">
       <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).')}}">
         <i class="crm-i {{ getOrderBy(col) }}"></i>
-        <span>{{ $ctrl.getFieldLabel(col) }}</span>
-        <a href class="crm-hover-button" title="{{:: ts('Clear') }}" ng-click="$ctrl.clearParam('select', $index)"><i class="crm-i fa-times" aria-hidden="true"></i></a>
+        <span ng-class="{'crm-sortable': $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>
+        </span>
       </th>
       <th class="form-inline">
         <input class="form-control crm-action-menu fa-plus" ng-model="controls.select" crm-ui-select="::{data: fieldsForSelect, placeholder: ts('Add')}" ng-change="addParam('select')">
index 9afbd05962ec7f02cc610677932a71031a62f718..57867876d3c7488ad96dd7646f9ad64dec834c71 100644 (file)
@@ -26,6 +26,7 @@
 
       $scope.controls = {tab: 'compose'};
       $scope.joinTypes = [{k: false, v: ts('Optional')}, {k: true, v: ts('Required')}];
+      $scope.groupOptions = CRM.searchActions.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);
+        }
 
         if (!this.savedSearch.api_params) {
           this.savedSearch.api_params = {
       };
 
       this.addDisplay = function(type) {
-        $scope.controls.tab = 'display_' + ctrl.savedSearch.displays.length;
         ctrl.savedSearch.displays.push({
           type: type
         });
+        $scope.selectTab('display_' + (ctrl.savedSearch.displays.length - 1));
+      };
+
+      this.removeDisplay = function(index) {
+        var display = ctrl.savedSearch.displays[index];
+        if (display.id) {
+          display.trashed = !display.trashed;
+        } else {
+          $scope.selectTab('compose');
+          ctrl.savedSearch.displays.splice(index, 1);
+        }
+      };
+
+      this.addGroup = function() {
+        ctrl.savedSearch.group = {
+          title: '',
+          description: '',
+          visibility: 'User and User Admin Only',
+          group_type: []
+        };
+        ctrl.groupExists = true;
+        $scope.selectTab('group');
+      };
+
+      $scope.selectTab = function(tab) {
+        if (tab === 'group') {
+          $scope.smartGroupColumns = searchMeta.getSmartGroupColumns(ctrl.savedSearch.api_entity, ctrl.savedSearch.api_params);
+          var smartGroupColumns = _.map($scope.smartGroupColumns, 'id');
+          if (smartGroupColumns.length && !_.includes(smartGroupColumns, ctrl.savedSearch.api_params.select[0])) {
+            ctrl.savedSearch.api_params.select.unshift(smartGroupColumns[0]);
+          }
+        }
+        ctrl.savedSearch.api_params.select = _.uniq(ctrl.savedSearch.api_params.select);
+        $scope.controls.tab = tab;
+      };
+
+      this.removeGroup = function() {
+        ctrl.groupExists = !ctrl.groupExists;
+        if (!ctrl.groupExists && (!ctrl.savedSearch.group || !ctrl.savedSearch.group.id)) {
+          ctrl.savedSearch.group = null;
+        }
+        $scope.selectTab('compose');
       };
 
       $scope.getJoinEntities = function() {
         })};
       };
 
+      $scope.sortableColumnOptions = {
+        axis: 'x',
+        handle: '.crm-sortable',
+        update: function(e, ui) {
+          // Don't allow items to be moved to position 0 if locked
+          if (!ui.item.sortable.dropindex && ctrl.groupExists) {
+            ui.item.sortable.cancel();
+          }
+        }
+      };
+
+      // Sets the default select clause based on commonly-named fields
       function getDefaultSelect() {
-        return _.filter(['id', 'display_name', 'label', 'title', 'location_type_id:label'], function(field) {
-          return !!searchMeta.getField(field, ctrl.savedSearch.api_entity);
+        var whitelist = ['id', 'name', 'subject', 'display_name', 'label', 'title'];
+        return _.transform(searchMeta.getEntity(ctrl.savedSearch.api_entity).fields, function(select, field) {
+          if (_.includes(whitelist, field.name) || _.includes(field.name, '_type_id')) {
+            select.push(field.name + (field.options ? ':label' : ''));
+          }
         });
       }
 
index a6b8dff76d4940e4a8e0a415d686bc51a4c3d700..b8fcb1820b6aa970c8762a2f8e4cccadcc6257ad 100644 (file)
@@ -1,5 +1,6 @@
 <div id="bootstrap-theme" class="crm-search">
   <h1 crm-page-title>{{:: ts('Search for %1', {1: $ctrl.entityTitle}) }}</h1>
+  <div crm-ui-debug="$ctrl.savedSearch"></div>
 
   <!--This warning will show if bootstrap is unavailable. Normally it will be hidden by the bootstrap .collapse class.-->
   <div class="messages warning no-popup collapse">
@@ -29,6 +30,7 @@
           <div ng-include="'~/searchAdmin/compose/pager.html'"></div>
         </div>
         <div ng-switch-when="group">
+          <fieldset ng-include="'~/searchAdmin/group.html'"></fieldset>
         </div>
         <div ng-switch-default>
         </div>
diff --git a/ext/search/ang/searchAdmin/group.html b/ext/search/ang/searchAdmin/group.html
new file mode 100644 (file)
index 0000000..6a4674f
--- /dev/null
@@ -0,0 +1,33 @@
+<div ng-if="!$ctrl.groupExists">
+  <div class="alert alert-warning">
+    {{:: ts('Smart group "%1" will be deleted.', {1: $ctrl.original.group.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">
+  <hr />
+  <div class="form-inline">
+   <label for="api-save-search-select-column">{{:: ts('Contact Column:') }} <span class="crm-marker">*</span></label>
+    <input id="api-save-search-select-column" ng-model="$ctrl.savedSearch.api_params.select[0]" class="form-control" crm-ui-select="{data: smartGroupColumns}"/>
+  </div>
+  <fieldset ng-show="smartGroupColumns.length">
+    <label>{{:: ts('Description:') }}</label>
+    <textarea class="form-control" ng-model="$ctrl.savedSearch.group.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">
+          {{ 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>
+    </div>
+  </fieldset>
+</div>
index e70810ec6cdc2845a04484879af44ee27bb4c1e9..a9d6591c017680eb2882c9f471287eb492fd7af9 100644 (file)
@@ -1,28 +1,43 @@
 <li role="presentation" ng-class="{active: controls.tab === 'compose'}">
-  <a href ng-click="controls.tab = 'compose'">
+  <a href ng-click="selectTab('compose')">
     <i class="crm-i fa-gears"></i>
     {{ ts('Compose Search') }}
   </a>
 </li>
-<li role="presentation" ng-class="{active: controls.tab === 'group'}">
-  <a href ng-click="controls.tab = 'group'">
+<li role="presentation" ng-class="{active: controls.tab === 'group'}" ng-if="$ctrl.savedSearch.group">
+  <a href ng-click="selectTab('group')" ng-class="{strikethrough: !$ctrl.groupExists}">
     <i class="crm-i fa-users"></i>
-    {{ ts('Smart Group: %1') }}
+    {{:: ts('Smart Group:') }} {{ $ctrl.savedSearch.group.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="controls.tab = ('display_' + $index)">
+  <a href ng-click="selectTab('display_' + $index)">
     <i class="crm-i {{ $ctrl.displayTypes[display.type].icon }}"></i>
     {{ $ctrl.displayTypes[display.type].label }}
   </a>
+  <button class="btn-xs crm-search-delete-display" ng-click="$ctrl.removeDisplay($index)">
+    <i class="crm-i fa-trash"></i>
+  </button>
 </li>
 <li role="presentation">
   <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-    <i class="crm-i fa-plus"></i> {{ ts('Add Display') }} <span class="caret"></span>
+    <i class="crm-i fa-plus"></i> {{:: ts('Add...') }} <span class="caret"></span>
   </button>
   <ul class="dropdown-menu">
-    <li ng-repeat="type in $ctrl.displayTypes" >
-      <a href ng-click="$ctrl.addDisplay(type.name)">{{ type.label }}</a>
+    <li ng-if="!$ctrl.savedSearch.group">
+      <a href ng-click="$ctrl.addGroup()">
+        <i class="crm-i fa-users"></i>
+        {{:: ts('Smart Group') }}
+      </a>
+    </li>
+    <li ng-repeat="type in ::$ctrl.displayTypes">
+      <a href ng-click="$ctrl.addDisplay(type.name)">
+        <i class="crm-i {{:: type.icon }}"></i>
+        {{:: type.label }}
+      </a>
     </li>
   </ul>
 </li>
index 276755632f688215c021a57bd6e4dc3d60550129..e792d5ceb95b9c278781a7e48453fe95f630ed74 100644 (file)
 .crm-flex-box > .crm-flex-4 {
   flex: 4;
 }
+.crm-sortable {
+  cursor: move;
+}
+
 #bootstrap-theme #crm-search-results-page-size {
   width: 5em;
 }
 #bootstrap-theme .crm-search-results {
   min-height: 200px;
 }
-.crm-search-results thead th[ng-repeat] {
-  cursor: pointer;
-}
-.crm-search-results thead th[ng-repeat] > span {
-  cursor: move;
+
+#bootstrap-theme.crm-search ul.nav.nav-stacked {
+  margin-left: 0;
+  margin-right: 20px;
 }
 
 #bootstrap-theme.crm-search fieldset {
 #bootstrap-theme.crm-search th.crm-search-result-select {
   padding-right: 10px;
 }
+
+#bootstrap-theme .crm-search-delete-display {
+  position: absolute;
+  right: 0;
+  top: 0;
+}