$key = $newAliases[$column['expr']] ?? $column['expr'];
unset($display['settings']['columns'][$c]['expr']);
$display['settings']['columns'][$c]['key'] = explode(' AS ', $key)[1] ?? $key;
+ $display['settings']['columns'][$c]['type'] = 'field';
}
\Civi\Api4\SearchDisplay::update(FALSE)
->setValues($display)
namespace Civi\Search;
+use CRM_Search_ExtensionUtil as E;
+
/**
* Class Admin
* @package Civi\Search
'operators' => \CRM_Utils_Array::makeNonAssociative(self::getOperators()),
'functions' => \CRM_Api4_Page_Api4Explorer::getSqlFunctions(),
'displayTypes' => Display::getDisplayTypes(['id', 'name', 'label', 'description', 'icon']),
+ 'styles' => \CRM_Utils_Array::makeNonAssociative(self::getStyles()),
'afformEnabled' => (bool) \CRM_Utils_Array::findAll(
\CRM_Extension_System::singleton()->getMapper()->getActiveModuleFiles(),
['fullName' => 'org.civicrm.afform']
'<' => '<',
'>=' => '≥',
'<=' => '≤',
- 'CONTAINS' => ts('Contains'),
- 'IN' => ts('Is One Of'),
- 'NOT IN' => ts('Not One Of'),
- 'LIKE' => ts('Is Like'),
- 'NOT LIKE' => ts('Not Like'),
- 'BETWEEN' => ts('Is Between'),
- 'NOT BETWEEN' => ts('Not Between'),
- 'IS NULL' => ts('Is Null'),
- 'IS NOT NULL' => ts('Not Null'),
+ 'CONTAINS' => E::ts('Contains'),
+ 'IN' => E::ts('Is One Of'),
+ 'NOT IN' => E::ts('Not One Of'),
+ 'LIKE' => E::ts('Is Like'),
+ 'NOT LIKE' => E::ts('Not Like'),
+ 'BETWEEN' => E::ts('Is Between'),
+ 'NOT BETWEEN' => E::ts('Not Between'),
+ 'IS NULL' => E::ts('Is Null'),
+ 'IS NOT NULL' => E::ts('Not Null'),
+ ];
+ }
+
+ /**
+ * @return string[]
+ */
+ public static function getStyles():array {
+ return [
+ 'default' => E::ts('Default'),
+ 'primary' => E::ts('Primary'),
+ 'success' => E::ts('Success'),
+ 'info' => E::ts('Info'),
+ 'warning' => E::ts('Warning'),
+ 'danger' => E::ts('Danger'),
];
}
unset($entity['paths'][$action]);
switch ($action) {
case 'view':
- $title = ts('View %1', [1 => $entity['title']]);
+ $title = E::ts('View %1', [1 => $entity['title']]);
break;
case 'update':
- $title = ts('Edit %1', [1 => $entity['title']]);
+ $title = E::ts('Edit %1', [1 => $entity['title']]);
break;
case 'delete':
- $title = ts('Delete %1', [1 => $entity['title']]);
+ $title = E::ts('Delete %1', [1 => $entity['title']]);
break;
default:
$alias = $baseEntity['name'] . "_{$bridge}_" . $targetEntityName;
$joins[$baseEntity['name']][] = [
'label' => $baseEntity['title'] . ' ' . $targetsTitle,
- 'description' => ts('Multiple %1 per %2', [1 => $targetsTitle, 2 => $baseEntity['title']]),
+ 'description' => E::ts('Multiple %1 per %2', [1 => $targetsTitle, 2 => $baseEntity['title']]),
'entity' => $targetEntityName,
'conditions' => array_merge(
[$bridge],
$alias = $targetEntityName . "_{$bridge}_" . $baseEntity['name'];
$joins[$targetEntityName][] = [
'label' => $targetEntity['title'] . ' ' . $baseEntity['title_plural'],
- 'description' => ts('Multiple %1 per %2', [1 => $baseEntity['title_plural'], 2 => $targetEntity['title']]),
+ 'description' => E::ts('Multiple %1 per %2', [1 => $baseEntity['title_plural'], 2 => $targetEntity['title']]),
'entity' => $baseEntity['name'],
'conditions' => array_merge(
[$bridge],
$scope.$ctrl = this;
})
- .factory('searchMeta', function() {
+ .factory('searchMeta', function($q) {
function getEntity(entityName) {
if (entityName) {
return _.find(CRM.crmSearchAdmin.schema, {name: entityName});
}
});
});
+ },
+ pickIcon: function() {
+ var deferred = $q.defer();
+ $('#crm-search-admin-icon-picker').off('change').siblings('.crm-icon-picker-button').click();
+ $('#crm-search-admin-icon-picker').on('change', function() {
+ deferred.resolve($(this).val());
+ });
+ return deferred.promise;
+ }
+ };
+ })
+ .directive('contenteditable', function() {
+ return {
+ require: 'ngModel',
+ link: function(scope, elm, attrs, ctrl) {
+ // view -> model
+ elm.on('blur', function() {
+ ctrl.$setViewValue(elm.html());
+ });
+
+ // model -> view
+ ctrl.$render = function() {
+ elm.html(ctrl.$viewValue);
+ };
}
};
});
+ // Shoehorn in a non-angular widget for picking icons
+ $(function() {
+ $('#crm-container').append('<div style="display:none"><input id="crm-search-admin-icon-picker"></div>');
+ CRM.loadScript(CRM.config.resourceBase + 'js/jquery/jquery.crmIconPicker.js').done(function() {
+ $('#crm-search-admin-icon-picker').crmIconPicker();
+ });
+ });
+
})(angular, CRM.$, CRM._);
this.preview = this.stale = false;
+ this.colTypes = {
+ links: {
+ label: ts('Links'),
+ icon: 'fa-link',
+ defaults: {
+ links: []
+ }
+ },
+ buttons: {
+ label: ts('Buttons'),
+ icon: 'fa-square-o',
+ defaults: {
+ size: 'btn-sm',
+ links: []
+ }
+ },
+ menu: {
+ label: ts('Menu'),
+ icon: 'fa-bars',
+ defaults: {
+ text: ts('Actions'),
+ style: 'default',
+ size: 'btn-sm',
+ icon: 'fa-bars',
+ links: []
+ }
+ },
+ };
+
this.sortableOptions = {
connectWith: '.crm-search-admin-edit-columns',
containment: '.crm-search-admin-edit-columns-wrapper'
};
+ this.styles = CRM.crmSearchAdmin.styles;
+
+ this.addCol = function(type) {
+ var col = _.cloneDeep(this.colTypes[type].defaults);
+ col.type = type;
+ if (this.display.type === 'table') {
+ col.alignment = 'text-right';
+ }
+ ctrl.display.settings.columns.push(col);
+ };
+
this.removeCol = function(index) {
- ctrl.hiddenColumns.push(ctrl.display.settings.columns[index]);
+ if (ctrl.display.settings.columns[index].type === 'field') {
+ ctrl.hiddenColumns.push(ctrl.display.settings.columns[index]);
+ }
ctrl.display.settings.columns.splice(index, 1);
};
return searchMeta.getDefaultLabel(expr);
};
+ this.getColLabel = function(col) {
+ if (col.type === 'field') {
+ return ctrl.getFieldLabel(col.key);
+ }
+ return ctrl.colTypes[col.type].label;
+ };
+
function fieldToColumn(fieldExpr, defaults) {
var info = searchMeta.parseExpr(fieldExpr),
values = _.cloneDeep(defaults);
return values;
}
+ this.getLinks = function() {
+ if (!ctrl.links) {
+ ctrl.links = buildLinks();
+ }
+ return ctrl.links;
+ };
+
+ // Build a list of all possible links to main entity or join entities
+ function buildLinks() {
+ // Links to main entity
+ var links = _.cloneDeep(searchMeta.getEntity(ctrl.savedSearch.api_entity).paths || []);
+ // Links to explicitly joined entities
+ _.each(ctrl.savedSearch.api_params.join, function(join) {
+ var joinName = join[0].split(' AS '),
+ joinEntity = searchMeta.getEntity(joinName[0]);
+ _.each(joinEntity.paths, function(path) {
+ var link = _.cloneDeep(path);
+ link.path = link.path.replace(/\[/g, '[' + joinName[1] + '.');
+ links.push(link);
+ });
+ });
+ // Links to implicit joins
+ _.each(ctrl.savedSearch.api_params.select, function(fieldName) {
+ if (!_.includes(fieldName, ' AS ')) {
+ var info = searchMeta.parseExpr(fieldName);
+ if (info.field && !info.suffix && !info.fn && (info.field.fk_entity || info.field.entity !== info.field.baseEntity)) {
+ var idField = info.field.fk_entity ? fieldName : fieldName.substr(0, fieldName.lastIndexOf('.')) + '_id';
+ if (!ctrl.crmSearchAdmin.canAggregate(idField)) {
+ var joinEntity = searchMeta.getEntity(info.field.fk_entity || info.field.entity);
+ _.each(joinEntity.paths, function(path) {
+ var link = _.cloneDeep(path);
+ link.path = link.path.replace(/\[id/g, '[' + idField);
+ links.push(link);
+ });
+ }
+ }
+ }
+ });
+ return links;
+ }
+
+ this.pickIcon = function(model, key) {
+ searchMeta.pickIcon().then(function(icon) {
+ model[key] = icon;
+ });
+ };
+
// Helper function to sort active from hidden columns and initialize each column with defaults
this.initColumns = function(defaults) {
if (!ctrl.display.settings.columns) {
}
});
_.eachRight(activeColumns, function(key, index) {
- if (!_.includes(selectAliases, key)) {
+ if (key && !_.includes(selectAliases, key)) {
ctrl.display.settings.columns.splice(index, 1);
}
});
--- /dev/null
+(function(angular, $, _) {
+ "use strict";
+
+ angular.module('crmSearchAdmin').component('crmSearchAdminLinkGroup', {
+ bindings: {
+ group: '<',
+ apiEntity: '<',
+ apiParams: '<',
+ links: '<'
+ },
+ templateUrl: '~/crmSearchAdmin/crmSearchAdminLinkGroup.html',
+ controller: function ($scope, $element, $timeout, searchMeta) {
+ var ts = $scope.ts = CRM.ts(),
+ ctrl = this;
+
+ this.styles = CRM.crmSearchAdmin.styles;
+
+ this.setValue = function(val, index) {
+ var link = ctrl.getLink(val),
+ item = ctrl.group[index];
+ if (item.path === val) {
+ return;
+ }
+ item.path = val;
+ item.icon = link ? defaultIcons[link.action] : 'fa-external-link';
+ if (val === 'civicrm/') {
+ $timeout(function () {
+ $('tr:eq(' + index + ') input[type=text]', $element).focus();
+ });
+ }
+ };
+
+ this.sortableOptions = {
+ containment: 'tbody',
+ direction: 'vertical',
+ helper: function(e, ui) {
+ // Prevent table row width from changing during drag
+ ui.children().each(function() {
+ $(this).width($(this).width());
+ });
+ return ui;
+ }
+ };
+
+ var defaultIcons = {
+ view: 'fa-external-link',
+ update: 'fa-pencil',
+ delete: 'fa-trash'
+ };
+
+ var defaultStyles = {
+ view: 'primary',
+ update: 'warning',
+ delete: 'danger'
+ };
+
+ $scope.pickIcon = function(index) {
+ searchMeta.pickIcon().then(function(icon) {
+ ctrl.group[index].icon = icon;
+ });
+ };
+
+ this.addItem = function(path) {
+ var link = ctrl.getLink(path);
+ ctrl.group.push({
+ path: path,
+ style: link && defaultStyles[link.action] || 'default',
+ text: link ? link.title : '',
+ icon: link && defaultIcons[link.action] || 'fa-external-link'
+ });
+ };
+
+ this.$onInit = function() {
+ if (!ctrl.group.length) {
+ if (ctrl.links.length) {
+ _.each(_.pluck(ctrl.links, 'path'), ctrl.addItem);
+ } else {
+ ctrl.addItem('civicrm/');
+ }
+ }
+ $element.on('change', 'select.crm-search-admin-select-path', function() {
+ var $select = $(this);
+ $scope.$apply(function() {
+ if ($select.closest('tfoot').length) {
+ ctrl.addItem($select.val());
+ $select.val('');
+ } else {
+ ctrl.setValue($select.val(), $select.closest('tr').index());
+ }
+ });
+ });
+ };
+
+ this.getLink = function(path) {
+ return _.findWhere(ctrl.links, {path: path});
+ };
+
+ }
+ });
+
+})(angular, CRM.$, CRM._);
--- /dev/null
+<table>
+ <thead>
+ <tr>
+ <th class="crm-search-admin-icon-col"></th>
+ <th class="crm-search-admin-icon-col">{{:: ts('Icon') }}</th>
+ <th>{{:: ts('Text') }}</th>
+ <th>{{:: ts('Link') }}</th>
+ <th>{{:: ts('Style') }}</th>
+ <th class="crm-search-admin-icon-col"></th>
+ </tr>
+ </thead>
+ <tbody ui-sortable="$ctrl.sortableOptions" ng-model="$ctrl.group">
+ <tr ng-repeat="item in $ctrl.group" class="bg-{{ item.style }}">
+ <td class="crm-search-admin-icon-col">
+ <i class="crm-i fa-arrows disabled"></i>
+ </td>
+ <td class="crm-search-admin-icon-col">
+ <span class="crm-editable-enabled" ng-click="pickIcon($index)">
+ <i class="{{ item.icon ? 'crm-i ' + item.icon : '' }}"></i>
+ </span>
+ </td>
+ <td>
+ <input type="text" class="form-control" ng-model="item.text">
+ </td>
+ <td class="form-inline">
+ <select class="form-control crm-search-admin-select-path" ng-show="$ctrl.links.length">
+ <option ng-repeat="link in $ctrl.links" value="{{ link.path }}" ng-selected="item.path === link.path">
+ {{ link.title }}
+ </option>
+ <option value="civicrm/" ng-selected="item.path && !$ctrl.getLink(item.path)">
+ {{ ts('Other...') }}
+ </option>
+ </select>
+ <input class="form-control" type="text" ng-if="item.path && !$ctrl.getLink(item.path)" ng-model="item.path" ng-model-options="{updateOn: 'blur'}" />
+ <crm-search-admin-token-select ng-if="item.path && !$ctrl.getLink(item.path)" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" model="item" field="path"></crm-search-admin-token-select>
+ </td>
+ <td class="form-inline">
+ <select class="form-control" ng-model="item.style">
+ <option ng-repeat="opt in $ctrl.styles" value="{{ opt.key }}">{{ opt.value }}</option>
+ </select>
+ </td>
+ <td class="crm-search-admin-icon-col">
+ <button ng-if="$ctrl.group.length > 1" type="button" class="btn btn-xs btn-danger-outline" ng-click="$ctrl.group.splice($index, 1)">
+ <i class="crm-i fa-times"></i>
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ <tfoot>
+ <tr>
+ <td colspan="6" class="form-inline">
+ <select class="form-control crm-search-admin-select-path" ng-show="$ctrl.links.length">
+ <option value="">
+ + {{:: ts('Add...') }}
+ </option>
+ <option ng-repeat="link in $ctrl.links" value="{{ link.path }}">
+ {{ link.title }}
+ </option>
+ <option value="civicrm/">
+ {{:: ts('Other...') }}
+ </option>
+ </select>
+ </td>
+ </tr>
+ </tfoot>
+</table>
bindings: {
column: '<',
apiEntity: '<',
- apiParams: '<'
- },
- require: {
- crmSearchAdmin: '^^crmSearchAdmin'
+ apiParams: '<',
+ links: '<'
},
templateUrl: '~/crmSearchAdmin/crmSearchAdminLinkSelect.html',
- controller: function ($scope, $element, $timeout, searchMeta) {
+ controller: function ($scope, $element, $timeout) {
var ts = $scope.ts = CRM.ts(),
ctrl = this;
- // Return all possible links to main entity or join entities
- function getLinks() {
- // Links to main entity
- var links = _.cloneDeep(searchMeta.getEntity(ctrl.apiEntity).paths || []);
- // Links to explicitly joined entities
- _.each(ctrl.apiParams.join, function(join) {
- var joinName = join[0].split(' AS '),
- joinEntity = searchMeta.getEntity(joinName[0]);
- _.each(joinEntity.paths, function(path) {
- var link = _.cloneDeep(path);
- link.path = link.path.replace(/\[/g, '[' + joinName[1] + '.');
- links.push(link);
- });
- });
- // Links to implicit joins
- _.each(ctrl.crmSearchAdmin.savedSearch.api_params.select, function(fieldName) {
- if (!_.includes(fieldName, ' AS ')) {
- var info = searchMeta.parseExpr(fieldName);
- if (info.field && !info.suffix && !info.fn && (info.field.fk_entity || info.field.entity !== info.field.baseEntity)) {
- var idField = info.field.fk_entity ? fieldName : fieldName.substr(0, fieldName.lastIndexOf('.')) + '_id';
- if (!ctrl.crmSearchAdmin.canAggregate(idField)) {
- var joinEntity = searchMeta.getEntity(info.field.fk_entity || info.field.entity);
- _.each(joinEntity.paths, function(path) {
- var link = _.cloneDeep(path);
- link.path = link.path.replace(/\[id/g, '[' + idField);
- links.push(link);
- });
- }
- }
- }
- });
- return links;
- }
-
this.setValue = function(val) {
var link = ctrl.getLink(val),
oldLink = ctrl.getLink(ctrl.column.link);
}
this.$onInit = function() {
- this.links = getLinks();
-
$('select', $element).on('change', function() {
$scope.$apply(onChange);
});
{{ $ctrl.column.link ? ts('Link:') : ts('Link') }}
</label>
<select class="form-control" ng-show="$ctrl.links.length && $ctrl.column.link">
- <option value="">{{ ts('None') }}</option>
<option ng-repeat="link in $ctrl.links" value="{{ link.path }}" ng-selected="$ctrl.column.link === link.path">
{{ link.title }}
</option>
--- /dev/null
+<div class="form-inline">
+ <label for="crm-search-admin-col-size-{{$index}}">
+ {{:: ts('Button Size:') }}
+ </label>
+ <select id="crm-search-admin-col-size-{{$index}}" class="form-control" ng-model="col.size">
+ <option value="btn-lg">{{:: ts('Large') }}</option>
+ <option value="">{{:: ts('Regular') }}</option>
+ <option value="btn-sm">{{:: ts('Small') }}</option>
+ <option value="btn-xs">{{:: ts('Tiny') }}</option>
+ </select>
+</div>
+<hr>
+<crm-search-admin-link-group links="$ctrl.parent.getLinks()" group="col.links" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams"></crm-search-admin-link-group>
-<crm-search-admin-link-select class="form-inline crm-search-admin-flex-row" column="col" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams"></crm-search-admin-link-select>
+<crm-search-admin-link-select class="form-inline crm-search-admin-flex-row" column="col" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" links=":: $ctrl.parent.getLinks()"></crm-search-admin-link-select>
<div class="form-inline crm-search-admin-flex-row">
<label>
<input type="checkbox" ng-checked="col.title" ng-click="col.title = col.title ? null : $ctrl.parent.getFieldLabel(col.key)" >
--- /dev/null
+<hr>
+<crm-search-admin-link-group links="$ctrl.parent.getLinks()" group="col.links" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams"></crm-search-admin-link-group>
--- /dev/null
+<div class="form-inline">
+ <label for="crm-search-admin-col-size-{{$index}}">
+ {{:: ts('Menu Button Size:') }}
+ </label>
+ <select id="crm-search-admin-col-size-{{$index}}" class="form-control" ng-model="col.size">
+ <option value="btn-lg">{{:: ts('Large') }}</option>
+ <option value="">{{:: ts('Regular') }}</option>
+ <option value="btn-sm">{{:: ts('Small') }}</option>
+ <option value="btn-xs">{{:: ts('Tiny') }}</option>
+ </select>
+ <label for="crm-search-admin-col-style-{{$index}}">
+ {{:: ts('Style:') }}
+ </label>
+ <select id="crm-search-admin-col-style-{{$index}}" class="form-control" ng-model="col.style">
+ <option ng-repeat="opt in $ctrl.parent.styles" value="{{ opt.key }}">{{ opt.value }}</option>
+ </select>
+</div>
+<div class="form-inline">
+ <label>
+ {{:: ts('Menu Text/Icon:') }}
+ </label>
+ <div class="btn-group">
+ <button type="button" class="btn btn-{{ col.style + ' ' + col.size }}">
+ <span class="crm-editable-enabled" ng-click="$ctrl.parent.pickIcon(col, 'icon')">
+ <i class="{{ col.icon ? 'crm-i ' + col.icon : '' }}"></i>
+ </span>
+ <span crm-ui-editable ng-model="col.text">{{ col.text }}</span>
+ </button>
+ </div>
+</div>
+<hr>
+<crm-search-admin-link-group links="$ctrl.parent.getLinks()" group="col.links" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams"></crm-search-admin-link-group>
--- /dev/null
+<button type="button" class="btn dropdown-toggle btn-default btn-sm" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+ <i class="crm-i fa-plus"></i>
+ {{:: ts('Add') }} <span class="caret"></span>
+</button>
+<ul class="dropdown-menu">
+ <li ng-repeat="(type, col) in $ctrl.parent.colTypes">
+ <a href ng-click="$ctrl.parent.addCol(type)"><i class="fa {{col.icon}}"></i> {{ col.label }}</a>
+ </li>
+</ul>
-<fieldset class="crm-search-admin-edit-columns crm-search-admin-unused-columns" ng-model="$ctrl.parent.hiddenColumns" ui-sortable="$ctrl.parent.sortableOptions">
- <legend>{{:: ts('Unused') }}</legend>
+<legend>{{:: ts('Unused') }}</legend>
+<div class="crm-search-admin-edit-columns crm-search-admin-unused-columns" ng-model="$ctrl.parent.hiddenColumns" ui-sortable="$ctrl.parent.sortableOptions">
<fieldset ng-repeat="col in $ctrl.parent.hiddenColumns" class="crm-draggable">
<legend>{{ $ctrl.parent.getFieldLabel(col.key) }}</legend>
<div class="form-inline">
</button>
</div>
</fieldset>
-</fieldset>
+</div>
pager: true
};
}
- ctrl.parent.initColumns({key: true, dataType: true});
+ ctrl.parent.initColumns({key: true, dataType: true, type: 'field'});
};
}
</div>
</fieldset>
<div class="crm-flex-box crm-search-admin-edit-columns-wrapper">
- <fieldset class="crm-search-admin-edit-columns" ng-model="$ctrl.display.settings.columns" ui-sortable="$ctrl.parent.sortableOptions">
+ <fieldset class="crm-flex-3">
<legend>{{:: ts('Fields') }}</legend>
- <fieldset ng-repeat="col in $ctrl.display.settings.columns" class="crm-draggable">
- <legend>{{ $ctrl.parent.getFieldLabel(col.key) }}</legend>
- <div class="form-inline" title="{{ ts('Should this item display on its own line or inline with other items?') }}">
- <label><input type="checkbox" ng-model="col.break"> {{:: ts('Display on new line') }}</label>
- <button type="button" class="btn-xs pull-right" ng-click="$ctrl.parent.removeCol($index)" title="{{:: ts('Hide') }}">
- <i class="crm-i fa-ban"></i>
- </button>
- </div>
- <div class="form-inline crm-search-admin-flex-row">
- <label>
- <input type="checkbox" ng-checked="col.label" ng-click="col.label = col.label ? null : $ctrl.parent.getFieldLabel(col.key)" >
- {{ col.label ? ts('Label:') : ts('Label') }}
- </label>
- <input ng-if="col.label" class="form-control" type="text" ng-model="col.label" ng-model-options="{updateOn: 'blur'}">
- <crm-search-admin-token-select ng-if="col.label" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" model="col" field="label"></crm-search-admin-token-select>
- </div>
- <div class="form-inline" ng-if="col.label">
- <label style="visibility: hidden"><input type="checkbox" disabled></label>
- <div class="checkbox">
- <label><input type="checkbox" ng-model="col.forceLabel"> {{:: ts('Show label even when field is blank') }}</label>
+ <div ng-include="'~/crmSearchAdmin/displays/common/addColMenu.html'" class="btn-group"></div>
+ <div class="crm-search-admin-edit-columns" ng-model="$ctrl.display.settings.columns" ui-sortable="$ctrl.parent.sortableOptions">
+ <fieldset ng-repeat="col in $ctrl.display.settings.columns" class="crm-draggable">
+ <legend>{{ $ctrl.parent.getColLabel(col) }}</legend>
+ <div class="form-inline" title="{{ ts('Should this item display on its own line or inline with other items?') }}">
+ <label><input type="checkbox" ng-model="col.break"> {{:: ts('Display on new line') }}</label>
+ <button type="button" class="btn-xs pull-right" ng-click="$ctrl.parent.removeCol($index)" title="{{:: ts('Remove') }}">
+ <i class="crm-i fa-ban"></i>
+ </button>
</div>
- </div>
- <div ng-include="'~/crmSearchAdmin/displays/common/fieldOptions.html'"></div>
- </fieldset>
+ <div class="form-inline crm-search-admin-flex-row">
+ <label>
+ <input type="checkbox" ng-checked="col.label" ng-click="col.label = col.label ? null : $ctrl.parent.getFieldLabel(col.key)" >
+ {{ col.label ? ts('Label:') : ts('Label') }}
+ </label>
+ <input ng-if="col.label" class="form-control" type="text" ng-model="col.label" ng-model-options="{updateOn: 'blur'}">
+ <crm-search-admin-token-select ng-if="col.label" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" model="col" field="label"></crm-search-admin-token-select>
+ </div>
+ <div class="form-inline" ng-if="col.label">
+ <label style="visibility: hidden"><input type="checkbox" disabled></label><!--To indent by 1 checkbox-width-->
+ <div class="checkbox">
+ <label><input type="checkbox" ng-model="col.forceLabel"> {{:: ts('Show label even when field is blank') }}</label>
+ </div>
+ </div>
+ <div ng-include="'~/crmSearchAdmin/displays/colType/' + col.type + '.html'"></div>
+ </fieldset>
+ </div>
</fieldset>
- <div ng-include="'~/crmSearchAdmin/displays/common/unusedColumns.html'"></div>
+ <fieldset ng-include="'~/crmSearchAdmin/displays/common/unusedColumns.html'"></fieldset>
</div>
pager: true
};
}
- ctrl.parent.initColumns({key: true, label: true, dataType: true});
+ ctrl.parent.initColumns({key: true, label: true, dataType: true, type: 'field'});
};
}
</div>
</fieldset>
<div class="crm-flex-box crm-search-admin-edit-columns-wrapper">
- <fieldset class="crm-search-admin-edit-columns" ng-model="$ctrl.display.settings.columns" ui-sortable="$ctrl.parent.sortableOptions">
+ <fieldset class="crm-flex-3">
<legend>{{:: ts('Columns') }}</legend>
- <fieldset ng-repeat="col in $ctrl.display.settings.columns" class="crm-draggable">
- <legend>{{ $ctrl.parent.getFieldLabel(col.key) }}</legend>
- <div class="form-inline crm-search-admin-flex-row">
- <label for="crm-search-admin-edit-col-{{ $index }}">{{:: ts('Header:') }}</label>
- <input id="crm-search-admin-edit-col-{{ $index }}" class="form-control" type="text" ng-model="col.label" >
-
- <button type="button" class="btn-xs" ng-click="$ctrl.parent.removeCol($index)" title="{{:: ts('Hide') }}">
- <i class="crm-i fa-ban"></i>
- </button>
- </div>
- <div class="form-inline">
- <label>{{:: ts('Alignment:') }}</label>
- <select ng-model="col.alignment" class="form-control">
- <option value="">{{:: ts('Left') }}</option>
- <option value="text-center">{{:: ts('Center') }}</option>
- <option value="text-right">{{:: ts('Right') }}</option>
- </select>
- </div>
- <div ng-include="'~/crmSearchAdmin/displays/common/fieldOptions.html'"></div>
- </fieldset>
+ <div ng-include="'~/crmSearchAdmin/displays/common/addColMenu.html'" class="btn-group"></div>
+ <div class="crm-search-admin-edit-columns" ng-model="$ctrl.display.settings.columns" ui-sortable="$ctrl.parent.sortableOptions">
+ <fieldset ng-repeat="col in $ctrl.display.settings.columns" class="crm-draggable">
+ <legend>{{ $ctrl.parent.getColLabel(col) }}</legend>
+ <div class="form-inline crm-search-admin-flex-row">
+ <label for="crm-search-admin-edit-col-{{ $index }}">{{:: ts('Header:') }}</label>
+ <input id="crm-search-admin-edit-col-{{ $index }}" class="form-control" type="text" ng-model="col.label" >
+ <button type="button" class="btn-xs" ng-click="$ctrl.parent.removeCol($index)" title="{{:: ts('Remove') }}">
+ <i class="crm-i fa-ban"></i>
+ </button>
+ </div>
+ <div class="form-inline">
+ <label>{{:: ts('Alignment:') }}</label>
+ <select ng-model="col.alignment" class="form-control">
+ <option value="">{{:: ts('Left') }}</option>
+ <option value="text-center">{{:: ts('Center') }}</option>
+ <option value="text-right">{{:: ts('Right') }}</option>
+ </select>
+ </div>
+ <div ng-include="'~/crmSearchAdmin/displays/colType/' + col.type + '.html'"></div>
+ </fieldset>
+ </div>
</fieldset>
- <div ng-include="'~/crmSearchAdmin/displays/common/unusedColumns.html'"></div>
+ <fieldset ng-include="'~/crmSearchAdmin/displays/common/unusedColumns.html'"></fieldset>
</div>
if (url.slice(0, 1) !== '/' && url.slice(0, 4) !== 'http') {
url = CRM.url(url);
}
- return _.escape(url);
+ return url;
}
// Returns html-escaped display value for a single column in a row
displayValue = column.rewrite ? replaceTokens(column.rewrite, rowData, rowMeta) : formatRawValue(column, rowData[key]),
result = _.escape(displayValue);
if (column.link) {
- result = '<a href="' + getUrl(column.link, rowData) + '">' + result + '</a>';
+ result = '<a href="' + _.escape(getUrl(column.link, rowData)) + '">' + result + '</a>';
}
return result;
}
formatDisplayValue: formatDisplayValue,
getApiParams: getApiParams,
getResults: getResults,
- replaceTokens: replaceTokens
+ replaceTokens: replaceTokens,
+ getUrl: getUrl
};
});
--- /dev/null
+<span ng-repeat="item in col.links">
+ <a class="btn {{:: col.size }} btn-{{:: item.style }}" href="{{:: displayUtils.getUrl(item.path, row) }}">
+ <i ng-if=":: item.icon" class="crm-i {{:: item.icon }}"></i>
+ {{:: item.text }}
+ </a>
+</span>
--- /dev/null
+<span ng-bind-html="$ctrl.formatFieldValue(row, col)"></span>
--- /dev/null
+<span ng-repeat="item in col.links">
+ <a class="text-{{:: item.style }}" href="{{:: displayUtils.getUrl(item.path, row) }}">
+ <i ng-if=":: item.icon" class="crm-i {{:: item.icon }}"></i>
+ {{:: item.text }}
+ </a>
+</span>
--- /dev/null
+<div class="btn-group">
+ <button type="button" class="dropdown-toggle {{:: col.size }} btn-{{:: col.style }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" ng-click="col.open = true">
+ <i ng-if=":: col.icon" class="crm-i {{:: col.icon }}"></i>
+ {{:: col.text }}
+ </button>
+ <ul class="dropdown-menu {{ col.alignment === 'text-right' ? 'dropdown-menu-right' : '' }}" ng-if=":: col.open">
+ <li ng-repeat="item in col.links" class="bg-{{:: item.style }}">
+ <a href="{{:: displayUtils.getUrl(item.path, row) }}">
+ <i ng-if=":: item.icon" class="crm-i {{:: item.icon }}"></i>
+ {{:: item.text }}
+ </a>
+ </li>
+ </ul>
+</div>
ctrl.getResults();
}
- $scope.formatResult = function(rowData, col) {
- var formatted = searchDisplayUtils.formatDisplayValue(rowData, col.key, ctrl.settings.columns);
- if (col.label && (formatted.length || col.forceLabel)) {
- var label = searchDisplayUtils.replaceTokens(col.label, rowData, ctrl.settings.columns);
- formatted = '<label>' + _.escape(label) + '</label> ' + formatted;
- }
- return formatted;
+ this.formatFieldValue = function(rowData, col) {
+ return searchDisplayUtils.formatDisplayValue(rowData, col.key, ctrl.settings.columns);
};
}
<li ng-repeat="row in $ctrl.results">
- <div ng-repeat="col in $ctrl.settings.columns" ng-bind-html="formatResult(row, col)" title="{{:: displayUtils.replaceTokens(col.title, row, $ctrl.settings.columns) }}" class="{{:: col.break ? '' : 'crm-inline-block' }}">
+ <div ng-repeat="col in $ctrl.settings.columns" title="{{:: displayUtils.replaceTokens(col.title, row, $ctrl.settings.columns) }}" class="{{:: col.break ? '' : 'crm-inline-block' }}">
+ <label ng-if=":: col.label && (col.type !== 'field' || col.forceLabel || row[col.key])">
+ {{:: displayUtils.replaceTokens(col.label, row, $ctrl.settings.columns) }}
+ </label>
+ <span ng-include="'~/crmSearchDisplay/colType/' + col.type + '.html'"></span>
</div>
</li>
* @param $event
*/
$scope.setSort = function(col, $event) {
+ if (col.type !== 'field') {
+ return;
+ }
var dir = $scope.getSort(col) === 'fa-sort-asc' ? 'DESC' : 'ASC';
if (!$event.shiftKey || !ctrl.sort) {
ctrl.sort = [];
ctrl.getResults();
};
- $scope.formatResult = function(rowData, col) {
+ this.formatFieldValue = function(rowData, col) {
return searchDisplayUtils.formatDisplayValue(rowData, col.key, ctrl.settings.columns);
};
<input type="checkbox" ng-checked="$ctrl.allRowsSelected" ng-click="selectAllRows()" >
</th>
<th ng-repeat="col in $ctrl.settings.columns" ng-click="setSort(col, $event)" title="{{:: ts('Click to sort results (shift-click to sort by multiple).') }}">
- <i class="crm-i {{ getSort(col) }}"></i>
+ <i ng-if="col.type === 'field'" class="crm-i {{ getSort(col) }}"></i>
<span>{{ col.label }}</span>
</th>
</tr>
<td ng-if="$ctrl.settings.actions">
<input type="checkbox" ng-checked="isRowSelected(row)" ng-click="selectRow(row)" ng-disabled="!(!loadingAllRows && row.id)">
</td>
- <td ng-repeat="col in $ctrl.settings.columns" ng-bind-html="formatResult(row, col)" title="{{:: displayUtils.replaceTokens(col.title, row, $ctrl.settings.columns) }}" class="{{:: col.alignment }}">
+ <td ng-repeat="col in $ctrl.settings.columns" ng-include="'~/crmSearchDisplay/colType/' + col.type + '.html'" title="{{:: displayUtils.replaceTokens(col.title, row, $ctrl.settings.columns) }}" class="{{:: col.alignment }}">
</td>
<td></td>
</tr>
top: 0;
}
+#bootstrap-theme .crm-search-admin-edit-columns {
+ height: 100%;
+}
+
#bootstrap-theme .crm-search-admin-flex-row {
display: flex;
align-items: center;
min-height: 60px;
}
+#bootstrap-theme.crm-search .crm-editable-enabled {
+ display: inline-block;
+ padding: 0 4px !important;
+ border: 2px solid transparent !important;
+ min-width: 21px;
+}
+#bootstrap-theme.crm-search .crm-editable-enabled:hover:not(:focus) {
+ border: 2px dashed grey !important;
+}
+#bootstrap-theme.crm-search .crm-editable-enabled:before,
+#bootstrap-theme.crm-search .crm-editable-enabled:after {
+ content: '';
+ display: none;
+}
+
+#bootstrap-theme .crm-search-admin-icon-col {
+ width: 24px;
+}
+#bootstrap-theme tbody .crm-search-admin-icon-col {
+ text-align: center;
+}
+
#bootstrap-theme input[type=search]::placeholder {
font-family: FontAwesome;
text-align: right;