Afform - Refactor toward multiple search displays on an afform
authorColeman Watts <coleman@civicrm.org>
Fri, 4 Mar 2022 03:39:19 +0000 (22:39 -0500)
committerColeman Watts <coleman@civicrm.org>
Tue, 15 Mar 2022 19:58:49 +0000 (15:58 -0400)
ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js
ext/afform/admin/ang/afGuiEditor/afGuiEditorPalette.html
ext/afform/admin/ang/afGuiEditor/afGuiSearch.component.js
ext/afform/admin/ang/afGuiEditor/afGuiSearch.html
ext/afform/admin/ang/afGuiEditor/config-form.html

index f2c4e9dcc2726f5e7501cd44b275fda47c53dea3..95e89ee2eacab14d3d98e842f3926593f1df6923 100644 (file)
 
         else if (editor.getFormType() === 'search') {
           editor.layout['#children'] = afGui.findRecursive(editor.afform.layout, {'af-fieldset': ''})[0]['#children'];
-          editor.searchDisplay = afGui.findRecursive(editor.layout['#children'], function(item) {
-            return item['#tag'] && item['#tag'].indexOf('crm-search-display-') === 0;
-          })[0];
-          editor.searchFilters = getSearchFilterOptions();
+          var searchFieldsets = afGui.findRecursive(editor.afform.layout, {'af-fieldset': ''});
+          editor.searchDisplays = _.transform(searchFieldsets, function(searchDisplays, fieldset) {
+            var displayElement = afGui.findRecursive(fieldset['#children'], function(item) {
+              return item['search-name'] && item['#tag'] && item['#tag'].indexOf('crm-search-display-') === 0;
+            })[0];
+            if (displayElement) {
+              searchDisplays[displayElement['search-name'] + (displayElement['display-name'] ? '.' + displayElement['display-name'] : '')] = {
+                element: displayElement,
+                fieldset: fieldset,
+                settings: afGui.getSearchDisplay(displayElement['search-name'], displayElement['display-name'])
+              };
+            }
+          }, {});
         }
 
         // Set changesSaved to true on initial load, false thereafter whenever changes are made to the model
       this.toggleContactSummary = function() {
         if (editor.afform.contact_summary) {
           editor.afform.contact_summary = false;
-          if (editor.afform.type === 'search') {
-            delete editor.searchDisplay.filters;
-          }
+          _.each(editor.searchDisplays, function(searchDisplay) {
+            delete searchDisplay.element.filters;
+          });
         } else {
           editor.afform.contact_summary = 'block';
-          if (editor.afform.type === 'search') {
-            editor.searchDisplay.filters = editor.searchFilters[0].key;
-          }
+          _.each(editor.searchDisplays, function(searchDisplay) {
+            var filterOptions = getSearchFilterOptions(searchDisplay.settings);
+            if (filterOptions.length) {
+              searchDisplay.element.filters = filterOptions[0].key;
+            }
+          });
         }
       };
 
-      function getSearchFilterOptions() {
-        var searchDisplay = afGui.getSearchDisplay(editor.searchDisplay['search-name'], editor.searchDisplay['display-name']),
+      function getSearchFilterOptions(searchDisplay) {
+        var
           entityCount = {},
           options = [];
 
index fe098c0f9685e7fe52a53f8643aa09d20301ee75..1d59dce2752822d827e202e19f7a07ca02d48795 100644 (file)
           <i ng-if="entity.loading" class="crm-i fa-spin fa-spinner"></i>
         </a>
       </li>
-      <li role="presentation" ng-repeat="(key, searchDisplay) in editor.meta.searchDisplays" class="fluid-width-tab" ng-class="{active: selectedEntityName === key}" title="{{ searchDisplay.label }}">
+      <li role="presentation" ng-repeat="(key, display) in editor.searchDisplays" class="fluid-width-tab" ng-class="{active: selectedEntityName === key}" title="{{ display.label }}">
         <a href ng-click="editor.selectEntity(key)">
-          <i class="crm-i {{:: searchDisplay['type:icon'] }}"></i>
-          <span>{{ searchDisplay.label }}</span>
+          <i ng-if="display.settings" class="crm-i {{:: display.settings['type:icon'] }}"></i>
+          <i ng-if="!display.settings" class="crm-i fa-spin fa-spinner"></i>
+          <span>{{ display.settings.label }}</span>
         </a>
       </li>
       <li role="presentation" class="dropdown" ng-if="editor.allowEntityConfig" title="{{:: ts('Add Entity') }}">
@@ -40,7 +41,7 @@
   <div class="panel-body" ng-repeat="entity in entities" ng-if="selectedEntityName === entity.name">
     <af-gui-entity entity="entity"></af-gui-entity>
   </div>
-  <div class="panel-body" ng-repeat="(key, searchDisplay) in editor.meta.searchDisplays" ng-if="selectedEntityName === key">
-    <af-gui-search display="searchDisplay"></af-gui-search>
+  <div class="panel-body" ng-repeat="(key, display) in editor.searchDisplays" ng-if="selectedEntityName === key">
+    <af-gui-search display="display"></af-gui-search>
   </div>
 </div>
index cd89fa8e0c3f83acbc7292268653500524b4a5bd..0247479f766c9fa9ae96a91e94372263cf1fae91 100644 (file)
       this.getFilterFields = function() {
         var fieldGroups = [],
           entities = getEntities();
-        if (ctrl.display.calc_fields && ctrl.display.calc_fields.length) {
+        if (ctrl.display.settings.calc_fields && ctrl.display.settings.calc_fields.length) {
           fieldGroups.push({
             text: ts('Calculated Fields'),
-            children: _.transform(ctrl.display.calc_fields, function(fields, el) {
+            children: _.transform(ctrl.display.settings.calc_fields, function(fields, el) {
               fields.push({id: el.name, text: el.defn.label, disabled: ctrl.fieldInUse(el.name)});
             }, [])
           });
       // Gets the name of the entity a field belongs to
       this.getFieldEntity = function(fieldName) {
         if (fieldName.indexOf('.') < 0) {
-          return ctrl.display['saved_search_id.api_entity'];
+          return ctrl.display.settings['saved_search_id.api_entity'];
         }
         var alias = fieldName.split('.')[0],
           entity;
-        _.each(ctrl.display['saved_search_id.api_params'].join, function(join) {
+        _.each(ctrl.display.settings['saved_search_id.api_params'].join, function(join) {
           var joinInfo = join[0].split(' AS ');
           if (alias === joinInfo[1]) {
             entity = joinInfo[0];
             return false;
           }
         });
-        return entity || ctrl.display['saved_search_id.api_entity'];
+        return entity || ctrl.display.settings['saved_search_id.api_entity'];
       };
 
       function buildCalcFieldList(search) {
         $scope.calcFieldList.length = 0;
-        _.each(_.cloneDeep(ctrl.display.calc_fields), function(field) {
+        _.each(_.cloneDeep(ctrl.display.settings.calc_fields), function(field) {
           if (!search || _.contains(field.defn.label.toLowerCase(), search)) {
             $scope.calcFieldList.push(field);
           }
@@ -93,7 +93,7 @@
       // Fetch all entities used in search (main entity + joins)
       function getEntities() {
         var
-          mainEntity = afGui.getEntity(ctrl.display['saved_search_id.api_entity']),
+          mainEntity = afGui.getEntity(ctrl.display.settings['saved_search_id.api_entity']),
           entityCount = {},
           entities = [{
             name: mainEntity.entity,
           }];
         entityCount[mainEntity.entity] = 1;
 
-        _.each(ctrl.display['saved_search_id.api_params'].join, function(join) {
+        _.each(ctrl.display.settings['saved_search_id.api_params'].join, function(join) {
           var joinInfo = join[0].split(' AS '),
             entity = afGui.getEntity(joinInfo[0]);
           entityCount[entity.entity] = (entityCount[entity.entity] || 0) + 1;
         if (_.findIndex(ctrl.filters, {name: fieldName}) >= 0) {
           return true;
         }
-        return !!getElement(ctrl.editor.layout['#children'], {'#tag': 'af-field', name: fieldName});
+        return !!getElement(ctrl.display.fieldset['#children'], {'#tag': 'af-field', name: fieldName});
       };
 
       // Checks if fields in a block are already in use on the form.
       // Note that if a block contains no fields it can be used repeatedly, so this will always return false for those.
       $scope.blockInUse = function(block) {
         if (block['af-join']) {
-          return !!getElement(ctrl.editor.layout['#children'], {'af-join': block['af-join']});
+          return !!getElement(ctrl.display.fieldset['#children'], {'af-join': block['af-join']});
         }
         var fieldsInBlock = _.pluck(afGui.findRecursive(afGui.meta.blocks[block['#tag']].layout, {'#tag': 'af-field'}), 'name');
-        return !!getElement(ctrl.editor.layout['#children'], function(item) {
+        return !!getElement(ctrl.display.fieldset['#children'], function(item) {
           return item['#tag'] === 'af-field' && _.includes(fieldsInBlock, item.name);
         });
       };
 
-      function getSearchDisplayElement() {
-        return getElement(ctrl.editor.layout['#children'], {'#tag': ctrl.display['type:name'], 'display-name': ctrl.display.name, 'search-name': ctrl.display['saved_search_id.name']});
-      }
-
       // Return an item matching criteria
       // Recursively checks the form layout, including block directives
       function getElement(group, criteria, found) {
       }
 
       function filtersToArray() {
-        return _.transform(ctrl.display.filters, function(result, value, key) {
+        if (!ctrl.display.element.filters || ctrl.display.element.filters === '{}') {
+          return [];
+        }
+        // Split contents by commas, ignoring commas inside quotes
+        var rawValues = _.trim(ctrl.display.element.filters, '{}').split(/,(?=(?:(?:[^']*'){2})*[^']*$)/);
+        return _.transform(rawValues, function(result, raw) {
+          raw = _.trim(raw);
+          var split;
+          if (raw.charAt(0) === '"') {
+            split = raw.slice(1).split(/"[ ]*:/);
+          } else if (raw.charAt(0) === "'") {
+            split = raw.slice(1).split(/'[ ]*:/);
+          } else {
+            split = raw.split(':');
+          }
+          var key = _.trim(split[0]);
+          var value = _.trim(split[1]);
+          var mode = 'val';
+          if (value.indexOf('routeParams') === 0) {
+            mode = 'routeParams';
+          } else if (value.indexOf('options') === 0) {
+            mode = 'options';
+          }
           var info = {
             name: key,
-            mode: value.indexOf('routeParams') === 0 ? 'url' : 'val'
+            mode: mode
           };
           // Object dot notation
-          if (info.mode === 'url' && value.indexOf('routeParams.') === 0) {
-            info.value = value.replace('routeParams.', '');
+          if (mode !== 'val' && value.indexOf(mode + '.') === 0) {
+            info.value = value.replace(mode + '.', '');
           }
           // Object bracket notation
-          else if (info.mode === 'url') {
+          else if (mode !== 'val') {
             info.value = decode(value.substring(value.indexOf('[') + 1, value.lastIndexOf(']')));
           }
           // Literal value
         ctrl.filters.push({
           name: fieldName,
           value: fieldName,
-          mode: 'url'
+          mode: 'routeParams'
         });
       };
 
       // Respond to changing a filter field name
       this.onChangeFilter = function(index) {
         var filter = ctrl.filters[index];
-        if (filter.name) {
-          filter.mode = 'url';
-          filter.value = filter.name;
-        } else {
+        // Clear filter
+        if (!filter.name) {
           ctrl.filters.splice(index, 1);
+        } else if (filter.mode === 'routeParams') {
+          // Set default value for routeParams
+          filter.value = filter.name;
         }
       };
 
       // Convert filters array to js notation & add to crm-search-display element
       function writeFilters() {
-        var element = getSearchDisplayElement(),
-          output = [];
+        var output = [];
         if (!ctrl.filters.length) {
-          delete element.filters;
+          if ('filters' in ctrl.display.element) {
+            delete ctrl.display.element.filters;
+          }
           return;
         }
         _.each(ctrl.filters, function(filter) {
             filter.name.match(/\W/) ? encode(filter.name) : filter.name,
           ];
           // Object dot notation
-          if (filter.mode === 'url' && !filter.value.match(/\W/)) {
-            keyVal.push('routeParams.' + filter.value);
+          if (filter.mode !== 'val' && !filter.value.match(/\W/)) {
+            keyVal.push(filter.mode + '.' + filter.value);
           }
           // Object bracket notation
-          else if (filter.mode === 'url') {
-            keyVal.push('routeParams[' + encode(filter.value) + ']');
+          else if (filter.mode !== 'val') {
+            keyVal.push(filter.mode + '[' + encode(filter.value) + ']');
           }
           // Literal value
           else {
           }
           output.push(keyVal.join(': '));
         });
-        element.filters = '{' + output.join(', ') + '}';
+        ctrl.display.element.filters = '{' + output.join(', ') + '}';
       }
 
       this.$onInit = function() {
index ee688e42b7318bbdef384dc139871d056a057aa3..bc8a7f908c2e1a77bb440db2fb96169a138033ab 100644 (file)
@@ -5,20 +5,25 @@
       <input class="form-control" ng-model="filter.name" ng-change="$ctrl.onChangeFilter($index)" crm-ui-select="{data: $ctrl.getFilterFields, placeholder: ' '}" />
       <div class="input-group">
         <div class="input-group-btn">
-          <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-            {{ filter.mode === 'url' ? ts('Url') : ts('Value') }}
+          <button type="button" ng-switch="filter.mode" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+            <span ng-switch-when="routeParams">{{:: ts('Url') }}</span>
+            <span ng-switch-when="val">{{:: ts('Value') }}</span>
+            <span ng-switch-when="options">{{:: ts('Current Contact') }}</span>
             <span class="caret"></span>
           </button>
           <ul class="dropdown-menu">
             <li>
-              <a href ng-click="$ctrl.onChangeFilter($index)">{{:: ts('Url variable') }}</a>
+              <a href ng-click="filter.mode = 'routeParams'; $ctrl.onChangeFilter($index)">{{:: ts('Url variable') }}</a>
             </li>
             <li>
               <a href ng-click="filter.mode = 'val'; filter.value = ''">{{:: ts('Fixed value') }}</a>
             </li>
+            <li ng-if="$ctrl.editor.afform.contact_summary">
+              <a href ng-click="filter.mode = 'options'; filter.value = 'contact_id';">{{:: ts('Current Contact') }}</a>
+            </li>
           </ul>
         </div>
-        <input ng-if="filter.mode === 'url'" class="form-control" ng-model="filter.value" />
+        <input ng-if="filter.mode === 'routeParams'" class="form-control" ng-model="filter.value" />
         <span ng-if="filter.mode === 'val'">
           <input class="form-control" af-gui-field-value="getField($ctrl.getFieldEntity(filter.name), filter.name)" ng-model="filter.value" />
         </span>
index 56ad2334c454a2a3df2758011da7827595a297d8..14a8946726024d349f99e9fdd831f886892dd3f3 100644 (file)
       </div>
       <p class="help-block">{{:: ts('Placement can be configured using the Contact Layout Editor.') }}</p>
     </div>
-    <div class="form-group" ng-if="editor.afform.contact_summary && editor.searchDisplay && editor.searchFilters.length > 1">
-      <div class="form-inline">
-        <label for="af_config_form_search_filters">
-          {{:: ts('Filter on:') }}
-        </label>
-        <select class="form-control" id="af_config_form_search_filters" ng-model="editor.searchDisplay.filters">
-          <option ng-repeat="option in editor.searchFilters" value="{{ option.key }}">{{ option.label }}</option>
-        </select>
-      </div>
-      <p class="help-block">{{:: ts('Choose which contact from the search should match the contact being viewed.') }}</p>
-    </div>
   </fieldset>
 
   <!--  Submit actions are only applicable to form types with a submit button (exclude blocks and search forms) -->