From: Coleman Watts Date: Fri, 4 Mar 2022 20:39:08 +0000 (-0500) Subject: Afform - Enable multiple search displays in a search form layout X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=7cc347c2bd153f0636a9fdd3597f1c6c76008fcf;p=civicrm-core.git Afform - Enable multiple search displays in a search form layout Allows dashboard-like layouts to be composed --- diff --git a/ext/afform/admin/Civi/Api4/Action/Afform/LoadAdminData.php b/ext/afform/admin/Civi/Api4/Action/Afform/LoadAdminData.php index aa85b05daf..9248593c76 100644 --- a/ext/afform/admin/Civi/Api4/Action/Afform/LoadAdminData.php +++ b/ext/afform/admin/Civi/Api4/Action/Afform/LoadAdminData.php @@ -184,7 +184,7 @@ class LoadAdminData extends \Civi\Api4\Generic\AbstractAction { ->setSavedSearch($displayTag['search-name']); } $display = $displayGet - ->addSelect('*', 'type:name', 'type:icon', 'saved_search_id.name', 'saved_search_id.api_entity', 'saved_search_id.api_params') + ->addSelect('*', 'type:name', 'type:icon', 'saved_search_id.name', 'saved_search_id.label', 'saved_search_id.api_entity', 'saved_search_id.api_params') ->execute()->first(); $display['calc_fields'] = $this->getCalcFields($display['saved_search_id.api_entity'], $display['saved_search_id.api_params']); $display['filters'] = empty($displayTag['filters']) ? NULL : (\CRM_Utils_JS::getRawProps($displayTag['filters']) ?: NULL); diff --git a/ext/afform/admin/ang/afAdmin/afAdminList.controller.js b/ext/afform/admin/ang/afAdmin/afAdminList.controller.js index 019281936b..cb25346714 100644 --- a/ext/afform/admin/ang/afAdmin/afAdminList.controller.js +++ b/ext/afform/admin/ang/afAdmin/afAdminList.controller.js @@ -1,7 +1,7 @@ (function(angular, $, _) { "use strict"; - angular.module('afAdmin').controller('afAdminList', function($scope, afforms, crmApi4, crmStatus) { + angular.module('afAdmin').controller('afAdminList', function($scope, afforms, crmApi4, crmStatus, afGui) { var ts = $scope.ts = CRM.ts('org.civicrm.afform_admin'), ctrl = $scope.$ctrl = this; this.sortField = 'title'; @@ -65,10 +65,13 @@ } this.createLinks = function() { - ctrl.searchCreateLinks = ''; - if ($scope.types[ctrl.tab].options) { + // Reset search input in dropdown + $scope.searchCreateLinks.label = ''; + // A value means it's alredy loaded. Null means it's loading. + if ($scope.types[ctrl.tab].options || $scope.types[ctrl.tab].options === null) { return; } + $scope.types[ctrl.tab].options = null; var links = []; if (ctrl.tab === 'form') { @@ -102,33 +105,7 @@ } if (ctrl.tab === 'search') { - var searchNames = []; - // Non-aggregated query will return the same search multiple times - once per display - crmApi4('SavedSearch', 'get', { - select: ['name', 'label', 'display.name', 'display.label', 'display.type:icon'], - where: [['api_entity', 'IS NOT NULL'], ['api_params', 'IS NOT NULL']], - join: [['SearchDisplay AS display', 'LEFT', ['id', '=', 'display.saved_search_id']]], - orderBy: {'label':'ASC'} - }).then(function(searches) { - _.each(searches, function(search) { - // Add default display for each search (track searchNames in a var to just add once per search) - if (!_.includes(searchNames, search.name)) { - searchNames.push(search.name); - links.push({ - url: '#create/search/' + search.name, - label: search.label + ': ' + ts('Search results table'), - icon: 'fa-table' - }); - } - // If the search has no displays (other than the default) this will be empty - if (search['display.name']) { - links.push({ - url: '#create/search/' + search.name + '.' + search['display.name'], - label: search.label + ': ' + search['display.label'], - icon: search['display.type:icon'] - }); - } - }); + afGui.getAllSearchDisplays().then(function(links) { $scope.types.search.options = links; }); } diff --git a/ext/afform/admin/ang/afGuiEditor.js b/ext/afform/admin/ang/afGuiEditor.js index b21e5a4447..60391af1f2 100644 --- a/ext/afform/admin/ang/afGuiEditor.js +++ b/ext/afform/admin/ang/afGuiEditor.js @@ -137,6 +137,45 @@ return CRM.afGuiEditor.searchDisplays[searchName + (displayName ? '.' + displayName : '')]; }, + getAllSearchDisplays: function() { + var links = [], + searchNames = [], + deferred = $q.defer(); + // Non-aggregated query will return the same search multiple times - once per display + crmApi4('SavedSearch', 'get', { + select: ['name', 'label', 'display.name', 'display.label', 'display.type:name', 'display.type:icon'], + where: [['api_entity', 'IS NOT NULL'], ['api_params', 'IS NOT NULL']], + join: [['SearchDisplay AS display', 'LEFT', ['id', '=', 'display.saved_search_id']]], + orderBy: {'label':'ASC'} + }).then(function(searches) { + _.each(searches, function(search) { + // Add default display for each search (track searchNames in a var to just add once per search) + if (!_.includes(searchNames, search.name)) { + searchNames.push(search.name); + links.push({ + key: search.name, + url: '#create/search/' + search.name, + label: search.label + ': ' + ts('Search results table'), + tag: 'crm-search-display-table', + icon: 'fa-table' + }); + } + // If the search has no displays (other than the default) this will be empty + if (search['display.name']) { + links.push({ + key: search.name + '.' + search['display.name'], + url: '#create/search/' + search.name + '.' + search['display.name'], + label: search.label + ': ' + search['display.label'], + tag: search['display.type:name'], + icon: search['display.type:icon'] + }); + } + }); + deferred.resolve(links); + }); + return deferred.promise; + }, + // Recursively searches a collection and its children using _.filter // Returns an array of all matches, or an object if the indexBy param is used findRecursive: function findRecursive(collection, predicate, indexBy) { diff --git a/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js b/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js index 95e89ee2ea..6e62c1fccc 100644 --- a/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js +++ b/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js @@ -24,6 +24,7 @@ this.afform = null; $scope.saving = false; $scope.selectedEntityName = null; + $scope.searchDisplayListFilter = {}; this.meta = afGui.meta; var editor = this, sortableOptions = {}; @@ -73,9 +74,11 @@ editor.layout['#children'].push(afGui.meta.elements.submit.element); } } - - else if (editor.getFormType() === 'block') { + else { editor.layout['#children'] = editor.afform.layout; + } + + if (editor.getFormType() === 'block') { editor.blockEntity = editor.afform.join_entity || editor.afform.entity_type; $scope.entities[editor.blockEntity] = backfillEntityDefaults({ type: editor.blockEntity, @@ -85,20 +88,7 @@ } else if (editor.getFormType() === 'search') { - editor.layout['#children'] = afGui.findRecursive(editor.afform.layout, {'af-fieldset': ''})[0]['#children']; - 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']) - }; - } - }, {}); + editor.searchDisplays = getSearchDisplaysOnForm(); } // Set changesSaved to true on initial load, false thereafter whenever changes are made to the model @@ -245,6 +235,92 @@ } }; + // Collects all search displays currently on the form + function getSearchDisplaysOnForm() { + var searchFieldsets = afGui.findRecursive(editor.afform.layout, {'af-fieldset': ''}); + return _.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']) + }; + } + }, {}); + } + + // Load data for "Add search display" dropdown + this.getSearchDisplaySelector = function() { + // Reset search input in dropdown + $scope.searchDisplayListFilter.label = ''; + // A value means it's alredy loaded. Null means it's loading. + if (!editor.searchOptions && editor.searchOptions !== null) { + editor.searchOptions = null; + afGui.getAllSearchDisplays().then(function(links) { + editor.searchOptions = links; + }); + } + }; + + this.addSearchDisplay = function(display) { + var searchName = display.key.split('.')[0]; + var displayName = display.key.split('.')[1] || ''; + var fieldset = { + '#tag': 'div', + 'af-fieldset': '', + '#children': [ + { + '#tag': display.tag, + 'search-name': searchName, + 'display-name': displayName, + } + ] + }; + var meta = { + fieldset: fieldset, + element: fieldset['#children'][0], + settings: afGui.getSearchDisplay(searchName, displayName), + }; + editor.searchDisplays[display.key] = meta; + + function addToCanvas() { + editor.layout['#children'].push(fieldset); + editor.selectEntity(display.key); + } + if (meta.settings) { + addToCanvas(); + } else { + $timeout(editor.adjustTabWidths); + crmApi4('Afform', 'loadAdminData', { + definition: {type: 'search'}, + entity: display.key + }, 0).then(function(data) { + afGui.addMeta(data); + meta.settings = afGui.getSearchDisplay(searchName, displayName); + addToCanvas(); + }); + } + }; + + // Triggered by afGuiContainer.removeElement + this.onRemoveElement = function() { + // Keep this.searchDisplays in-sync when deleteing stuff from the form + if (editor.getFormType() === 'search') { + var current = getSearchDisplaysOnForm(); + _.each(_.keys(editor.searchDisplays), function(key) { + if (!(key in current)) { + delete editor.searchDisplays[key]; + editor.selectEntity(null); + } + }); + } + }; + + // This function used to be needed to build a menu of available contact_id fields + // but is no longer used for that and is overkill for what it does now. function getSearchFilterOptions(searchDisplay) { var entityCount = {}, diff --git a/ext/afform/admin/ang/afGuiEditor/afGuiEditorPalette.html b/ext/afform/admin/ang/afGuiEditor/afGuiEditorPalette.html index 1d59dce275..56b6eccb03 100644 --- a/ext/afform/admin/ang/afGuiEditor/afGuiEditorPalette.html +++ b/ext/afform/admin/ang/afGuiEditor/afGuiEditorPalette.html @@ -16,7 +16,7 @@
  • -
  • {{ !block ? ts('Delete this container') : ts('Delete this block') }}
  • +
  • {{ !block ? ts('Remove container') : ts('Remove block') }}
  • diff --git a/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.component.js b/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.component.js index f076241ace..af68e05108 100644 --- a/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.component.js +++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.component.js @@ -52,9 +52,35 @@ return entityName === ctrl.editor.getSelectedEntityName(); }; + $scope.isSelectedSearchFieldset = function(node) { + var key = $scope.getSearchKey(node); + return key === ctrl.editor.getSelectedEntityName(); + }; + + $scope.getSearchKey = function(node) { + var searchDisplays = afGui.findRecursive(node['#children'], function(item) { + return item['#tag'] && item['#tag'].indexOf('crm-search-display-') === 0 && item['search-name']; + }); + if (searchDisplays && searchDisplays.length) { + return searchDisplays[0]['search-name'] + (searchDisplays[0]['display-name'] ? '.' + searchDisplays[0]['display-name'] : ''); + } + }; + + this.getSearchDisplay = function(node) { + var searchKey = $scope.getSearchKey(node); + if (searchKey) { + return afGui.getSearchDisplay.apply(null, searchKey.split('.')); + } + }; + $scope.selectEntity = function() { if (ctrl.node['af-fieldset']) { ctrl.editor.selectEntity(ctrl.node['af-fieldset']); + } else if ('af-fieldset' in ctrl.node) { + var searchKey = $scope.getSearchKey(ctrl.node); + if (searchKey) { + ctrl.editor.selectEntity(searchKey); + } } }; @@ -269,9 +295,12 @@ if (node['#tag'] === 'af-field') { return 'field'; } - if ('af-fieldset' in node) { + if (node['af-fieldset']) { return 'fieldset'; } + else if ('af-fieldset' in node) { + return 'searchFieldset'; + } if (node['af-join']) { return 'join'; } @@ -289,6 +318,7 @@ this.removeElement = function(element) { afGui.removeRecursive($scope.getSetChildren(), {$$hashKey: element.$$hashKey}); + ctrl.editor.onRemoveElement(); }; this.removeField = function(fieldName) { @@ -320,8 +350,9 @@ } // If entityName is not declared, this field belongs to a search var entityType, + searchDisplay = ctrl.getSearchDisplay(ctrl.node), prefix = _.includes(fieldName, '.') ? fieldName.split('.')[0] : null; - _.each(afGui.meta.searchDisplays, function(searchDisplay) { + if (searchDisplay) { if (prefix) { _.each(searchDisplay['saved_search_id.api_params'].join, function(join) { var joinInfo = join[0].split(' AS '); @@ -334,10 +365,7 @@ if (!entityType && fieldName && afGui.getField(searchDisplay['saved_search_id.api_entity'], fieldName)) { entityType = searchDisplay['saved_search_id.api_entity']; } - if (entityType) { - return false; - } - }); + } return entityType || _.map(afGui.meta.searchDisplays, 'saved_search_id.api_entity')[0]; }; diff --git a/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.html b/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.html index ad3fe18eb1..c8852476ca 100644 --- a/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.html +++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.html @@ -1,6 +1,7 @@
    {{ $ctrl.editor.getEntity($ctrl.entityName).label }} + {{:: ts('Search Display') }} {{ $ctrl.join ? ts($ctrl.join) + ':' : ts('Block:') }} {{ tags[$ctrl.node['#tag']] }} + + +
    + +
  • +
  • + +
  • {{:: ts('Remove display') }}
  • diff --git a/ext/afform/admin/ang/afGuiEditor/elements/afGuiSearchDisplay.html b/ext/afform/admin/ang/afGuiEditor/elements/afGuiSearchDisplay.html index 7666a197af..90b5fdeede 100644 --- a/ext/afform/admin/ang/afGuiEditor/elements/afGuiSearchDisplay.html +++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiSearchDisplay.html @@ -1,6 +1,6 @@
    - {{ $ctrl.display.label }} + {{:: $ctrl.display['saved_search_id.label'] }}: {{:: $ctrl.display.label }}

    - +