Skip to content

Commit

Permalink
Merge pull request #22887 from colemanw/afformLayout
Browse files Browse the repository at this point in the history
Afform - compose layouts with multiple SearchKit displays
  • Loading branch information
eileenmcnaughton authored Mar 17, 2022
2 parents ea5619d + 735574e commit f31546c
Show file tree
Hide file tree
Showing 15 changed files with 296 additions and 117 deletions.
2 changes: 1 addition & 1 deletion ext/afform/admin/Civi/Api4/Action/Afform/LoadAdminData.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ public function _run(\Civi\Api4\Generic\Result $result) {
->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);
Expand Down
37 changes: 7 additions & 30 deletions ext/afform/admin/ang/afAdmin/afAdminList.controller.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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;
});
}
Expand Down
4 changes: 2 additions & 2 deletions ext/afform/admin/ang/afAdmin/afAdminList.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ <h1 crm-page-title>{{:: ts('Form Builder') }}</h1>
<div class="form-inline">
<label for="afform-list-filter">{{:: ts('Filter:') }}</label>
<input class="form-control" type="search" id="afform-list-filter" ng-model="$ctrl.searchAfformList" placeholder="&#xf002">
<div class="btn-group pull-right" ng-if="types[$ctrl.tab].options !== false">
<div class="btn-group pull-right" ng-if="types[$ctrl.tab].options !== false" af-gui-menu>
<a ng-if="types[$ctrl.tab].default" href="{{ types[$ctrl.tab].default }}" class="btn btn-primary">
{{ ts('New %1', {1: types[$ctrl.tab].label }) }}
</a>
<button type="button" ng-click="$ctrl.createLinks()" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span ng-class="{'sr-only': types[$ctrl.tab].default}">{{ ts('New %1', {1: types[$ctrl.tab].label }) }}</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<ul class="dropdown-menu" ng-if="menu.open">
<li ng-class="{disabled: !types[$ctrl.tab].options || !types[$ctrl.tab].options.length}">
<input ng-if="types[$ctrl.tab].options && types[$ctrl.tab].options.length" type="search" class="form-control" placeholder="&#xf002" ng-model="searchCreateLinks.label">
<a href ng-if="!types[$ctrl.tab].options"><i class="crm-i fa-spinner fa-spin"></i></a>
Expand Down
39 changes: 39 additions & 0 deletions ext/afform/admin/ang/afGuiEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
116 changes: 102 additions & 14 deletions ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
this.afform = null;
$scope.saving = false;
$scope.selectedEntityName = null;
$scope.searchDisplayListFilter = {};
this.meta = afGui.meta;
var editor = this,
sortableOptions = {};
Expand Down Expand Up @@ -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,
Expand All @@ -85,11 +88,7 @@
}

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();
editor.searchDisplays = getSearchDisplaysOnForm();
}

// Set changesSaved to true on initial load, false thereafter whenever changes are made to the model
Expand Down Expand Up @@ -222,19 +221,108 @@
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;
}
});
}
};

// 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);
}
});
}
};

function getSearchFilterOptions() {
var searchDisplay = afGui.getSearchDisplay(editor.searchDisplay['search-name'], editor.searchDisplay['display-name']),
// 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 = {},
options = [];

Expand Down
35 changes: 27 additions & 8 deletions ext/afform/admin/ang/afGuiEditor/afGuiEditorPalette.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,18 @@
<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 }}">
<a href ng-click="editor.selectEntity(key)">
<i class="crm-i {{:: searchDisplay['type:icon'] }}"></i>
<span>{{ searchDisplay.label }}</span>
<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="display.settings && editor.selectEntity(key)">
<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') }}">
<li role="presentation" class="dropdown" ng-if="editor.allowEntityConfig" title="{{:: ts('Add Entity') }}" af-gui-menu>
<a href class="dropdown-toggle" data-toggle="dropdown">
<i class="crm-i fa-plus"></i>
</a>
<ul class="dropdown-menu dropdown-menu-right">
<ul class="dropdown-menu" ng-if="menu.open">
<li ng-repeat="(entityName, entity) in editor.meta.entities" ng-if="entity.defaults">
<a href ng-click="editor.addEntity(entityName, true)">
<i class="crm-i {{:: entity.icon }}"></i>
Expand All @@ -34,13 +35,31 @@
</li>
</ul>
</li>
<li role="presentation" class="dropdown" ng-if="editor.getFormType() === 'search'" title="{{:: ts('Add Search') }}" af-gui-menu>
<a href class="dropdown-toggle" data-toggle="dropdown" ng-click="editor.getSearchDisplaySelector();">
<i class="crm-i fa-plus"></i>
</a>
<ul class="dropdown-menu" ng-if="menu.open">
<li ng-class="{disabled: !editor.searchOptions || !editor.searchOptions.length}">
<input ng-if="editor.searchOptions && editor.searchOptions.length" type="search" class="form-control" placeholder="&#xf002" ng-model="searchDisplayListFilter.label">
<a href ng-if="!editor.searchOptions"><i class="crm-i fa-spinner fa-spin"></i></a>
<a href ng-if="editor.searchOptions && !editor.searchOptions.length">{{:: ts('None Found') }}</a>
</li>
<li ng-repeat="link in editor.searchOptions | filter:searchDisplayListFilter" class="{{:: link.class }}">
<a href ng-click="editor.addSearchDisplay(link)">
<i class="crm-i {{:: link.icon }}"></i>
{{:: link.label }}
</a>
</li>
</ul>
</li>
</ul>
</div>
<div class="panel-body" ng-include="'~/afGuiEditor/config-form.html'" ng-if="selectedEntityName === null"></div>
<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>
Loading

0 comments on commit f31546c

Please sign in to comment.