SearchKit - Autogenerate default display table for saved searches; calculate links...
[civicrm-core.git] / ext / search_kit / ang / crmSearchAdmin / crmSearchAdmin.component.js
index 9145885821d8100dd139a0131b85d657dbe54de0..1100fe36f60108290defd58f21a754a6b83ddb39 100644 (file)
@@ -6,13 +6,15 @@
       savedSearch: '<'
     },
     templateUrl: '~/crmSearchAdmin/crmSearchAdmin.html',
-    controller: function($scope, $element, $location, $timeout, crmApi4, dialogService, searchMeta, formatForSelect2) {
+    controller: function($scope, $element, $location, $timeout, crmApi4, dialogService, searchMeta) {
       var ts = $scope.ts = CRM.ts('org.civicrm.search_kit'),
         ctrl = this,
+        afformLoad,
         fieldsForJoinGetters = {};
 
       this.DEFAULT_AGGREGATE_FN = 'GROUP_CONCAT';
-
+      this.afformEnabled = CRM.crmSearchAdmin.afformEnabled;
+      this.afformAdminEnabled = CRM.crmSearchAdmin.afformAdminEnabled;
       this.displayTypes = _.indexBy(CRM.crmSearchAdmin.displayTypes, 'id');
 
       $scope.controls = {tab: 'compose', joinType: 'LEFT'};
@@ -73,6 +75,7 @@
         });
 
         loadFieldOptions();
+        loadAfforms();
       };
 
       function onChangeAnything() {
@@ -93,6 +96,7 @@
         } else if (params.id) {
           apiCalls.deleteGroup = ['Group', 'delete', {where: [['saved_search_id', '=', params.id]]}];
         }
+        _.remove(params.displays, {trashed: true});
         if (params.displays && params.displays.length) {
           chain.displays = ['SearchDisplay', 'replace', {where: [['saved_search_id', '=', '$id']], records: params.displays}];
         } else if (params.id) {
           } else if (!display.trashed) {
             $scope.selectTab('display_' + index);
           }
+          if (display.trashed && afformLoad) {
+            afformLoad.then(function() {
+              if (ctrl.afforms[display.name]) {
+                var msg = ctrl.afforms[display.name].length === 1 ?
+                  ts('Form "%1" will be deleted if the embedded display "%2" is deleted.', {1: ctrl.afforms[display.name][0].title, 2: display.label}) :
+                  ts('%1 forms will be deleted if the embedded display "%2" is deleted.', {1: ctrl.afforms[display.name].length, 2: display.label});
+                CRM.alert(msg, ts('Display embedded'), 'alert');
+              }
+            });
+          }
         } else {
           $scope.selectTab('compose');
           ctrl.savedSearch.displays.splice(index, 1);
       function reconcileAggregateColumns() {
         _.each(ctrl.savedSearch.api_params.select, function(col, pos) {
           var info = searchMeta.parseExpr(col),
-            fieldExpr = info.path + info.suffix;
+            fieldExpr = (_.findWhere(info.args, {type: 'field'}) || {}).value;
           if (ctrl.canAggregate(col)) {
             // Ensure all non-grouped columns are aggregated if using GROUP BY
             if (!info.fn || info.fn.category !== 'aggregate') {
-              ctrl.savedSearch.api_params.select[pos] = ctrl.DEFAULT_AGGREGATE_FN + '(DISTINCT ' + fieldExpr + ') AS ' + ctrl.DEFAULT_AGGREGATE_FN + '_DISTINCT_' + fieldExpr.replace(/[.:]/g, '_');
+              ctrl.savedSearch.api_params.select[pos] = ctrl.DEFAULT_AGGREGATE_FN + '(DISTINCT ' + fieldExpr + ') AS ' + ctrl.DEFAULT_AGGREGATE_FN + '_' + fieldExpr.replace(/[.:]/g, '_');
             }
           } else {
             // Remove aggregate functions when no grouping
 
       // Deletes an item from an array param
       this.clearParam = function(name, idx) {
+        if (name === 'select') {
+          // Function selectors use `ng-repeat` with `track by $index` so must be refreshed when splicing the array
+          ctrl.hideFuncitons();
+        }
         ctrl.savedSearch.api_params[name].splice(idx, 1);
       };
 
+      this.hideFuncitons = function() {
+        $scope.controls.showFunctions = false;
+      };
+
       function onChangeSelect(newSelect, oldSelect) {
         // When removing a column from SELECT, also remove from ORDER BY & HAVING
         _.each(_.difference(oldSelect, newSelect), function(col) {
         if (!ctrl.savedSearch.api_params.groupBy.length) {
           return false;
         }
-        var info = searchMeta.parseExpr(col);
+        var arg = _.findWhere(searchMeta.parseExpr(col).args, {type: 'field'}) || {};
+        // If the column is not a database field, no
+        if (!arg.field || !arg.field.entity || arg.field.type !== 'Field') {
+          return false;
+        }
         // If the column is used for a groupBy, no
-        if (ctrl.savedSearch.api_params.groupBy.indexOf(info.path) > -1) {
+        if (ctrl.savedSearch.api_params.groupBy.indexOf(arg.path) > -1) {
           return false;
         }
         // If the entity this column belongs to is being grouped by primary key, then also no
-        var idField = searchMeta.getEntity(info.field.entity).primary_key[0];
-        return ctrl.savedSearch.api_params.groupBy.indexOf(info.prefix + idField) < 0;
+        var idField = searchMeta.getEntity(arg.field.entity).primary_key[0];
+        return ctrl.savedSearch.api_params.groupBy.indexOf(arg.prefix + idField) < 0;
       };
 
       $scope.fieldsForGroupBy = function() {
       };
 
       function getFieldsForJoin(joinEntity) {
-        return {results: ctrl.getAllFields(':name', ['Field', 'Custom'], null, joinEntity)};
+        return {results: ctrl.getAllFields(':name', ['Field'], null, joinEntity)};
       }
 
+      // @return {function}
       $scope.fieldsForJoin = function(joinEntity) {
         if (!fieldsForJoinGetters[joinEntity]) {
           fieldsForJoinGetters[joinEntity] = _.wrap(joinEntity, getFieldsForJoin);
 
       this.getAllFields = function(suffix, allowedTypes, disabledIf, topJoin) {
         disabledIf = disabledIf || _.noop;
-        function formatFields(entityName, join) {
+
+        function formatEntityFields(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;
-              }
-              if (!allowedTypes || _.includes(allowedTypes, field.type)) {
-                result.push(item);
-              }
-            });
-          }
-
           // Add extra searchable fields from bridge entity
           if (join && join.bridge) {
-            addFields(_.filter(searchMeta.getEntity(join.bridge).fields, function(field) {
+            formatFields(_.filter(searchMeta.getEntity(join.bridge).fields, function(field) {
               return (field.name !== 'id' && field.name !== 'entity_id' && field.name !== 'entity_table' && !field.fk_entity);
-            }));
+            }), result, prefix);
           }
 
-          addFields(searchMeta.getEntity(entityName).fields);
+          formatFields(searchMeta.getEntity(entityName).fields, result, prefix);
+          return result;
+        }
+
+        function formatFields(fields, result, prefix) {
+          prefix = typeof prefix === 'undefined' ? '' : prefix;
+          _.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;
+            }
+            if (!allowedTypes || _.includes(allowedTypes, field.type)) {
+              result.push(item);
+            }
+          });
           return result;
         }
 
             text: joinInfo.label,
             description: joinInfo.description,
             icon: joinEntity.icon,
-            children: formatFields(joinEntity.name, joinInfo)
+            children: formatEntityFields(joinEntity.name, joinInfo)
           });
         }
 
         result.push({
           text: mainEntity.title_plural,
           icon: mainEntity.icon,
-          children: formatFields(ctrl.savedSearch.api_entity)
+          children: formatEntityFields(ctrl.savedSearch.api_entity)
         });
+
+        // Include SearchKit's pseudo-fields if specifically requested
+        if (allowedTypes && _.includes(allowedTypes, 'Pseudo')) {
+          result.push({
+            text: ts('Extra'),
+            icon: 'fa-gear',
+            children: formatFields(CRM.crmSearchAdmin.pseudoFields, [])
+          });
+        }
+
         _.each(joinEntities, addJoin);
         return result;
       };
           var item = {
             id: info.alias,
             text: ctrl.getFieldLabel(name),
-            description: info.field && info.field.description
+            description: info.fn ? info.fn.description : info.args[0].field && info.args[0].field.description
           };
           if (disabledIf(item.id)) {
             item.disabled = true;
         });
       };
 
+      this.isPseudoField = function(name) {
+        return _.findIndex(CRM.crmSearchAdmin.pseudoFields, {name: name}) >= 0;
+      };
+
       /**
        * Fetch pseudoconstants for main entity + joined entities
        *
           var join = searchMeta.getJoin(joinClause[0]),
             joinEntity = searchMeta.getEntity(join.entity),
             primaryKey = joinEntity.primary_key[0],
+            // Links for aggregate columns get aggregated using GROUP_CONCAT
             isAggregate = ctrl.canAggregate(join.alias + '.' + primaryKey),
+            joinPrefix = (isAggregate ? ctrl.DEFAULT_AGGREGATE_FN + '_' : '') + join.alias + '.',
             bridgeEntity = _.isString(joinClause[2]) ? searchMeta.getEntity(joinClause[2]) : null;
           _.each(joinEntity.paths, function(path) {
             var link = _.cloneDeep(path);
-            link.isAggregate = isAggregate;
-            link.path = link.path.replace(/\[/g, '[' + join.alias + '.');
+            link.path = link.path.replace(/\[/g, '[' + joinPrefix);
+            if (isAggregate) {
+              link.path = link.path.replace(/[.:]/g, '_');
+            }
             link.join = join.alias;
             addTitle(link, join.label);
             links.push(link);
         // Links to implicit joins
         _.each(ctrl.savedSearch.api_params.select, function(fieldName) {
           if (!_.includes(fieldName, ' AS ')) {
-            var info = searchMeta.parseExpr(fieldName);
-            if (info.field && !info.suffix && !info.fn && (info.field.fk_entity || info.field.name !== info.field.fieldName)) {
+            var info = searchMeta.parseExpr(fieldName).args[0];
+            if (info.field && !info.suffix && !info.fn && info.field.type === 'Field' && (info.field.fk_entity || info.field.name !== info.field.fieldName)) {
               var idFieldName = info.field.fk_entity ? fieldName : fieldName.substr(0, fieldName.lastIndexOf('.')),
-                idField = searchMeta.parseExpr(idFieldName).field;
+                idField = searchMeta.parseExpr(idFieldName).args[0].field;
               if (!ctrl.canAggregate(idFieldName)) {
                 var joinEntity = searchMeta.getEntity(idField.fk_entity),
                   label = (idField.join ? idField.join.label + ': ' : '') + (idField.input_attrs && idField.input_attrs.label || idField.label);
         return _.uniq(links, 'path');
       };
 
+      function loadAfforms() {
+        if (ctrl.afformEnabled && ctrl.savedSearch.id) {
+          var findDisplays = _.transform(ctrl.savedSearch.displays, function(findDisplays, display) {
+            if (display.id && display.name) {
+              findDisplays.push(['search_displays', 'CONTAINS', ctrl.savedSearch.name + '.' + display.name]);
+            }
+          }, [['search_displays', 'CONTAINS', ctrl.savedSearch.name]]);
+          afformLoad = crmApi4('Afform', 'get', {
+            select: ['name', 'title', 'search_displays'],
+            where: [['OR', findDisplays]]
+          }).then(function(afforms) {
+            ctrl.afforms = {};
+            _.each(afforms, function(afform) {
+              _.each(_.uniq(afform.search_displays), function(searchNameDisplayName) {
+                var displayName = searchNameDisplayName.split('.')[1] || '';
+                ctrl.afforms[displayName] = ctrl.afforms[displayName] || [];
+                ctrl.afforms[displayName].push({
+                  title: afform.title,
+                  link: ctrl.afformAdminEnabled ? CRM.url('civicrm/admin/afform#/edit/' + afform.name) : '',
+                });
+              });
+            });
+          });
+        }
+      }
+
+      // Creating an Afform opens a new tab, so when switching back to this tab, re-check for Afforms
+      $(window).on('focus', _.debounce(function() {
+        $scope.$apply(loadAfforms);
+      }, 10000, {leading: true, trailing: false}));
+
     }
   });