$savedSearch = self::retrieve(['id' => $id]);
// APIv4 search
if (!empty($savedSearch->api_entity)) {
- return CRM_Utils_System::url('civicrm/search', NULL, FALSE, "/edit/$id");
+ return CRM_Utils_System::url('civicrm/admin/search', NULL, FALSE, "/edit/$id");
}
// Classic search builder
if (!empty($savedSearch->mapping_id)) {
public function run() {
$breadCrumb = [
'title' => ts('Search Kit'),
- 'url' => CRM_Utils_System::url('civicrm/search', NULL, FALSE, '/list'),
+ 'url' => CRM_Utils_System::url('civicrm/admin/search', NULL, FALSE, '/list'),
];
CRM_Utils_System::appendBreadCrumb([$breadCrumb]);
// Load angular module
$loader = new Civi\Angular\AngularLoader();
- $loader->setModules(['searchAdmin']);
- $loader->setPageName('civicrm/search');
+ $loader->setPageName('civicrm/admin/search');
$loader->useApp([
'defaultRoute' => '/list',
]);
--- /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 |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ * Angular base page for search admin
+ */
+class CRM_Search_Page_Search extends CRM_Core_Page {
+
+ public function run() {
+
+ Civi::resources()->addBundle('bootstrap3');
+
+ // Load angular module
+ $loader = new Civi\Angular\AngularLoader();
+ $loader->setPageName('civicrm/search');
+ $loader->useApp();
+ $loader->load();
+
+ parent::run();
+ }
+
+}
->addValue('parent_id:name', 'Search')
->addValue('label', E::ts('Search Kit'))
->addValue('name', 'search_kit')
- ->addValue('url', 'civicrm/search')
+ ->addValue('url', 'civicrm/admin/search')
->addValue('icon', 'crm-i fa-search-plus')
->addValue('has_separator', 2)
->addValue('weight', 99)
'title' => ts('Update %1'),
'icon' => 'fa-save',
'entities' => [],
- 'uiDialog' => ['templateUrl' => '~/searchActions/crmSearchActionUpdate.html'],
+ 'uiDialog' => ['templateUrl' => '~/crmSearchActions/crmSearchActionUpdate.html'],
],
'delete' => [
'title' => ts('Delete %1'),
'icon' => 'fa-trash',
'entities' => [],
- 'uiDialog' => ['templateUrl' => '~/searchActions/crmSearchActionDelete.html'],
+ 'uiDialog' => ['templateUrl' => '~/crmSearchActions/crmSearchActionDelete.html'],
],
];
// Autoloader data for search actions.
return [
'js' => [
- 'ang/searchActions.module.js',
- 'ang/searchActions/*.js',
- 'ang/searchActions/*/*.js',
+ 'ang/crmSearchActions.module.js',
+ 'ang/crmSearchActions/*.js',
+ 'ang/crmSearchActions/*/*.js',
],
'partials' => [
- 'ang/searchActions',
+ 'ang/crmSearchActions',
],
'basePages' => [],
'requires' => ['crmUi', 'crmUtil', 'dialogService', 'api4'],
"use strict";
// Declare module
- angular.module('searchActions', CRM.angRequires('searchActions'));
+ angular.module('crmSearchActions', CRM.angRequires('crmSearchActions'));
})(angular, CRM.$, CRM._);
(function(angular, $, _) {
"use strict";
- angular.module('searchActions').controller('SaveSmartGroup', function ($scope, $element, $timeout, crmApi4, dialogService, searchMeta) {
+ angular.module('crmSearchActions').controller('SaveSmartGroup', function ($scope, $element, $timeout, crmApi4, dialogService, searchMeta) {
var ts = $scope.ts = CRM.ts(),
model = $scope.model;
$scope.groupEntityRefParams = {
$scope.perm = {
administerReservedGroups: CRM.checkPerm('administer reserved groups')
};
- $scope.groupOptions = CRM.searchActions.groupOptions;
+ $scope.groupOptions = CRM.crmSearchActions.groupOptions;
$element.on('change', '#api-save-search-select-group', function() {
if ($(this).val()) {
$scope.$apply(function() {
(function(angular, $, _) {
"use strict";
- angular.module('searchActions').controller('crmSearchActionDelete', function($scope, crmApi4, dialogService, searchMeta) {
+ angular.module('crmSearchActions').controller('crmSearchActionDelete', function($scope, crmApi4, dialogService, searchMeta) {
var ts = $scope.ts = CRM.ts(),
model = $scope.model,
ctrl = $scope.$ctrl = this;
(function(angular, $, _) {
"use strict";
- angular.module('searchActions').controller('crmSearchActionUpdate', function ($scope, $timeout, crmApi4, dialogService, searchMeta) {
+ angular.module('crmSearchActions').controller('crmSearchActionUpdate', function ($scope, $timeout, crmApi4, dialogService, searchMeta) {
var ts = $scope.ts = CRM.ts(),
model = $scope.model,
ctrl = $scope.$ctrl = this;
(function(angular, $, _) {
"use strict";
- angular.module('searchActions').component('crmSearchActions', {
+ angular.module('crmSearchActions').component('crmSearchActions', {
bindings: {
entity: '<',
refresh: '&',
ids: '<'
},
- templateUrl: '~/searchActions/crmSearchActions.html',
+ templateUrl: '~/crmSearchActions/crmSearchActions.html',
controller: function($scope, crmApi4, dialogService, searchMeta) {
var ts = $scope.ts = CRM.ts(),
ctrl = this,
where: [['name', 'IN', ['update', 'delete']]],
}, ['name']).then(function(allowed) {
_.each(allowed, function(action) {
- CRM.searchActions.tasks[action].entities.push(ctrl.entity);
+ CRM.crmSearchActions.tasks[action].entities.push(ctrl.entity);
});
- var actions = _.transform(_.cloneDeep(CRM.searchActions.tasks), function(actions, action) {
+ var actions = _.transform(_.cloneDeep(CRM.crmSearchActions.tasks), function(actions, action) {
if (_.includes(action.entities, ctrl.entity)) {
action.title = action.title.replace('%1', entityTitle);
actions.push(action);
(function(angular, $, _) {
"use strict";
- angular.module('searchActions').directive('saveSmartGroup', function() {
+ angular.module('crmSearchActions').directive('saveSmartGroup', function() {
return {
bindToController: {
load: '<',
entity: '<',
params: '<'
},
+ restrict: 'A',
controller: function ($scope, $element, dialogService) {
var ts = $scope.ts = CRM.ts(),
ctrl = this;
autoOpen: false,
title: ts('Save smart group')
});
- dialogService.open('saveSearchDialog', '~/searchActions/saveSmartGroup.html', model, options);
+ dialogService.open('saveSearchDialog', '~/crmSearchActions/saveSmartGroup.html', model, options);
};
}
};
--- /dev/null
+<?php
+// Autoloader data for SearchDisplay module.
+return [
+ 'js' => [
+ 'ang/crmSearchDisplay.module.js',
+ 'ang/crmSearchDisplay/*.js',
+ 'ang/crmSearchDisplay/*/*.js',
+ ],
+ 'partials' => [
+ 'ang/crmSearchDisplay',
+ ],
+ 'basePages' => [],
+ 'requires' => ['crmUi', 'api4', 'crmSearchActions', 'ui.bootstrap'],
+ 'exports' => [
+ 'crm-search-display-table' => 'E',
+ ],
+];
--- /dev/null
+(function(angular, $, _) {
+ "use strict";
+
+ // Declare module
+ angular.module('crmSearchDisplay', CRM.angRequires('crmSearchDisplay'));
+
+})(angular, CRM.$, CRM._);
--- /dev/null
+(function(angular, $, _) {
+ "use strict";
+
+ angular.module('crmSearchDisplay').component('crmSearchDisplayTable', {
+ bindings: {
+ apiEntity: '<',
+ apiParams: '<',
+ settings: '<'
+ },
+ templateUrl: '~/crmSearchDisplay/crmSearchDisplayTable.html',
+ controller: function($scope, crmApi4) {
+ var ts = $scope.ts = CRM.ts(),
+ ctrl = this;
+
+ this.page = 1;
+
+ this.$onInit = function() {
+ this.orderBy = this.apiParams.orderBy || {};
+ this.limit = parseInt(ctrl.settings.limit || 0, 10);
+ _.each(ctrl.settings.columns, function(col, num) {
+ var index = ctrl.apiParams.select.indexOf(col.expr);
+ if (_.includes(col.expr, '(') && !_.includes(col.expr, ' AS ')) {
+ col.expr += ' AS column_' + num;
+ ctrl.apiParams.select[index] += ' AS column_' + num;
+ }
+ col.key = _.last(col.expr.split(' AS '));
+ });
+ getResults();
+ };
+
+ function getResults() {
+ var params = _.merge(_.cloneDeep(ctrl.apiParams), {limit: ctrl.limit, offset: (ctrl.page - 1) * ctrl.limit, orderBy: ctrl.orderBy});
+ if (ctrl.settings.pager) {
+ params.select.push('row_count');
+ }
+ crmApi4(ctrl.apiEntity, 'get', params).then(function(results) {
+ ctrl.results = results;
+ ctrl.rowCount = results.count;
+ });
+ }
+
+ this.changePage = function() {
+ getResults();
+ };
+
+ /**
+ * Returns crm-i icon class for a sortable column
+ * @param col
+ * @returns {string}
+ */
+ $scope.getOrderBy = function(col) {
+ var dir = ctrl.orderBy && ctrl.orderBy[col.key];
+ if (dir) {
+ return 'fa-sort-' + dir.toLowerCase();
+ }
+ return 'fa-sort disabled';
+ };
+
+ /**
+ * Called when clicking on a column header
+ * @param col
+ * @param $event
+ */
+ $scope.setOrderBy = function(col, $event) {
+ var dir = $scope.getOrderBy(col) === 'fa-sort-asc' ? 'DESC' : 'ASC';
+ if (!$event.shiftKey) {
+ ctrl.orderBy = {};
+ }
+ ctrl.orderBy[col.key] = dir;
+ getResults();
+ };
+
+ $scope.formatResult = function(row, col) {
+ var value = row[col.key];
+ return formatFieldValue(col, value);
+ };
+
+ function formatFieldValue(col, value) {
+ var type = col.dataType;
+ if (_.isArray(value)) {
+ return _.map(value, function(val) {
+ return formatFieldValue(col, val);
+ }).join(', ');
+ }
+ if (value && (type === 'Date' || type === 'Timestamp') && /^\d{4}-\d{2}-\d{2}/.test(value)) {
+ return CRM.utils.formatDate(value, null, type === 'Timestamp');
+ }
+ else if (type === 'Boolean' && typeof value === 'boolean') {
+ return value ? ts('Yes') : ts('No');
+ }
+ else if (type === 'Money' && typeof value === 'number') {
+ return CRM.formatMoney(value);
+ }
+ return value;
+ }
+
+ }
+ });
+
+})(angular, CRM.$, CRM._);
--- /dev/null
+<table>
+ <thead>
+ <tr>
+ <th class="crm-search-result-select" ng-if="$ctrl.settings.actions">
+ <input type="checkbox" ng-checked="$ctrl.allRowsSelected" ng-click="selectAllRows()" >
+ </th>
+ <th ng-repeat="col in $ctrl.settings.columns" ng-click="setOrderBy(col, $event)" title="{{:: ts('Click to sort results (shift-click to sort by multiple).') }}">
+ <i class="crm-i {{ getOrderBy(col) }}"></i>
+ <span>{{ col.label }}</span>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="row in $ctrl.results">
+ <td ng-if="$ctrl.settings.actions">
+ <input type="checkbox" ng-checked="isRowSelected(row)" ng-click="selectRow(row)" ng-disabled="!(loading === false && !loadingAllRows && row.id)">
+ </td>
+ <td ng-repeat="col in $ctrl.settings.columns">
+ {{ formatResult(row, col) }}
+ </td>
+ <td></td>
+ </tr>
+ </tbody>
+</table>
+<div class="text-center" ng-if="$ctrl.rowCount">
+ <ul uib-pagination
+ class="pagination"
+ boundary-links="true"
+ total-items="$ctrl.rowCount"
+ ng-model="$ctrl.page"
+ ng-change="$ctrl.changePage()"
+ items-per-page="$ctrl.limit"
+ max-size="6"
+ force-ellipses="true"
+ previous-text="‹"
+ next-text="›"
+ first-text="«"
+ last-text="»"
+ ></ul>
+</div>
--- /dev/null
+<?php
+// Autoloader data for SearchDisplay module.
+return [
+ 'js' => [
+ 'ang/crmSearchPage.module.js',
+ 'ang/crmSearchPage/*.js',
+ 'ang/crmSearchPage/*/*.js',
+ ],
+ 'partials' => [
+ 'ang/crmSearchPage',
+ ],
+ 'basePages' => ['civicrm/search'],
+ 'requires' => ['ngRoute', 'api4', 'crmUi', 'crmSearchDisplay'],
+];
--- /dev/null
+(function(angular, $, _) {
+ "use strict";
+
+ // Declare module
+ angular.module('crmSearchPage', CRM.angRequires('crmSearchPage'))
+
+
+ .config(function($routeProvider) {
+ $routeProvider.when('/display/:savedSearchName/:displayName', {
+ controller: 'crmSearchPageDisplay',
+ templateUrl: '~/crmSearchPage/display.html',
+ resolve: {
+ // Load saved search display
+ display: function($route, crmApi4) {
+ var params = $route.current.params;
+ return crmApi4('SearchDisplay', 'get', {
+ where: [['name', '=', params.displayName], ['saved_search.name', '=', params.savedSearchName]],
+ select: ['*', 'saved_search.api_entity', 'saved_search.api_params']
+ }, 0);
+ }
+ }
+ });
+ })
+
+ // Controller for displaying a search
+ .controller('crmSearchPageDisplay', function($scope, $routeParams, $location, display) {
+ this.display = display;
+ $scope.$ctrl = this;
+ });
+
+})(angular, CRM.$, CRM._);
--- /dev/null
+<h1 crm-page-title>{{:: $ctrl.display.label }}</h1>
+
+<div ng-switch="$ctrl.display.type" id="bootstrap-theme">
+ <div ng-switch-when="table">
+ <crm-search-display-table api-entity="$ctrl.display['saved_search.api_entity']" api-params="$ctrl.display['saved_search.api_params']" settings="$ctrl.display.settings"></crm-search-display-table>
+ </div>
+</div>
'partials' => [
'ang/searchAdmin',
],
- 'basePages' => [],
- 'requires' => ['crmUi', 'crmUtil', 'ngRoute', 'ui.sortable', 'ui.bootstrap', 'api4', 'searchActions'],
+ 'basePages' => ['civicrm/admin/search'],
+ 'requires' => ['crmUi', 'crmUtil', 'ngRoute', 'ui.sortable', 'ui.bootstrap', 'api4', 'crmSearchDisplay', 'crmSearchActions'],
'settingsFactory' => ['\Civi\Search\Admin', 'getAdminSettings'],
];
// Load data for lists
savedSearches: function(crmApi4) {
return crmApi4('SavedSearch', 'get', {
- select: ['id', 'api_entity', 'form_values', 'COUNT(search_display.id) AS displays', 'GROUP_CONCAT(group.title) AS groups'],
+ select: ['id', 'label', 'api_entity', 'form_values', 'COUNT(search_display.id) AS displays', 'GROUP_CONCAT(group.title) AS groups'],
join: [['SearchDisplay AS search_display'], ['Group AS group']],
where: [['api_entity', 'IS NOT NULL']],
groupBy: ['id']
return crmApi4('SavedSearch', 'get', {
where: [['id', '=', params.id]],
chain: {
- group: ['Group', 'get', {where: [['saved_search_id', '=', '$id']]}, 0],
+ groups: ['Group', 'get', {where: [['saved_search_id', '=', '$id']]}],
displays: ['SearchDisplay', 'get', {where: [['saved_search_id', '=', '$id']]}]
}
}, 0);
<div class="crm-flex-box">
<div>
- <div class="form-inline">
- <label for="crm-search-main-entity">{{:: ts('Search for') }}</label>
- <input id="crm-search-main-entity" class="form-control" ng-model="$ctrl.savedSearch.api_entity" crm-ui-select="::{allowClear: false, data: entities}" />
- </div>
<div ng-if=":: $ctrl.paramExists('join')">
<fieldset ng-repeat="join in $ctrl.savedSearch.api_params.join">
<div class="form-inline">
<th class="crm-search-result-select">
<input type="checkbox" ng-checked="$ctrl.allRowsSelected" ng-click="selectAllRows()" ng-disabled="!(loading === false && !loadingAllRows && $ctrl.results[$ctrl.page] && $ctrl.results[$ctrl.page][0].id)">
</th>
- <th ng-repeat="col in $ctrl.savedSearch.api_params.select" ng-click="setOrderBy(col, $event)" title="{{:: ts('Drag to reorder columns, click to sort results (shift-click to sort by multiple).')}}">
+ <th ng-repeat="col in $ctrl.savedSearch.api_params.select" ng-click="setOrderBy(col, $event)" title="{{:: ts('Drag to reorder columns, click to sort results (shift-click to sort by multiple).') }}">
<i class="crm-i {{ getOrderBy(col) }}"></i>
- <span ng-class="{'crm-sortable': $index || !$ctrl.groupExists}">{{ $ctrl.getFieldLabel(col) }}</span>
+ <span ng-class="{'crm-draggable': $index || !$ctrl.groupExists}">{{ $ctrl.getFieldLabel(col) }}</span>
<span ng-switch="$index || !$ctrl.groupExists ? 'sortable' : 'locked'">
<i ng-switch-when="locked" class="crm-i fa-lock" aria-hidden="true"></i>
<a href ng-switch-default class="crm-hover-button" title="{{:: ts('Clear') }}" ng-click="$ctrl.clearParam('select', $index)"><i class="crm-i fa-times" aria-hidden="true"></i></a>
$scope.controls = {tab: 'compose'};
$scope.joinTypes = [{k: false, v: ts('Optional')}, {k: true, v: ts('Required')}];
- $scope.groupOptions = CRM.searchActions.groupOptions;
+ $scope.groupOptions = CRM.crmSearchActions.groupOptions;
$scope.entities = formatForSelect2(CRM.vars.search.schema, 'name', 'title_plural', ['description', 'icon']);
this.perm = {
editGroups: CRM.checkPerm('edit groups')
this.entityTitle = searchMeta.getEntity(this.savedSearch.api_entity).title_plural;
this.savedSearch.displays = this.savedSearch.displays || [];
- this.groupExists = !!this.savedSearch.group;
-
- this.original = _.indexBy(_.cloneDeep(this.savedSearch.displays), 'id');
- if (this.savedSearch.group) {
- this.original.group = _.cloneDeep(this.savedSearch.group);
- }
+ this.savedSearch.groups = this.savedSearch.groups || [];
+ this.groupExists = !!this.savedSearch.groups.length;
if (!this.savedSearch.api_params) {
this.savedSearch.api_params = {
+ version: 4,
select: getDefaultSelect(),
orderBy: {},
where: [],
$scope.$watch('$ctrl.savedSearch.api_params.having', onChangeFilters, true);
}
+ $scope.$watch('$ctrl.savedSearch', onChangeAnything, true);
+
+ // Is this savedSearch record saved, unsaved or saving
+ $scope.status = this.savedSearch && this.savedSearch.id ? 'saved' : 'unsaved';
+
loadFieldOptions();
};
+ function onChangeAnything() {
+ $scope.status = 'unsaved';
+ }
+
+ this.save = function() {
+ $scope.status = 'saving';
+ var params = _.cloneDeep(ctrl.savedSearch),
+ apiCalls = {},
+ chain = {};
+ if (ctrl.groupExists) {
+ chain.groups = ['Group', 'save', {defaults: {saved_search_id: '$id'}, records: params.groups}];
+ delete params.groups;
+ } else if (params.id) {
+ apiCalls.deleteGroup = ['Group', 'delete', {where: [['saved_search_id', '=', params.id]]}];
+ }
+ if (params.displays && params.displays.length) {
+ chain.displays = ['SearchDisplay', 'replace', {where: [['saved_search_id', '=', '$id']], records: params.displays}];
+ } else if (params.id) {
+ apiCalls.deleteDisplays = ['SearchDisplay', 'delete', {where: [['saved_search_id', '=', params.id]]}];
+ }
+ delete params.displays;
+ apiCalls.saved = ['SavedSearch', 'save', {records: [params], chain: chain}, 0];
+ crmApi4(apiCalls).then(function(results) {
+ ctrl.savedSearch.id = results.saved.id;
+ ctrl.savedSearch.groups = results.saved.groups || [];
+ ctrl.savedSearch.displays = results.saved.displays || [];
+ if ($scope.status === 'saving') {
+ $scope.status = 'saved';
+ }
+ });
+ };
+
this.paramExists = function(param) {
return _.includes(searchMeta.getEntity(ctrl.savedSearch.api_entity).params, param);
};
this.addDisplay = function(type) {
ctrl.savedSearch.displays.push({
- type: type
+ type: type,
+ label: '',
+ settings: {}
});
$scope.selectTab('display_' + (ctrl.savedSearch.displays.length - 1));
};
};
this.addGroup = function() {
- ctrl.savedSearch.group = {
+ ctrl.savedSearch.groups.push({
title: '',
description: '',
visibility: 'User and User Admin Only',
group_type: []
- };
+ });
ctrl.groupExists = true;
$scope.selectTab('group');
};
this.removeGroup = function() {
ctrl.groupExists = !ctrl.groupExists;
- if (!ctrl.groupExists && (!ctrl.savedSearch.group || !ctrl.savedSearch.group.id)) {
- ctrl.savedSearch.group = null;
+ if (!ctrl.groupExists && (!ctrl.savedSearch.groups.length || !ctrl.savedSearch.groups[0].id)) {
+ ctrl.savedSearch.groups.length = 0;
}
$scope.selectTab('compose');
};
$scope.sortableColumnOptions = {
axis: 'x',
- handle: '.crm-sortable',
+ handle: '.crm-draggable',
update: function(e, ui) {
// Don't allow items to be moved to position 0 if locked
if (!ui.item.sortable.dropindex && ctrl.groupExists) {
<form>
<div class="navbar-form clearfix">
- <div class="form-group pull-right">
- <button class="btn btn-default" ng-disabled="$ctrl.saved" ng-click="$ctrl.save()">
- {{ $ctrl.saved ? ts('Saved') : ts('Save') }}
+ <label for="crm-saved-search-label">{{:: ts('Label:') }}</label>
+ <input id="crm-saved-search-label" class="form-control" ng-model="$ctrl.savedSearch.label" type="text" />
+ <label for="crm-search-main-entity">{{:: ts('Search for:') }}</label>
+ <input id="crm-search-main-entity" class="form-control" ng-model="$ctrl.savedSearch.api_entity" crm-ui-select="::{allowClear: false, data: entities}" ng-disabled="$ctrl.savedSearch.id" />
+ <div class="btn-group btn-group-md pull-right">
+ <button type="submit" 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>
<fieldset ng-include="'~/searchAdmin/group.html'"></fieldset>
</div>
<div ng-switch-default>
+ <div ng-repeat="display in $ctrl.savedSearch.displays" ng-if="controls.tab === ('display_' + $index)">
+ <div ng-include="'~/searchAdmin/display.html'"></div>
+ </div>
</div>
</div>
</div>
--- /dev/null
+<fieldset>
+ <div class="form-inline">
+ <label for="search_display_label">{{:: ts('Name:') }} <span class="crm-marker">*</span></label>
+ <input id="search_display_label" type="text" class="form-control" ng-model="display.label" required />
+ <label class="pull-right">{{:: $ctrl.displayTypes[display.type].label }}</label>
+ </div>
+</fieldset>
+<!-- Load template with the name of this display type, e.g. 'table.html' -->
+<div ng-if="display.type" ng-include="'~/searchAdmin/displays/' + display.type + '.html'"></div>
--- /dev/null
+(function(angular, $, _) {
+ "use strict";
+
+ angular.module('searchAdmin').component('searchAdminDisplayTable', {
+ bindings: {
+ display: '<',
+ apiEntity: '<',
+ apiParams: '<'
+ },
+ require: {
+ crmSearchAdmin: '^crmSearchAdmin'
+ },
+ templateUrl: '~/searchAdmin/displays/searchAdminDisplayTable.html',
+ controller: function($scope, searchMeta) {
+ var ts = $scope.ts = CRM.ts(),
+ ctrl = this;
+
+ function fieldToColumn(fieldExpr) {
+ var info = searchMeta.parseExpr(fieldExpr);
+ return {
+ expr: fieldExpr,
+ label: ctrl.getFieldLabel(fieldExpr),
+ dataType: (info.fn && info.fn.name === 'COUNT') ? 'Integer' : info.field.data_type
+ };
+ }
+
+ this.sortableOptions = {
+ connectWith: '.crm-search-admin-table-columns',
+ containment: '.crm-search-admin-table-columns-wrapper'
+ };
+
+ this.removeCol = function(index) {
+ ctrl.hiddenColumns.push(ctrl.display.settings.columns[index]);
+ ctrl.display.settings.columns.splice(index, 1);
+ };
+
+ this.restoreCol = function(index) {
+ ctrl.display.settings.columns.push(ctrl.hiddenColumns[index]);
+ ctrl.hiddenColumns.splice(index, 1);
+ };
+
+ this.$onInit = function () {
+ ctrl.display.settings.limit = parseInt(ctrl.display.settings.limit || 0, 10);
+ ctrl.getFieldLabel = ctrl.crmSearchAdmin.getFieldLabel;
+ if (!ctrl.display.settings.columns) {
+ ctrl.display.settings.columns = _.transform(ctrl.apiParams.select, function(columns, fieldExpr) {
+ columns.push(fieldToColumn(fieldExpr));
+ });
+ ctrl.hiddenColumns = [];
+ } else {
+ var activeColumns = _.collect(ctrl.display.settings.columns, 'expr');
+ ctrl.hiddenColumns = _.transform(ctrl.apiParams.select, function(hiddenColumns, fieldExpr) {
+ if (!_.includes(activeColumns, fieldExpr)) {
+ hiddenColumns.push(fieldToColumn(fieldExpr));
+ }
+ });
+ _.each(activeColumns, function(fieldExpr, index) {
+ if (!_.includes(ctrl.apiParams.select, fieldExpr)) {
+ ctrl.display.settings.columns.splice(index, 1);
+ }
+ });
+ }
+ };
+
+ }
+ });
+
+})(angular, CRM.$, CRM._);
--- /dev/null
+<fieldset>
+ <div class="form-inline">
+ <label for="crm-search-admin-table-limit">{{ ts('Results to display (0 for no limit):') }}</label>
+ <input id="crm-search-admin-table-limit" type="number" min="0" step="1" class="form-control" ng-model="$ctrl.display.settings.limit">
+ <label><input type="checkbox" ng-model="$ctrl.display.settings.pager"> {{ ts('Use Pager') }}</label>
+ </div>
+</fieldset>
+<div class="crm-flex-box crm-search-admin-table-columns-wrapper">
+ <fieldset class="crm-search-admin-table-columns" ng-model="$ctrl.display.settings.columns" ui-sortable="$ctrl.sortableOptions">
+ <legend>{{:: ts('Columns') }}</legend>
+ <fieldset ng-repeat="col in $ctrl.display.settings.columns" class="crm-draggable">
+ <legend>{{ $ctrl.getFieldLabel(col.expr) }}</legend>
+ <div class="form-inline">
+ <label>{{ ts('Label:') }}</label> <input class="form-control" type="text" ng-model="col.label" />
+ <button class="btn-xs pull-right" ng-click="$ctrl.removeCol($index)" title="{{:: ts('Hide') }}">
+ <i class="crm-i fa-ban"></i>
+ </button>
+ </div>
+ </fieldset>
+ </fieldset>
+ <fieldset class="crm-search-admin-table-columns" ng-model="$ctrl.hiddenColumns" ui-sortable="$ctrl.sortableOptions">
+ <legend>{{:: ts('Hidden Columns') }}</legend>
+ <fieldset ng-repeat="col in $ctrl.hiddenColumns" class="crm-draggable">
+ <legend>{{ $ctrl.getFieldLabel(col.expr) }}</legend>
+ <div class="form-inline">
+ <label>{{ ts('Label:') }}</label> <input disabled class="form-control" type="text" ng-model="col.label" />
+ <button class="btn-xs pull-right" ng-click="$ctrl.restoreCol($index)" title="{{:: ts('Show') }}">
+ <i class="crm-i fa-undo"></i>
+ </button>
+ </div>
+ </fieldset>
+ </fieldset>
+</div>
--- /dev/null
+<search-admin-display-table api-entity="$ctrl.savedSearch.api_entity" api-params="$ctrl.savedSearch.api_params" display="display"></search-admin-display-table>
+<hr>
+<!--<crm-search-display-table api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" settings="display.settings"></crm-search-display-table>-->
<div ng-if="!$ctrl.groupExists">
<div class="alert alert-warning">
- {{:: ts('Smart group "%1" will be deleted.', {1: $ctrl.original.group.title}) }}
+ {{:: ts('Smart group "%1" will be deleted.', {1: $ctrl.savedSearch.groups[0].title}) }}
</div>
</div>
<div ng-if="$ctrl.groupExists">
<div class="alert alert-warning" ng-show="!smartGroupColumns.length">
{{:: ts('Unable to create smart group because this search does not include any contacts.') }}
</div>
- <input class="form-control" placeholder="{{:: ts('Group Title') }}" ng-model="$ctrl.savedSearch.group.title" ng-disabled="!smartGroupColumns.length">
+ <input class="form-control" placeholder="{{:: ts('Group Title') }}" ng-model="$ctrl.savedSearch.groups[0].title" ng-disabled="!smartGroupColumns.length">
<hr />
<div class="form-inline">
<label for="api-save-search-select-column">{{:: ts('Contact Column:') }} <span class="crm-marker">*</span></label>
</div>
<fieldset ng-show="smartGroupColumns.length">
<label>{{:: ts('Description:') }}</label>
- <textarea class="form-control" ng-model="$ctrl.savedSearch.group.description"></textarea>
+ <textarea class="form-control" ng-model="$ctrl.savedSearch.groups[0].description"></textarea>
<div class="form-inline">
<label>{{:: ts('Group Type:') }} </label>
<div class="checkbox" ng-repeat="option in groupOptions.group_type track by option.id">
<label>
- <input type="checkbox" checklist-model="$ctrl.savedSearch.group.group_type" checklist-value="option.id">
+ <input type="checkbox" checklist-model="$ctrl.savedSearch.groups[0].group_type" checklist-value="option.id">
{{ option.label }}
</label>
</div>
</div>
<div class="form-inline">
<label>{{:: ts('Visibility:') }}</label>
- <select class="form-control" ng-model="$ctrl.savedSearch.group.visibility" ng-options="item.id as item.label for item in groupOptions.visibility track by item.id" crm-ui-select></select>
+ <select class="form-control" ng-model="$ctrl.savedSearch.groups[0].visibility" ng-options="item.id as item.label for item in groupOptions.visibility track by item.id" crm-ui-select></select>
</div>
</fieldset>
</div>
(function(angular, $, _) {
"use strict";
- angular.module('searchAdmin').controller('searchList', function($scope, savedSearches) {
+ angular.module('searchAdmin').controller('searchList', function($scope, savedSearches, crmApi4) {
var ts = $scope.ts = CRM.ts(),
ctrl = $scope.$ctrl = this;
this.savedSearches = savedSearches;
this.entityTitles = _.transform(CRM.vars.search.schema, function(titles, entity) {
titles[entity.name] = entity.titlePlural;
}, {});
+
+ this.deleteSearch = function(search) {
+ var index = _.findIndex(savedSearches, {id: search.id});
+ if (index > -1) {
+ crmApi4([
+ ['Group', 'delete', {where: [['saved_search_id', '=', search.id]]}],
+ ['SavedSearch', 'delete', {where: [['id', '=', search.id]]}]
+ ]);
+ savedSearches.splice(index, 1);
+ }
+ };
});
})(angular, CRM.$, CRM._);
<thead>
<tr>
<th>{{:: ts('ID') }}</th>
+ <th>{{:: ts('Label') }}</th>
<th>{{:: ts('For') }}</th>
<th>{{:: ts('Displays') }}</th>
<th>{{:: ts('Smart Group') }}</th>
<tbody>
<tr ng-repeat="search in $ctrl.savedSearches">
<td>{{ search.id }}</td>
+ <td>{{ search.label }}</td>
<td>{{ $ctrl.entityTitles[search.api_entity] }}</td>
<td>{{ search.displays }}</td>
<td>{{ search.groups.join(', ') }}</td>
<td>
- <a href="#/load/SavedSearch/{{ search.id }}">{{:: ts('Edit') }}</a>
+ <a href="#/edit/{{ search.id }}">{{:: ts('Edit') }}</a>
+ <a href crm-confirm="{type: 'delete', obj: search}" on-yes="$ctrl.deleteSearch(search)">{{:: ts('Delete') }}</a>
</td>
</tr>
<tr ng-if="$ctrl.savedSearches.length === 0">
<td colspan="9">
<p class="messages status no-popup text-center">
{{:: ts('No saved searches.')}}
- <a href="#/create/Contact/">{{:: ts('New Search') }}</a>
</p>
</td>
</tr>
{{ ts('Compose Search') }}
</a>
</li>
-<li role="presentation" ng-class="{active: controls.tab === 'group'}" ng-if="$ctrl.savedSearch.group">
+<li role="presentation" ng-class="{active: controls.tab === 'group'}" ng-if="$ctrl.savedSearch.groups.length">
<a href ng-click="selectTab('group')" ng-class="{strikethrough: !$ctrl.groupExists}">
<i class="crm-i fa-users"></i>
- {{:: ts('Smart Group:') }} {{ $ctrl.savedSearch.group.title }}
+ {{:: ts('Smart Group:') }} {{ $ctrl.savedSearch.groups[0].title }}
</a>
<button class="btn-xs crm-search-delete-display" ng-click="$ctrl.removeGroup()">
<i class="crm-i fa-trash"></i>
</button>
</li>
<li role="presentation" ng-repeat="display in $ctrl.savedSearch.displays" ng-class="{active: controls.tab === ('display_' + $index)}">
- <a href ng-click="selectTab('display_' + $index)">
+ <a href ng-click="selectTab('display_' + $index)" ng-class="{strikethrough: display.trashed}">
<i class="crm-i {{ $ctrl.displayTypes[display.type].icon }}"></i>
- {{ $ctrl.displayTypes[display.type].label }}
+ {{ display.label || ts('Untitled') }}
</a>
<button class="btn-xs crm-search-delete-display" ng-click="$ctrl.removeDisplay($index)">
<i class="crm-i fa-trash"></i>
<i class="crm-i fa-plus"></i> {{:: ts('Add...') }} <span class="caret"></span>
</button>
<ul class="dropdown-menu">
- <li ng-if="!$ctrl.savedSearch.group">
+ <li ng-if="!$ctrl.savedSearch.groups.length">
<a href ng-click="$ctrl.addGroup()">
<i class="crm-i fa-users"></i>
{{:: ts('Smart Group') }}
.crm-flex-box > .crm-flex-4 {
flex: 4;
}
-.crm-sortable {
+.crm-draggable {
cursor: move;
}
<menu>
<item>
<path>civicrm/search</path>
- <page_callback>CRM_Search_Page_Admin</page_callback>
+ <page_callback>CRM_Search_Page_Search</page_callback>
<access_arguments>access CiviCRM</access_arguments>
</item>
+ <item>
+ <path>civicrm/admin/search</path>
+ <page_callback>CRM_Search_Page_Admin</page_callback>
+ <access_arguments>administer CiviCRM</access_arguments>
+ </item>
</menu>