Adds a 'view' link to the searchKit admin listing, which links to the default display
Adds a 'view' dropdown to the compose search screen, with links to the default + all other displays
Updates Afform to work with default search displays
namespace Civi\Api4\Generic;
-use Civi\Api4\Utils\SelectUtil;
-
/**
* Base class for all `Get` api actions.
*
* @package Civi\Api4\Generic
- *
- * @method $this setSelect(array $selects) Set array of fields to be selected (wildcard * allowed)
- * @method array getSelect()
*/
abstract class AbstractGetAction extends AbstractQueryAction {
- /**
- * Fields to return for each $ENTITY. Defaults to all fields `[*]`.
- *
- * Use the * wildcard by itself to select all available fields, or use it to match similarly-named fields.
- * E.g. `is_*` will match fields named is_primary, is_active, etc.
- *
- * Set to `["row_count"]` to return only the number of $ENTITIES found.
- *
- * @var array
- */
- protected $select = [];
+ use Traits\SelectParamTrait;
/**
* Only return the number of found items.
}
}
- /**
- * Adds all standard fields matched by the * wildcard
- *
- * Note: this function only deals with simple wildcard expressions.
- * It ignores those containing special characters like dots or parentheses,
- * they are handled separately in Api4SelectQuery.
- *
- * @throws \API_Exception
- */
- protected function expandSelectClauseWildcards() {
- if (!$this->select) {
- $this->select = ['*'];
- }
- // Get expressions containing wildcards but no dots or parentheses
- $wildFields = array_filter($this->select, function($item) {
- return strpos($item, '*') !== FALSE && strpos($item, '.') === FALSE && strpos($item, '(') === FALSE && strpos($item, ' ') === FALSE;
- });
- if ($wildFields) {
- // Wildcards should not match "Extra" fields
- $standardFields = array_filter(array_map(function($field) {
- return $field['type'] === 'Extra' ? NULL : $field['name'];
- }, $this->entityFields()));
- foreach ($wildFields as $item) {
- $pos = array_search($item, array_values($this->select));
- $matches = SelectUtil::getMatchingFields($item, $standardFields);
- array_splice($this->select, $pos, 1, $matches);
- }
- }
- $this->select = array_unique($this->select);
- }
-
/**
* Helper to parse the WHERE param for getRecords to perform simple pre-filtering.
*
return FALSE;
}
- /**
- * Add one or more fields to be selected (wildcard * allowed)
- * @param string ...$fieldNames
- * @return $this
- */
- public function addSelect(string ...$fieldNames) {
- $this->select = array_merge($this->select, $fieldNames);
- return $this;
- }
-
}
--- /dev/null
+<?php
+
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Api4\Generic\Traits;
+
+use Civi\Api4\Utils\SelectUtil;
+
+/**
+ * @method $this setSelect(array $selects) Set array of fields to be selected (wildcard * allowed)
+ * @method array getSelect()
+ * @package Civi\Api4\Generic
+ */
+trait SelectParamTrait {
+
+ /**
+ * Fields to return for each $ENTITY. Defaults to all fields `[*]`.
+ *
+ * Use the * wildcard by itself to select all available fields, or use it to match similarly-named fields.
+ * E.g. `is_*` will match fields named is_primary, is_active, etc.
+ *
+ * Set to `["row_count"]` to return only the number of $ENTITIES found.
+ *
+ * @var array
+ */
+ protected $select = [];
+
+ /**
+ * Add one or more fields to be selected (wildcard * allowed)
+ * @param string ...$fieldNames
+ * @return $this
+ */
+ public function addSelect(string ...$fieldNames) {
+ $this->select = array_merge($this->select, $fieldNames);
+ return $this;
+ }
+
+ /**
+ * Adds all standard fields matched by the * wildcard
+ *
+ * Note: this function only deals with simple wildcard expressions.
+ * It ignores those containing special characters like dots or parentheses,
+ * they are handled separately in Api4SelectQuery.
+ *
+ * @throws \API_Exception
+ */
+ protected function expandSelectClauseWildcards() {
+ if (!$this->select) {
+ $this->select = ['*'];
+ }
+ // Get expressions containing wildcards but no dots or parentheses
+ $wildFields = array_filter($this->select, function($item) {
+ return strpos($item, '*') !== FALSE && strpos($item, '.') === FALSE && strpos($item, '(') === FALSE && strpos($item, ' ') === FALSE;
+ });
+ if ($wildFields) {
+ // Wildcards should not match "Extra" fields
+ $standardFields = array_filter(array_map(function($field) {
+ return $field['type'] === 'Extra' ? NULL : $field['name'];
+ }, $this->entityFields()));
+ foreach ($wildFields as $item) {
+ $pos = array_search($item, array_values($this->select));
+ $matches = SelectUtil::getMatchingFields($item, $standardFields);
+ array_splice($this->select, $pos, 1, $matches);
+ }
+ }
+ $this->select = array_unique($this->select);
+ }
+
+}
}
}
foreach ($displayTags as $displayTag) {
- $display = \Civi\Api4\SearchDisplay::get(FALSE)
- ->addWhere('name', '=', $displayTag['display-name'])
- ->addWhere('saved_search.name', '=', $displayTag['search-name'])
- ->addSelect('*', 'type:name', 'type:icon', 'saved_search.name', 'saved_search.api_entity', 'saved_search.api_params')
+ if (isset($displayTag['display-name']) && strlen($displayTag['display-name'])) {
+ $displayGet = \Civi\Api4\SearchDisplay::get(FALSE)
+ ->addWhere('name', '=', $displayTag['display-name'])
+ ->addWhere('saved_search_id.name', '=', $displayTag['search-name']);
+ }
+ else {
+ $displayGet = \Civi\Api4\SearchDisplay::getDefault(FALSE)
+ ->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')
->execute()->first();
- $display['calc_fields'] = $this->getCalcFields($display['saved_search.api_entity'], $display['saved_search.api_params']);
+ $display['calc_fields'] = $this->getCalcFields($display['saved_search_id.api_entity'], $display['saved_search_id.api_params']);
$info['search_displays'][] = $display;
if ($newForm) {
$info['definition']['layout'][0]['#children'][] = $displayTag + ['#tag' => $display['type:name']];
}
- $entities[] = $display['saved_search.api_entity'];
- foreach ($display['saved_search.api_params']['join'] ?? [] as $join) {
+ $entities[] = $display['saved_search_id.api_entity'];
+ foreach ($display['saved_search_id.api_params']['join'] ?? [] as $join) {
$entities[] = explode(' AS ', $join[0])[0];
}
}
}
if (ctrl.tab === 'search') {
- crmApi4('SearchDisplay', 'get', {
- select: ['name', 'label', 'type:icon', 'saved_search.name', 'saved_search.label']
- }).then(function(searchDisplays) {
- _.each(searchDisplays, function(searchDisplay) {
- links.push({
- url: '#create/search/' + searchDisplay['saved_search.name'] + '.' + searchDisplay.name,
- label: searchDisplay['saved_search.label'] + ': ' + searchDisplay.label,
- icon: searchDisplay['type:icon']
- });
+ 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']
+ });
+ }
});
- $scope.types.search.options = _.sortBy(links, 'Label');
+ $scope.types.search.options = links;
});
}
};
<td>{{:: afform.placement.join(', ') }}</td>
<td class="text-right">
<a ng-if="afform.type !== 'system'" href="#/edit/{{:: afform.name }}" class="btn btn-xs btn-primary">{{:: ts('Edit') }}</a>
- <a ng-if="afform.type !== 'system'" href="#/clone/{{:: afform.name }}" class="btn btn-xs btn-primary">{{:: ts('Clone') }}</a>
+ <a ng-if="afform.type !== 'system'" href="#/clone/{{:: afform.name }}" class="btn btn-xs btn-secondary">{{:: ts('Clone') }}</a>
<a href ng-if="afform.has_local" class="btn btn-xs btn-danger" crm-confirm="{type: afform.has_base ? 'revert' : 'delete', obj: afform}" on-yes="$ctrl.revert(afform)">
{{ afform.has_base ? ts('Revert') : ts('Delete') }}
</a>
);
}
_.each(data.search_displays, function(display) {
- CRM.afGuiEditor.searchDisplays[display['saved_search.name'] + '.' + display.name] = display;
+ CRM.afGuiEditor.searchDisplays[display['saved_search_id.name'] + (display.name ? '.' + display.name : '')] = display;
});
},
return fields[fieldName] || fields[fieldName.substr(fieldName.indexOf('.') + 1)];
},
+ getSearchDisplay: function(searchName, displayName) {
+ return CRM.afGuiEditor.searchDisplays[searchName + (displayName ? '.' + displayName : '')];
+ },
+
// 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) {
};
function getSearchFilterOptions() {
- var searchDisplay = editor.meta.searchDisplays[editor.searchDisplay['search-name'] + '.' + editor.searchDisplay['display-name']],
+ var searchDisplay = afGui.getSearchDisplay(editor.searchDisplay['search-name'], editor.searchDisplay['display-name']),
entityCount = {},
options = [];
- addFields(searchDisplay['saved_search.api_entity'], '');
+ addFields(searchDisplay['saved_search_id.api_entity'], '');
- _.each(searchDisplay['saved_search.api_params'].join, function(join) {
+ _.each(searchDisplay['saved_search_id.api_params'].join, function(join) {
var joinInfo = join[0].split(' AS ');
addFields(joinInfo[0], joinInfo[1] + '.');
});
function buildFieldList(search) {
$scope.fieldList.length = 0;
- var entity = afGui.getEntity(ctrl.display['saved_search.api_entity']),
+ var entity = afGui.getEntity(ctrl.display['saved_search_id.api_entity']),
entityCount = {};
entityCount[entity.entity] = 1;
$scope.fieldList.push({
fields: filterFields(entity.fields)
});
- _.each(ctrl.display['saved_search.api_params'].join, function(join) {
+ _.each(ctrl.display['saved_search_id.api_params'].join, function(join) {
var joinInfo = join[0].split(' AS '),
entity = afGui.getEntity(joinInfo[0]),
alias = joinInfo[1];
prefix = _.includes(fieldName, '.') ? fieldName.split('.')[0] : null;
_.each(afGui.meta.searchDisplays, function(searchDisplay) {
if (prefix) {
- _.each(searchDisplay['saved_search.api_params'].join, function(join) {
+ _.each(searchDisplay['saved_search_id.api_params'].join, function(join) {
var joinInfo = join[0].split(' AS ');
if (prefix === joinInfo[1]) {
entityType = joinInfo[0];
}
});
}
- if (!entityType && fieldName && afGui.getField(searchDisplay['saved_search.api_entity'], fieldName)) {
- entityType = searchDisplay['saved_search.api_entity'];
+ 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.api_entity')[0];
+ return entityType || _.map(afGui.meta.searchDisplays, 'saved_search_id.api_entity')[0];
};
}
ctrl = this;
this.$onInit = function() {
- ctrl.display = afGui.meta.searchDisplays[ctrl.node['search-name'] + '.' + ctrl.node['display-name']];
+ ctrl.display = afGui.getSearchDisplay(ctrl.node['search-name'], ctrl.node['display-name']);
ctrl.editUrl = CRM.url('civicrm/admin/search#/edit/' + ctrl.display.saved_search_id);
};
foreach ($tags[0] ?? [] as $tag) {
$searchName = $displayName = [];
preg_match('/search-name\\s*=\\s*[\'"]([^\'"]+)[\'"]/', $tag, $searchName);
+ // Note: display name will be blank when using the default (autogenerated) display
preg_match('/display-name\\s*=\\s*[\'"]([^\'"]+)[\'"]/', $tag, $displayName);
- if (!empty($searchName[1]) && !empty($displayName[1])) {
- $searchDisplays[] = $searchName[1] . '.' . $displayName[1];
+ if (!empty($searchName[1])) {
+ $searchDisplays[] = $searchName[1] . (empty($displayName[1]) ? '' : '.' . $displayName[1]);
}
}
return $searchDisplays;
}
elseif (is_null($this->display)) {
$this->display = SearchDisplay::getDefault(FALSE)
+ ->addSelect('*', 'type:name')
->setSavedSearch($this->savedSearch)
->execute()->first();
- $this->display['type:name'] = 'crm-search-display-table';
}
// Displays with acl_bypass must be embedded on an afform which the user has access to
if (
class GetDefault extends \Civi\Api4\Generic\AbstractAction {
use SavedSearchInspectorTrait;
+ use \Civi\Api4\Generic\Traits\ArrayQueryActionTrait;
+ use \Civi\Api4\Generic\Traits\SelectParamTrait;
/**
* Either the name of the savedSearch or an array containing the savedSearch definition (for preview mode)
throw new UnauthorizedException('Access denied');
}
$this->loadSavedSearch();
+ $this->expandSelectClauseWildcards();
// Use label from saved search
$label = $this->savedSearch['label'] ?? '';
// Fall back on entity title as label
$label = CoreUtil::getInfoItem($this->savedSearch['api_entity'], 'title_plural');
}
$display = [
- 'saved_search_id' => $this->savedSearch['id'] ?? NULL,
+ 'id' => NULL,
'name' => NULL,
+ 'saved_search_id' => $this->savedSearch['id'] ?? NULL,
'label' => $label,
'type' => 'table',
+ 'type:label' => E::ts('Table'),
+ 'type:name' => 'crm-search-display-table',
+ 'type:icon' => 'fa-table',
'acl_bypass' => FALSE,
'settings' => [
- 'button' => E::ts('Search'),
'actions' => TRUE,
'limit' => \Civi::settings()->get('default_pager_size'),
'classes' => ['table', 'table-striped'],
'columns' => [],
],
];
+ // Allow implicit-join-style selection of saved search fields
+ foreach ($this->savedSearch as $key => $val) {
+ $display['saved_search_id.' . $key] = $val;
+ }
foreach ($this->getSelectClause() as $key => $clause) {
$display['settings']['columns'][] = $this->configureColumn($clause, $key);
}
'alignment' => 'text-right',
'links' => $this->getLinksMenu(),
];
- $result[] = $display;
+ $result->exchangeArray($this->selectArray([$display]));
}
/**
this.afformEnabled = CRM.crmSearchAdmin.afformEnabled;
this.afformAdminEnabled = CRM.crmSearchAdmin.afformAdminEnabled;
this.displayTypes = _.indexBy(CRM.crmSearchAdmin.displayTypes, 'id');
+ this.searchDisplayPath = CRM.url('civicrm/search');
+ this.afformPath = CRM.url('civicrm/admin/afform');
$scope.controls = {tab: 'compose', joinType: 'LEFT'};
$scope.joinTypes = [
}
if (display.trashed && afformLoad) {
afformLoad.then(function() {
- if (ctrl.afforms[display.name]) {
- var msg = ctrl.afforms[display.name].length === 1 ?
- ts('Form "%1" will be deleted if the embedded display "%2" is deleted.', {1: ctrl.afforms[display.name][0].title, 2: display.label}) :
- ts('%1 forms will be deleted if the embedded display "%2" is deleted.', {1: ctrl.afforms[display.name].length, 2: display.label});
+ var displayForms = _.filter(ctrl.afforms, function(form) {
+ return _.includes(form.displays, ctrl.savedSearch.name + '.' + display.name);
+ });
+ if (displayForms.length) {
+ var msg = displayForms.length === 1 ?
+ ts('Form "%1" will be deleted if the embedded display "%2" is deleted.', {1: displayForms[0].title, 2: display.label}) :
+ ts('%1 forms will be deleted if the embedded display "%2" is deleted.', {1: displayForms.length, 2: display.label});
CRM.alert(msg, ts('Display embedded'), 'alert');
}
});
};
function loadAfforms() {
+ ctrl.afforms = null;
if (ctrl.afformEnabled && ctrl.savedSearch.id) {
var findDisplays = _.transform(ctrl.savedSearch.displays, function(findDisplays, display) {
if (display.id && display.name) {
select: ['name', 'title', 'search_displays'],
where: [['OR', findDisplays]]
}).then(function(afforms) {
- ctrl.afforms = {};
+ ctrl.afforms = [];
_.each(afforms, function(afform) {
- _.each(_.uniq(afform.search_displays), function(searchNameDisplayName) {
- var displayName = searchNameDisplayName.split('.')[1] || '';
- ctrl.afforms[displayName] = ctrl.afforms[displayName] || [];
- ctrl.afforms[displayName].push({
- title: afform.title,
- link: ctrl.afformAdminEnabled ? CRM.url('civicrm/admin/afform#/edit/' + afform.name) : '',
- });
+ ctrl.afforms.push({
+ title: afform.title,
+ displays: afform.search_displays,
+ link: ctrl.afformAdminEnabled ? CRM.url('civicrm/admin/afform#/edit/' + afform.name) : '',
});
});
+ ctrl.afformCount = ctrl.afforms.length;
});
}
}
- // Creating an Afform opens a new tab, so when switching back to this tab, re-check for Afforms
+ // Creating an Afform opens a new tab, so when switching back after > 10 sec, re-check for Afforms
$(window).on('focus', _.debounce(function() {
$scope.$apply(loadAfforms);
}, 10000, {leading: true, trailing: false}));
<div class="crm-flex-4 form-inline">
<label for="crm-search-main-entity">{{:: ts('Search for') }}</label>
<input id="crm-search-main-entity" class="form-control huge collapsible-optgroups" ng-model="$ctrl.savedSearch.api_entity" crm-ui-select="::{allowClear: false, data: mainEntitySelect}" ng-disabled="$ctrl.savedSearch.id" />
- <div class="btn-group btn-group-md pull-right">
- <button type="button" class="btn" ng-class="{'btn-primary': status === 'unsaved', 'btn-warning': status === 'saving', 'btn-success': status === 'saved'}" ng-disabled="status !== 'unsaved'" ng-click="$ctrl.save()">
- <i class="crm-i" ng-class="{'fa-check': status !== 'saving', 'fa-spin fa-spinner': status === 'saving'}"></i>
- <span ng-if="status === 'saved'">{{:: ts('Saved') }}</span>
- <span ng-if="status === 'unsaved'">{{:: ts('Save') }}</span>
- <span ng-if="status === 'saving'">{{:: ts('Saving...') }}</span>
- </button>
+
+ <div class="form-group pull-right">
+
+ <div class="btn-group" ng-if="$ctrl.afformEnabled && $ctrl.savedSearch.id">
+ <button type="button" ng-click="$ctrl.openAfformMenu = true;" class="btn dropdown-toggle btn-primary-outline" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+ <i class="crm-i fa-list-alt"></i>
+ {{ ($ctrl.afformCount !== undefined) ? ($ctrl.afformCount === 1 ? ts('1 Form') : ts('%1 Forms', {1: $ctrl.afformCount})) : ts('Forms...') }}
+ <span class="caret"></span>
+ </button>
+ <ul class="dropdown-menu dropdown-menu-right" ng-if=":: $ctrl.openAfformMenu">
+ <li ng-if=":: $ctrl.afformAdminEnabled">
+ <a target="_blank" href="{{:: $ctrl.afformPath + '#/create/search/' + $ctrl.savedSearch.name }}">
+ <i class="fa fa-plus"></i> {{:: ts('Create form for search results table') }}
+ </a>
+ </li>
+ <li ng-repeat="display in $ctrl.savedSearch.displays" ng-if="$ctrl.afformAdminEnabled && display.id">
+ <a target="_blank" href="{{:: $ctrl.afformPath + '#/create/search/' + $ctrl.savedSearch.name + '.' + display.name }}">
+ <i class="fa fa-plus"></i> {{:: ts('Create form for %1', {1: display.label}) }}
+ </a>
+ </li>
+ <li class="divider" role="separator" ng-if="$ctrl.afformAdminEnabled && $ctrl.afforms.length"></li>
+ <li ng-if="!$ctrl.afforms" class="disabled">
+ <a href>
+ <i class="crm-i fa-spinner fa-spin"></i>
+ </a>
+ </li>
+ <li ng-repeat="afform in $ctrl.afforms" title="{{:: $ctrl.afformAdminEnabled ? ts('Edit form') : '' }}">
+ <a target="_blank" ng-href="{{:: afform.link }}">
+ <i class="crm-i {{:: $ctrl.afformAdminEnabled ? 'fa-pencil-square-o' : 'fa-list-alt' }}"></i>
+ {{:: afform.title }}
+ </a>
+ </li>
+ </ul>
+ </div>
+
+ <div class="btn-group" ng-if="$ctrl.savedSearch.id">
+ <a ng-href="{{ $ctrl.searchDisplayPath + '#/display/' + $ctrl.savedSearch.name }}" target="_blank" class="btn btn-primary-outline" title="{{:: ts('View search results table') }}">
+ <i class="crm-i fa-external-link"></i>
+ {{:: ts('View') }}
+ </a>
+ <button type="button" ng-click="$ctrl.openDisplayMenu = true;" class="btn btn-primary-outline dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+ <span class="caret"></span>
+ </button>
+ <ul class="dropdown-menu dropdown-menu-right" ng-if=":: $ctrl.openDisplayMenu">
+ <li title="{{:: ts('View search results table') }}">
+ <a ng-href="{{ $ctrl.searchDisplayPath + '#/display/' + $ctrl.savedSearch.name }}" target="_blank">
+ <i class="crm-i fa-table"></i>
+ {{:: ts('Search results table') }}
+ </a>
+ </li>
+ <li ng-repeat="display in $ctrl.savedSearch.displays" ng-if="display.id" ng-class="{disabled: display.acl_bypass}" title="{{:: display.acl_bypass ? ts('Display has permissions disabled') : ts('View display') }}">
+ <a ng-href="{{ display.acl_bypass ? '' : $ctrl.searchDisplayPath + '#/display/' + $ctrl.savedSearch.name + '/' + display.name }}" target="_blank">
+ <i class="crm-i {{ display.acl_bypass ? 'fa-unlock' : $ctrl.displayTypes[display.type].icon }}"></i>
+ {{ display.label }}
+ </a>
+ </li>
+ </ul>
+ </div>
+
+ <div class="btn-group">
+ <button type="button" class="btn" ng-class="{'btn-primary': status === 'unsaved', 'btn-warning': status === 'saving', 'btn-success': status === 'saved'}" ng-disabled="status !== 'unsaved'" ng-click="$ctrl.save()">
+ <i class="crm-i" ng-class="{'fa-check': status !== 'saving', 'fa-spin fa-spinner': status === 'saving'}"></i>
+ <span ng-if="status === 'saved'">{{:: ts('Saved') }}</span>
+ <span ng-if="status === 'unsaved'">{{:: ts('Save') }}</span>
+ <span ng-if="status === 'saving'">{{:: ts('Saving...') }}</span>
+ </button>
+ </div>
+
</div>
+
</div>
</div>
<div class="crm-flex-box">
ctrl = this,
afforms;
- this.afformPath = CRM.url('civicrm/admin/afform');
this.isSuperAdmin = CRM.checkPerm('all CiviCRM permissions and ACLs');
this.aclBypassHelp = ts('Only users with "all CiviCRM permissions and ACLs" can disable permission checks.');
}
};
- // @return {Array}
- this.getAfforms = function() {
- if (ctrl.display.name && ctrl.crmSearchAdmin.afforms) {
- if (!afforms || (ctrl.crmSearchAdmin.afforms[ctrl.display.name] && afforms !== ctrl.crmSearchAdmin.afforms[ctrl.display.name])) {
- afforms = ctrl.crmSearchAdmin.afforms[ctrl.display.name] || [];
- }
- }
- return afforms;
- };
-
$scope.$watch('$ctrl.display.settings', function() {
ctrl.stale = true;
}, true);
<i class="crm-i fa-unlock"></i>
{{:: ts('Anyone who can view this display will be able to see all results, regardless of their permission level.') }}
</div>
- <div class="btn-group pull-right" ng-if="$ctrl.crmSearchAdmin.afformEnabled && $ctrl.display.name">
- <button type="button" ng-click="$ctrl.openAfformMenu = true;" class="btn dropdown-toggle btn-primary-outline" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
- <i class="crm-i fa-list-alt"></i>
- {{ $ctrl.getAfforms() ? ($ctrl.getAfforms().length === 1 ? ts('1 Form') : ts('%1 Forms', {1: $ctrl.getAfforms().length})) : ts('Forms...') }}
- <span class="caret"></span>
- </button>
- <ul class="dropdown-menu" ng-if=":: $ctrl.openAfformMenu">
- <li ng-if=":: $ctrl.crmSearchAdmin.afformAdminEnabled">
- <a target="_blank" href="{{:: $ctrl.afformPath + '#/create/search/' + $ctrl.crmSearchAdmin.savedSearch.name + '.' + $ctrl.display.name }}">
- <i class="fa fa-plus"></i> {{:: ts('Create form for this display') }}
- </a>
- </li>
- <li class="divider" role="separator" ng-if=":: $ctrl.crmSearchAdmin.afformAdminEnabled"></li>
- <li ng-if="!$ctrl.getAfforms()" class="disabled">
- <a href>
- <i class="crm-i fa-spinner fa-spin"></i>
- </a>
- </li>
- <li ng-repeat="afform in $ctrl.getAfforms()" title="{{:: $ctrl.crmSearchAdmin.afformAdminEnabled ? ts('Edit form') : '' }}">
- <a target="_blank" ng-href="{{:: afform.link }}">
- <i class="crm-i {{:: $ctrl.crmSearchAdmin.afformAdminEnabled ? 'fa-pencil-square-o' : 'fa-list-alt' }}"></i>
- {{:: afform.title }}
- </a>
- </li>
- </ul>
- </div>
</div>
</fieldset>
function buildSettings() {
ctrl.apiEntity = ctrl.search.api_entity;
ctrl.settings = _.cloneDeep(CRM.crmSearchAdmin.defaultDisplay.settings);
+ ctrl.settings.button = ts('Search');
+ // The default-display settings contain just one column (the last one, with the links menu)
ctrl.settings.columns = _.transform(ctrl.search.api_params.select, function(columns, fieldExpr) {
columns.push(searchMeta.fieldToColumn(fieldExpr, {label: true, sortable: true}));
}).concat(ctrl.settings.columns);
-<div class="btn-group" ng-if=":: row.data.display_name">
+<div class="btn-group">
<button type="button" ng-click="$ctrl.loadAfforms(); row.openAfformMenu = true;" class="btn btn-xs dropdown-toggle btn-primary-outline" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ $ctrl.afforms ? (row.afform_count === 1 ? ts('1 Form') : ts('%1 Forms', {1: row.afform_count})) : ts('Forms...') }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" ng-if=":: row.openAfformMenu">
+ <li ng-if="::$ctrl.afformAdminEnabled">
+ <a target="_blank" href="{{:: $ctrl.afformPath + '#/create/search/' + row.data.name }}">
+ <i class="fa fa-plus"></i> {{:: ts('Create form for search results table') }}
+ </a>
+ </li>
<li ng-repeat="display_name in row.data.display_name" ng-if="::$ctrl.afformAdminEnabled">
<a target="_blank" href="{{:: $ctrl.afformPath + '#/create/search/' + row.data.name + '.' + display_name }}">
<i class="fa fa-plus"></i> {{:: ts('Create form for %1', {1: row.data.display_label[$index]}) }}
-<a class="btn btn-xs btn-default" href="#/edit/{{:: row.data.id }}" ng-if="row.permissionToEdit">
+<a class="btn btn-xs btn-default" title="{{:: ts('View search results table') }}" ng-href="{{:: $ctrl.searchDisplayPath + '#/display/' + row.data.name }}" target="_blank">
+ {{:: ts('View') }}
+</a>
+<a class="btn btn-xs btn-primary" href="#/edit/{{:: row.data.id }}" ng-if="row.permissionToEdit">
{{:: ts('Edit') }}
</a>
-<a class="btn btn-xs btn-default" href="#/create/{{:: row.data.api_entity + '?params=' + $ctrl.encode(row.data.api_params) }}">
+<a class="btn btn-xs btn-secondary" href="#/create/{{:: row.data.api_entity + '?params=' + $ctrl.encode(row.data.api_params) }}">
{{:: ts('Clone') }}
</a>
<a href class="btn btn-xs btn-danger" ng-click="$ctrl.confirmDelete(row)">
<ul class="dropdown-menu" ng-if=":: row.openDisplayMenu">
<li ng-repeat="display_name in row.data.display_name" ng-class="{disabled: row.data.display_acl_bypass[$index]}" title="{{:: row.data.display_acl_bypass[$index] ? ts('Display has permissions disabled') : ts('View display') }}">
<a ng-href="{{:: row.data.display_acl_bypass[$index] ? '' : $ctrl.searchDisplayPath + '#/display/' + row.data.name + '/' + display_name }}" target="_blank">
- <i class="fa {{:: row.display_icon.rw[$index] }}"></i>
+ <i class="crm-i {{:: row.data.display_icon[$index] }}"></i>
{{:: row.data.display_label[$index] }}
</a>
</li>
.config(function($routeProvider) {
// Load & render a SearchDisplay
- $routeProvider.when('/display/:savedSearchName/:displayName', {
+ $routeProvider.when('/display/:savedSearchName/:displayName?', {
controller: 'crmSearchPageDisplay',
// Dynamic template generates the directive for each display type
template: '<h1 crm-page-title>{{:: $ctrl.display.label }}</h1>\n' +
'<div ng-include="\'~/crmSearchPage/displayType/\' + $ctrl.display.type + \'.html\'" id="bootstrap-theme"></div>',
resolve: {
// Load saved search display
- display: function($route, crmApi4) {
+ info: function($route, crmApi4) {
var params = $route.current.params;
- return crmApi4('SearchDisplay', 'get', {
- where: [['name', '=', params.displayName], ['saved_search_id.name', '=', params.savedSearchName]],
- select: ['*', 'saved_search_id.api_entity', 'saved_search_id.name']
- }, 0);
+ var apiCalls = {
+ search: ['SavedSearch', 'get', {
+ select: ['name', 'api_entity'],
+ where: [['name', '=', params.savedSearchName]]
+ }, 0]
+ };
+ if (params.displayName) {
+ apiCalls.display = ['SearchDisplay', 'get', {
+ where: [['name', '=', params.displayName], ['saved_search_id.name', '=', params.savedSearchName]],
+ }, 0];
+ } else {
+ apiCalls.display = ['SearchDisplay', 'getDefault', {
+ savedSearch: params.savedSearchName,
+ }, 0];
+ }
+ return crmApi4(apiCalls);
}
}
});
})
// Controller for displaying a search
- .controller('crmSearchPageDisplay', function($scope, $location, display) {
+ .controller('crmSearchPageDisplay', function($scope, $location, info) {
var ctrl = $scope.$ctrl = this;
- this.display = display;
- this.searchName = display['saved_search_id.name'];
- this.apiEntity = display['saved_search_id.api_entity'];
+ this.display = info.display;
+ this.searchName = info.search.name;
+ this.apiEntity = info.search.api_entity;
$scope.$watch(function() {return $location.search();}, function(params) {
ctrl.filters = params;
--- /dev/null
+<?php
+namespace api\v4\SearchDisplay;
+
+use Civi\Api4\SearchDisplay;
+use Civi\Test\HeadlessInterface;
+use Civi\Test\TransactionalInterface;
+
+/**
+ * @group headless
+ */
+class SearchDisplayTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, TransactionalInterface {
+
+ public function setUpHeadless() {
+ return \Civi\Test::headless()
+ ->installMe(__DIR__)
+ ->apply();
+ }
+
+ public function testGetDefault() {
+ $params = [
+ 'api_entity' => 'Contact',
+ 'api_params' => [
+ 'version' => 4,
+ 'select' => ['first_name', 'last_name', 'contact_sub_type:label', 'gender_id'],
+ 'where' => [],
+ ],
+ ];
+ $display = SearchDisplay::getDefault(FALSE)
+ ->setSavedSearch($params)
+ ->addSelect('*', 'saved_search_id.api_entity', 'type:name')
+ ->execute()->single();
+
+ $this->assertCount(5, $display['settings']['columns']);
+ $this->assertEquals('Contacts', $display['label']);
+ $this->assertEquals('crm-search-display-table', $display['type:name']);
+ $this->assertEquals('Contact', $display['saved_search_id.api_entity']);
+ }
+
+}