->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);
(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';
}
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') {
}
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;
});
}
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) {
this.afform = null;
$scope.saving = false;
$scope.selectedEntityName = null;
+ $scope.searchDisplayListFilter = {};
this.meta = afGui.meta;
var editor = this,
sortableOptions = {};
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,
}
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
}
};
+ // 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 = {},
</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>
<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>
</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="" 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>
<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 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>
<div ng-repeat="fieldGroup in fieldList">
<div ng-if="fieldGroup.fields.length">
<label>{{:: fieldGroup.label }}</label>
- <div ui-sortable="{update: buildPaletteLists, items: '> 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>
<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>
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);
+ }
}
};
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';
}
this.removeElement = function(element) {
afGui.removeRecursive($scope.getSetChildren(), {$$hashKey: element.$$hashKey});
+ ctrl.editor.onRemoveElement();
};
this.removeField = function(fieldName) {
}
// 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 ');
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];
};
<div class="af-gui-bar" ng-if="$ctrl.node['#tag']" ng-click="selectEntity()" >
<div ng-if="!$ctrl.loading" class="form-inline">
<span ng-if="$ctrl.getNodeType($ctrl.node) == 'fieldset'">{{ $ctrl.editor.getEntity($ctrl.entityName).label }}</span>
+ <span ng-if="$ctrl.getNodeType($ctrl.node) == 'searchFieldset'">{{:: ts('Search Display') }}</span>
<span ng-if="block">{{ $ctrl.join ? ts($ctrl.join) + ':' : ts('Block:') }}</span>
<span ng-if="!block">{{ tags[$ctrl.node['#tag']] }}</span>
<select ng-if="block" ng-model="block.directive" ng-change="selectBlockDirective()">
<button type="button" class="btn btn-default btn-xs dropdown-toggle af-gui-add-element-button" data-toggle="dropdown" title="{{:: ts('Configure') }}">
<span><i class="crm-i fa-gear"></i></span>
</button>
- <ul class="dropdown-menu dropdown-menu-right" ng-if="menu.open" ng-include="'~/afGuiEditor/elements/afGuiContainer-menu.html'"></ul>
+ <ul class="dropdown-menu dropdown-menu-right" ng-if="menu.open" ng-include="'~/afGuiEditor/elements/' + ($ctrl.node['af-fieldset'] === '' ? 'afGuiSearchContainer' : 'afGuiContainer') + '-menu.html'"></ul>
</div>
</div>
</div>
<af-gui-text ng-switch-when="text" node="item" delete-this="$ctrl.removeElement(item)" class="af-gui-element af-gui-text" ></af-gui-text>
<af-gui-markup ng-switch-when="markup" node="item" delete-this="$ctrl.removeElement(item)" class="af-gui-markup" ></af-gui-markup>
<af-gui-button ng-switch-when="button" node="item" delete-this="$ctrl.removeElement(item)" class="af-gui-element af-gui-button" ></af-gui-button>
+ <af-gui-container ng-switch-when="searchFieldset" node="item" delete-this="$ctrl.removeElement(item)" style="{{ item.style }}" class="af-gui-container af-gui-fieldset af-gui-container-type-{{ item['#tag'] }}" ng-class="{'af-entity-selected': isSelectedSearchFieldset(item)}" data-entity="{{ getSearchKey(item) }}" ></af-gui-container>
<af-gui-search-display ng-switch-when="searchDisplay" node="item" class="af-gui-element"></af-gui-search-display>
</div>
</div>
<li role="separator" class="divider"></li>
<li>
<a href ng-click="$ctrl.deleteThis()" title="{{:: ts('Remove field from form') }}">
- <span class="text-danger"><i class="crm-i fa-trash"></i> {{:: ts('Delete this field') }}</span>
+ <span class="text-danger"><i class="crm-i fa-trash"></i> {{:: ts('Remove field') }}</span>
</a>
</li>
--- /dev/null
+<li ng-if="tags[$ctrl.node['#tag']]">
+ <div class="af-gui-field-select-in-dropdown form-inline" ng-click="$event.stopPropagation()">
+ {{:: ts('Element:') }}
+ <select class="form-control" ng-model="$ctrl.node['#tag']" title="{{:: ts('Container type') }}">
+ <option ng-repeat="(opt, label) in tags" value="{{ opt }}">{{ label }}</option>
+ </select>
+ </div>
+</li>
+<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> {{:: ts('Remove display') }}</span></a></li>
<div class="af-gui-bar">
<div class="form-inline">
- <span>{{ $ctrl.display.label }}</span>
+ <span>{{:: $ctrl.display['saved_search_id.label'] }}: {{:: $ctrl.display.label }}</span>
<div class="btn-group pull-right" af-gui-menu>
<button type="button" class="btn btn-default btn-xs dropdown-toggle af-gui-add-element-button" data-toggle="dropdown" title="{{:: ts('Configure') }}">
<span><i class="crm-i fa-gear"></i></span>
</div>
</div>
<p class="text-center af-gui-search-display">
- <i class="crm-i fa-3x {{ $ctrl.display['type:icon'] }}"></i>
+ <i class="crm-i fa-3x {{:: $ctrl.display['type:icon'] }}"></i>
</p>