Skip to content

Commit

Permalink
Afform - Enable multiple search displays in a search form layout
Browse files Browse the repository at this point in the history
Allows dashboard-like layouts to be composed
  • Loading branch information
colemanw committed Mar 13, 2022
1 parent 1e00d81 commit e7e1e02
Show file tree
Hide file tree
Showing 12 changed files with 215 additions and 63 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
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
108 changes: 92 additions & 16 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,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
Expand Down Expand Up @@ -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 = {},
Expand Down
22 changes: 20 additions & 2 deletions ext/afform/admin/ang/afGuiEditor/afGuiEditorPalette.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
</a>
</li>
<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)">
<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>
Expand All @@ -26,7 +26,7 @@
<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">
<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 @@ -35,6 +35,24 @@
</li>
</ul>
</li>
<li role="presentation" class="dropdown" ng-if="editor.getFormType() === 'search'" title="{{:: ts('Add Search') }}">
<a href class="dropdown-toggle" data-toggle="dropdown" ng-click="editor.getSearchDisplaySelector();">
<i class="crm-i fa-plus"></i>
</a>
<ul class="dropdown-menu">
<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>
Expand Down
6 changes: 3 additions & 3 deletions ext/afform/admin/ang/afGuiEditor/afGuiSearch.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@
</div>
<div ng-if="blockList.length">
<label>{{:: ts('Blocks') }}</label>
<div ui-sortable="$ctrl.editor.getSortableOptions()" ui-sortable-update="buildPaletteLists" ng-model="blockList">
<div ui-sortable="$ctrl.editor.getSortableOptions($ctrl.editor.getSelectedEntityName())" ui-sortable-update="buildPaletteLists" ng-model="blockList">
<div ng-repeat="block in blockList" ng-class="{disabled: blockInUse(block)}">
<div class="af-gui-palette-item">{{:: blockTitles[$index] }}</div>
</div>
</div>
</div>
<div ng-if="calcFieldList.length">
<label>{{:: ts('Calculated Fields') }}</label>
<div ui-sortable="$ctrl.editor.getSortableOptions()" ui-sortable-update="buildPaletteLists" ng-model="calcFieldList">
<div ui-sortable="$ctrl.editor.getSortableOptions($ctrl.editor.getSelectedEntityName())" ui-sortable-update="buildPaletteLists" ng-model="calcFieldList">
<div ng-repeat="field in calcFieldList" ng-class="{disabled: $ctrl.fieldInUse(field.name)}">
<div class="af-gui-palette-item">{{:: field.defn.label }}</div>
</div>
Expand All @@ -68,7 +68,7 @@
<div ng-repeat="fieldGroup in fieldList">
<div ng-if="fieldGroup.fields.length">
<label>{{:: fieldGroup.label }}</label>
<div ui-sortable="{update: buildPaletteLists, items: '&gt; div:not(.disabled)', connectWith: '[ui-sortable]', placeholder: 'af-gui-dropzone'}" ui-sortable-update="$ctrl.editor.onDrop" ng-model="fieldGroup.fields">
<div ui-sortable="$ctrl.editor.getSortableOptions($ctrl.editor.getSelectedEntityName())" ui-sortable-update="buildPaletteLists" ng-model="fieldGroup.fields">
<div ng-repeat="field in fieldGroup.fields" ng-class="{disabled: $ctrl.fieldInUse(field.name)}">
{{:: getField(fieldGroup.entityType, field.name).label }}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@
<li><af-gui-menu-item-border node="$ctrl.node"></af-gui-menu-item-border></li>
<li><af-gui-menu-item-background node="$ctrl.node"></af-gui-menu-item-background></li>
<li role="separator" class="divider"></li>
<li><a href ng-click="$ctrl.deleteThis()"><span class="text-danger"><i class="crm-i fa-trash"></i> {{ !block ? ts('Delete this container') : ts('Delete this block') }}</span></a></li>
<li><a href ng-click="$ctrl.deleteThis()"><span class="text-danger"><i class="crm-i fa-trash"></i> {{ !block ? ts('Remove container') : ts('Remove block') }}</span></a></li>
Loading

0 comments on commit e7e1e02

Please sign in to comment.