SearchKit - Make UI less confusing by not allowing joins to be switched
[civicrm-core.git] / ext / search / ang / crmSearchAdmin / crmSearchAdmin.component.js
index 2e7d617cf08b055fe0ac1e626d2fc30b1328f25c..353851435ff2b3e1a3f255bfa4f87cb6a1dab10f 100644 (file)
@@ -6,7 +6,7 @@
       savedSearch: '<'
     },
     templateUrl: '~/crmSearchAdmin/crmSearchAdmin.html',
-    controller: function($scope, $element, $timeout, crmApi4, dialogService, searchMeta, formatForSelect2) {
+    controller: function($scope, $element, $location, $timeout, crmApi4, dialogService, searchMeta, formatForSelect2) {
       var ts = $scope.ts = CRM.ts(),
         ctrl = this;
 
       $scope.controls = {tab: 'compose'};
       $scope.joinTypes = [{k: false, v: ts('Optional')}, {k: true, v: ts('Required')}];
       $scope.groupOptions = CRM.crmSearchActions.groupOptions;
-      $scope.entities = formatForSelect2(CRM.vars.search.schema, 'name', 'title_plural', ['description', 'icon']);
+      // Try to create a sensible list of entities one might want to search for,
+      // excluding those whos primary purpose is to provide joins or option lists to other entities
+      var primaryEntities = _.filter(CRM.crmSearchAdmin.schema, function(entity) {
+        return !_.includes(entity.type, 'EntityBridge') && !_.includes(entity.type, 'OptionList');
+      });
+      $scope.entities = formatForSelect2(primaryEntities, 'name', 'title_plural', ['description', 'icon']);
       this.perm = {
         editGroups: CRM.checkPerm('edit groups')
       };
         this.savedSearch.groups = this.savedSearch.groups || [];
         this.groupExists = !!this.savedSearch.groups.length;
 
-        if (!this.savedSearch.api_params) {
-          this.savedSearch.api_params = {
-            version: 4,
-            select: getDefaultSelect(),
-            orderBy: {},
-            where: [],
-          };
+        if (!this.savedSearch.id) {
+          $scope.$bindToRoute({
+            param: 'params',
+            expr: '$ctrl.savedSearch.api_params',
+            deep: true,
+            default: {
+              version: 4,
+              select: getDefaultSelect(),
+              orderBy: {},
+              where: [],
+            }
+          });
         }
 
         $scope.$watchCollection('$ctrl.savedSearch.api_params.select', onChangeSelect);
         delete params.displays;
         apiCalls.saved = ['SavedSearch', 'save', {records: [params], chain: chain}, 0];
         crmApi4(apiCalls).then(function(results) {
+          // After saving a new search, redirect to the edit url
+          if (!ctrl.savedSearch.id) {
+            $location.url('edit/' + results.saved.id);
+          }
           // Set new status to saved unless the user changed something in the interim
           var newStatus = $scope.status === 'unsaved' ? 'unsaved' : 'saved';
-          ctrl.savedSearch.id = results.saved.id;
           if (results.saved.groups && results.saved.groups.length) {
             ctrl.savedSearch.groups[0].id = results.saved.groups[0].id;
           }
         }
       };
 
+      function addNum(name, num) {
+        return name + (num < 10 ? '_0' : '_') + num;
+      }
+
+      function getExistingJoins() {
+        return _.transform(ctrl.savedSearch.api_params.join || [], function(joins, join) {
+          joins[join[0].split(' AS ')[1]] = searchMeta.getJoin(join[0]);
+        }, {});
+      }
+
+      $scope.getJoin = searchMeta.getJoin;
+
       $scope.getJoinEntities = function() {
-        var joinEntities = _.transform(CRM.vars.search.links[ctrl.savedSearch.api_entity], function(joinEntities, link) {
-          var entity = searchMeta.getEntity(link.entity);
-          if (entity) {
-            joinEntities.push({
-              id: link.entity + ' AS ' + link.alias,
-              text: entity.title_plural,
-              description: '(' + link.alias + ')',
-              icon: entity.icon
-            });
+        var existingJoins = getExistingJoins();
+
+        function addEntityJoins(entity, stack, baseEntity) {
+          return _.transform(CRM.crmSearchAdmin.joins[entity], function(joinEntities, join) {
+            var num = 0;
+            // Add all joins that don't just point directly back to the original entity
+            if (!(baseEntity === join.entity && !join.multi)) {
+              do {
+                appendJoin(joinEntities, join, ++num, stack, entity);
+              } while (addNum((stack ? stack + '_' : '') + join.alias, num) in existingJoins);
+            }
+          }, []);
+        }
+
+        function appendJoin(collection, join, num, stack, baseEntity) {
+          var alias = addNum((stack ? stack + '_' : '') + join.alias, num),
+            opt = {
+              id: join.entity + ' AS ' + alias,
+              description: join.description,
+              text: join.label + (num > 1 ? ' ' + num : ''),
+              icon: searchMeta.getEntity(join.entity).icon,
+              disabled: alias in existingJoins
+            };
+          if (alias in existingJoins) {
+            opt.children = addEntityJoins(join.entity, (stack ? stack + '_' : '') + alias, baseEntity);
           }
-        }, []);
-        return {results: joinEntities};
+          collection.push(opt);
+        }
+
+        return {results: addEntityJoins(ctrl.savedSearch.api_entity)};
       };
 
       $scope.addJoin = function() {
         $timeout(function() {
           if ($scope.controls.join) {
             ctrl.savedSearch.api_params.join = ctrl.savedSearch.api_params.join || [];
-            ctrl.savedSearch.api_params.join.push([$scope.controls.join, false]);
+            var join = searchMeta.getJoin($scope.controls.join),
+              params = [$scope.controls.join, false];
+            _.each(_.cloneDeep(join.conditions), function(condition) {
+              params.push(condition);
+            });
+            _.each(_.cloneDeep(join.defaults), function(condition) {
+              params.push(condition);
+            });
+            ctrl.savedSearch.api_params.join.push(params);
             loadFieldOptions();
           }
           $scope.controls.join = '';
         });
       };
 
-      $scope.changeJoin = function(idx) {
-        if (ctrl.savedSearch.api_params.join[idx][0]) {
-          ctrl.savedSearch.api_params.join[idx].length = 2;
-          loadFieldOptions();
-        } else {
-          ctrl.clearParam('join', idx);
-        }
-      };
-
       $scope.changeGroupBy = function(idx) {
         if (!ctrl.savedSearch.api_params.groupBy[idx]) {
           ctrl.clearParam('groupBy', idx);
        */
       $scope.setOrderBy = function(col, $event) {
         var dir = $scope.getOrderBy(col) === 'fa-sort-asc' ? 'DESC' : 'ASC';
-        if (!$event.shiftKey) {
+        if (!$event.shiftKey || !ctrl.savedSearch.api_params.orderBy) {
           ctrl.savedSearch.api_params.orderBy = {};
         }
         ctrl.savedSearch.api_params.orderBy[col] = dir;
         if (ctrl.savedSearch.api_params.groupBy.length) {
           _.each(ctrl.savedSearch.api_params.select, function(col, pos) {
             if (!_.contains(col, '(') && ctrl.canAggregate(col)) {
-              ctrl.savedSearch.api_params.select[pos] = ctrl.DEFAULT_AGGREGATE_FN + '(' + col + ')';
+              ctrl.savedSearch.api_params.select[pos] = ctrl.DEFAULT_AGGREGATE_FN + '(DISTINCT ' + col + ')';
             }
           });
         }
         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) {
 
       $scope.formatResult = function(row, col) {
         var info = searchMeta.parseExpr(col),
-          key = info.fn ? (info.fn.name + ':' + info.path + info.suffix) : col,
-          value = row[key];
+          value = row[info.alias];
         if (info.fn && info.fn.name === 'COUNT') {
           return value;
         }
       }
 
       function getAllFields(suffix, disabledIf) {
-        function formatFields(entityName, prefix) {
-          return _.transform(searchMeta.getEntity(entityName).fields, function(result, field) {
-            var item = {
-              id: prefix + field.name + (field.options ? suffix : ''),
-              text: field.label,
-              description: field.description
-            };
-            if (disabledIf(item.id)) {
-              item.disabled = true;
-            }
-            result.push(item);
-          }, []);
+        function formatFields(entityName, join) {
+          var prefix = join ? join.alias + '.' : '',
+            result = [];
+
+          function addFields(fields) {
+            _.each(fields, function(field) {
+              var item = {
+                id: prefix + field.name + (field.options ? suffix : ''),
+                text: field.label,
+                description: field.description
+              };
+              if (disabledIf(item.id)) {
+                item.disabled = true;
+              }
+              result.push(item);
+            });
+          }
+
+          // Add extra searchable fields from bridge entity
+          if (join && join.bridge) {
+            addFields(_.filter(searchMeta.getEntity(join.bridge).fields, function(field) {
+              return (field.name !== 'id' && field.name !== 'entity_id' && field.name !== 'entity_table' && !field.fk_entity);
+            }));
+          }
+
+          addFields(searchMeta.getEntity(entityName).fields);
+          return result;
         }
 
         var mainEntity = searchMeta.getEntity(ctrl.savedSearch.api_entity),
           result = [{
             text: mainEntity.title_plural,
             icon: mainEntity.icon,
-            children: formatFields(ctrl.savedSearch.api_entity, '')
+            children: formatFields(ctrl.savedSearch.api_entity)
           }];
         _.each(ctrl.savedSearch.api_params.join, function(join) {
-          var joinName = join[0].split(' AS '),
-            joinEntity = searchMeta.getEntity(joinName[0]);
+          var joinInfo = searchMeta.getJoin(join[0]),
+            joinEntity = searchMeta.getEntity(joinInfo.entity);
           result.push({
-            text: joinEntity.title_plural + ' (' + joinName[1] + ')',
+            text: joinInfo.label,
+            description: joinInfo.description,
             icon: joinEntity.icon,
-            children: formatFields(joinEntity.name, joinName[1] + '.')
+            children: formatFields(joinEntity.name, joinInfo)
           });
         });
         return result;
           enqueue(mainEntity);
         }
         _.each(ctrl.savedSearch.api_params.join, function(join) {
-          var joinName = join[0].split(' AS '),
-            joinEntity = searchMeta.getEntity(joinName[0]);
+          var joinInfo = searchMeta.getJoin(join[0]),
+            joinEntity = searchMeta.getEntity(joinInfo.entity),
+            bridgeEntity = joinInfo.bridge ? searchMeta.getEntity(joinInfo.bridge) : null;
           if (typeof joinEntity.optionsLoaded === 'undefined') {
             enqueue(joinEntity);
           }
+          if (bridgeEntity && typeof bridgeEntity.optionsLoaded === 'undefined') {
+            enqueue(bridgeEntity);
+          }
         });
         if (!_.isEmpty(entities)) {
           crmApi4(entities).then(function(results) {