'layout' => [],
];
break;
+
+ case 'search':
+ $info['definition'] = $this->definition + [
+ 'title' => '',
+ 'permission' => 'access CiviCRM',
+ 'layout' => [
+ [
+ '#tag' => 'div',
+ 'af-fieldset' => '',
+ '#children' => [],
+ ],
+ ],
+ ];
+ break;
}
}
$entities[] = $info['definition']['join'] ?? $info['definition']['block'];
}
+ if ($info['definition']['type'] === 'search') {
+ $getFieldsMode = 'search';
+ $displayTags = [];
+ if ($newForm) {
+ [$searchName, $displayName] = array_pad(explode('.', $this->entity ?? ''), 2, '');
+ $displayTags[] = ['search-name' => $searchName, 'display-name' => $displayName];
+ }
+ else {
+ foreach (\Civi\Search\Display::getDisplayTypes(['name']) as $displayType) {
+ $displayTags = array_merge($displayTags, \CRM_Utils_Array::findAll($info['definition']['layout'], ['#tag' => $displayType['name']]));
+ }
+ }
+ 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')
+ ->execute()->first();
+ $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[] = explode(' AS ', $join[0])[0];
+ }
+ }
+ $entities = array_unique($entities);
+ }
+
// Optimization - since contact fields are a combination of these three,
// we'll combine them client-side rather than sending them via ajax.
- if (array_intersect($entities, ['Individual', 'Household', 'Organization'])) {
+ elseif (array_intersect($entities, ['Individual', 'Household', 'Organization'])) {
$entities = array_diff($entities, ['Contact']);
}
'name' => 'fields',
'data_type' => 'Array',
],
+ [
+ 'name' => 'search_displays',
+ 'data_type' => 'Array',
+ ],
];
}
});
$scope.tabs.block.options = _.sortBy(links, 'Label');
}
+
+ 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']
+ });
+ });
+ $scope.tabs.search.options = _.sortBy(links, 'Label');
+ });
+ }
};
this.revert = function(afform) {
<tr>
<th>{{:: ts('Title') }}</th>
<th>{{:: ts('Name') }}</th>
- <th>{{:: ts('Server Route') }}</th>
- <th>{{:: ts('Frontend?') }}</th>
+ <th>{{:: ts('Page') }}</th>
+ <th>{{:: ts('Style') }}</th>
<th></th>
</tr>
</thead>
background-color: #b3b3b3;
}
+#afGuiEditor .af-gui-bar > .form-inline > span {
+ color: #696969;
+ font-style: italic;
+}
+
#afGuiEditor .af-gui-element {
position: relative;
padding: 0 3px 3px;
display: block;
}
+#afGuiEditor .af-gui-search-display {
+ border: 1px dotted gray;
+ color: gray;
+ padding: 3em;
+ background-color: #f9f9f9;
+}
+
#afGuiEditor .af-gui-container-type-fieldset {
box-shadow: 0 0 5px #bbbbbb;
}
delete entity.fields;
});
CRM.afGuiEditor.blocks = {};
+ CRM.afGuiEditor.searchDisplays = {};
},
// Takes the results from api.Afform.loadAdminData and processes the metadata
(CRM.afGuiEditor.entities.Organization || {}).fields
);
}
+ _.each(data.search_displays, function(display) {
+ CRM.afGuiEditor.searchDisplays[display['saved_search.name'] + '.' + display.name] = display;
+ });
},
meta: CRM.afGuiEditor,
},
getField: function(entityName, fieldName) {
- return CRM.afGuiEditor.entities[entityName].fields[fieldName];
+ var fields = CRM.afGuiEditor.entities[entityName].fields;
+ return fields[fieldName] || fields[fieldName.substr(fieldName.indexOf('.') + 1)];
},
// Recursively searches a collection and its children using _.filter
}
}
- else if ($scope.afform.type === 'block') {
+ if ($scope.afform.type === 'block') {
editor.layout['#children'] = $scope.afform.layout;
editor.blockEntity = $scope.afform.join || $scope.afform.block;
$scope.entities[editor.blockEntity] = {
};
}
+ if ($scope.afform.type === 'search') {
+ editor.layout['#children'] = afGui.findRecursive($scope.afform.layout, {'af-fieldset': ''})[0]['#children'];
+
+ }
+
// Set changesSaved to true on initial load, false thereafter whenever changes are made to the model
$scope.changesSaved = editor.mode === 'edit' ? 1 : false;
$scope.$watch('afform', function () {
<i ng-if="entity.loading" class="crm-i fa-spin fa-spinner"></i>
</a>
</li>
+ <li role="presentation" ng-repeat="(key, searchDisplay) in editor.meta.searchDisplays" ng-class="{active: selectedEntityName === key}">
+ <a href ng-click="editor.selectEntity(key)">
+ <span>{{ searchDisplay.label }}</span>
+ </a>
+ </li>
<li role="presentation" class="dropdown" ng-if="editor.allowEntityConfig">
<a href class="dropdown-toggle" data-toggle="dropdown" title="{{ ts('Add Entity') }}">
<span><i class="crm-i fa-plus"></i></span>
<div class="panel-body" ng-repeat="entity in entities" ng-if="selectedEntityName === entity.name">
<af-gui-entity entity="entity"></af-gui-entity>
</div>
+ <div class="panel-body" ng-repeat="(key, searchDisplay) in editor.meta.searchDisplays" ng-if="selectedEntityName === key">
+ <af-gui-search display="searchDisplay"></af-gui-search>
+ </div>
</div>
--- /dev/null
+// https://civicrm.org/licensing
+(function(angular, $, _) {
+ "use strict";
+
+ angular.module('afGuiEditor').component('afGuiSearch', {
+ templateUrl: '~/afGuiEditor/afGuiSearch.html',
+ bindings: {
+ display: '<'
+ },
+ require: {editor: '^^afGuiEditor'},
+ controller: function ($scope, $timeout, afGui) {
+ var ts = $scope.ts = CRM.ts();
+ var ctrl = this;
+ $scope.controls = {};
+ $scope.fieldList = [];
+ $scope.elementList = [];
+ $scope.elementTitles = [];
+
+ $scope.getField = afGui.getField;
+
+ function buildPaletteLists() {
+ var search = $scope.controls.fieldSearch ? $scope.controls.fieldSearch.toLowerCase() : null;
+ buildFieldList(search);
+ buildElementList(search);
+ }
+
+ function buildFieldList(search) {
+ $scope.fieldList.length = 0;
+ var entity = afGui.getEntity(ctrl.display['saved_search.api_entity']),
+ entityCount = {};
+ entityCount[entity.entity] = 1;
+ $scope.fieldList.push({
+ entityType: entity.entity,
+ label: ts('%1 Fields', {1: entity.label}),
+ fields: filterFields(entity.fields)
+ });
+
+ _.each(ctrl.display['saved_search.api_params'].join, function(join) {
+ var joinInfo = join[0].split(' AS '),
+ entity = afGui.getEntity(joinInfo[0]),
+ alias = joinInfo[1];
+ entityCount[entity.entity] = entityCount[entity.entity] ? entityCount[entity.entity] + 1 : 1;
+ $scope.fieldList.push({
+ entityType: entity.entity,
+ label: ts('%1 Fields', {1: entity.label + (entityCount[entity.entity] > 1 ? ' ' + entityCount[entity.entity] : '')}),
+ fields: filterFields(entity.fields, alias)
+ });
+ });
+
+ function filterFields(fields, prefix) {
+ return _.transform(fields, function(fieldList, field) {
+ if (!search || _.contains(field.name, search) || _.contains(field.label.toLowerCase(), search)) {
+ fieldList.push({
+ "#tag": "af-field",
+ name: (prefix ? prefix + '.' : '') + field.name
+ });
+ }
+ }, []);
+ }
+ }
+
+ function buildElementList(search) {
+ $scope.elementList.length = 0;
+ $scope.elementTitles.length = 0;
+ _.each(afGui.meta.elements, function(element, name) {
+ if (!search || _.contains(name, search) || _.contains(element.title.toLowerCase(), search)) {
+ var node = _.cloneDeep(element.element);
+ if (name === 'fieldset') {
+ return;
+ }
+ $scope.elementList.push(node);
+ $scope.elementTitles.push(element.title);
+ }
+ });
+ }
+
+ $scope.clearSearch = function() {
+ $scope.controls.fieldSearch = '';
+ };
+
+ // This gets called from jquery-ui so we have to manually apply changes to scope
+ $scope.buildPaletteLists = function() {
+ $timeout(function() {
+ $scope.$apply(function() {
+ buildPaletteLists();
+ });
+ });
+ };
+
+ // Checks if a field is on the form or set as a value
+ $scope.fieldInUse = function(fieldName) {
+ return check(ctrl.editor.layout['#children'], {'#tag': 'af-field', name: fieldName});
+ };
+
+ // Check for a matching item for this entity
+ // Recursively checks the form layout, including block directives
+ function check(group, criteria, found) {
+ if (!found) {
+ found = {};
+ }
+ if (_.find(group, criteria)) {
+ found.match = true;
+ return true;
+ }
+ _.each(group, function(item) {
+ if (found.match) {
+ return false;
+ }
+ if (_.isPlainObject(item)) {
+ // Recurse through everything
+ if (item['#children']) {
+ check(item['#children'], criteria, found);
+ }
+ // Recurse into block directives
+ else if (item['#tag'] && item['#tag'] in afGui.meta.blocks) {
+ check(afGui.meta.blocks[item['#tag']].layout, criteria, found);
+ }
+ }
+ });
+ return found.match;
+ }
+
+ $scope.$watch('controls.fieldSearch', buildPaletteLists);
+ }
+ });
+
+})(angular, CRM.$, CRM._);
--- /dev/null
+<div>
+ <fieldset class="af-gui-entity-palette">
+ <legend class="form-inline">
+ {{:: ts('Add:') }}
+ <input ng-model="controls.fieldSearch" class="form-control" type="search" placeholder="" title="{{:: ts('Search fields') }}" />
+ </legend>
+ <div class="af-gui-entity-palette-select-list">
+ <div ng-if="elementList.length">
+ <label>{{:: ts('Elements') }}</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="elementList">
+ <div ng-repeat="element in elementList" >
+ {{:: elementTitles[$index] }}
+ </div>
+ </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 ng-repeat="field in fieldGroup.fields" ng-class="{disabled: fieldInUse(field.name)}">
+ {{:: getField(fieldGroup.entityType, field.name).label }}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </fieldset>
+</div>
if (node['#tag'] === 'af-field') {
return 'field';
}
- if (node['af-fieldset']) {
+ if ('af-fieldset' in node) {
return 'fieldset';
}
if (node['af-join']) {
if (node['#tag'] && node['#tag'] in afGui.meta.blocks) {
return 'container';
}
+ if (node['#tag'] && (node['#tag'].slice(0, 19) === 'crm-search-display-')) {
+ return 'searchDisplay';
+ }
var classes = afGui.splitClass(node['class']),
types = ['af-container', 'af-text', 'af-button', 'af-markup'],
type = _.intersection(types, classes);
};
this.getEntityName = function() {
- return ctrl.entityName.split('-join-')[0];
+ return ctrl.entityName ? ctrl.entityName.split('-join-')[0] : null;
};
// Returns the primary entity type for this container e.g. "Contact"
};
// Returns the entity type for fields within this conainer (join entity type if this is a join, else the primary entity type)
- this.getFieldEntityType = function() {
- var joinType = ctrl.entityName.split('-join-');
- return joinType[1] || (ctrl.editor && ctrl.editor.getEntity(joinType[0]).type);
+ this.getFieldEntityType = function(fieldName) {
+ // If entityName is declared for this fieldset, return entity-type or join-type
+ if (ctrl.entityName) {
+ var joinType = ctrl.entityName.split('-join-');
+ return joinType[1] || (ctrl.editor && ctrl.editor.getEntity(joinType[0]).type);
+ }
+ // If entityName is not declared, this field belongs to a search
+ var entityType,
+ prefix = _.includes(fieldName, '.') ? fieldName.split('.')[0] : null;
+ _.each(afGui.meta.searchDisplays, function(searchDisplay) {
+ if (prefix) {
+ _.each(searchDisplay['saved_search.api_params'].join, function(join) {
+ var joinInfo = join[0].split(' AS ');
+ if (prefix === joinInfo[1]) {
+ entityType = joinInfo[0];
+ return false;
+ }
+ });
+ }
+ if (!entityType && afGui.getField(searchDisplay['saved_search.api_entity'], fieldName)) {
+ entityType = searchDisplay['saved_search.api_entity'];
+ }
+ if (entityType) {
+ return false;
+ }
+ });
+ return entityType;
};
}
<div ng-if="!$ctrl.loading" class="form-inline" af-gui-menu>
<span ng-if="$ctrl.getNodeType($ctrl.node) == 'fieldset'">{{ $ctrl.editor.getEntity($ctrl.entityName).label }}</span>
<span ng-if="block">{{ $ctrl.join ? ts($ctrl.join) + ':' : ts('Block:') }}</span>
- <span ng-if="!block">{{ tags[$ctrl.node['#tag']].toLowerCase() }}</span>
+ <span ng-if="!block">{{ tags[$ctrl.node['#tag']] }}</span>
<select ng-if="block" ng-model="block.directive" ng-change="selectBlockDirective()">
<option value="">{{:: ts('Custom') }}</option>
<option ng-value="option.id" ng-repeat="option in block.options track by option.id">{{ option.text }}</option>
<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-search-display ng-switch-when="searchDisplay" node="item" class="af-gui-element"></af-gui-search-display>
</div>
</div>
</div>
$scope.meta = afGui.meta;
};
- $scope.getEntity = function() {
- return ctrl.editor ? ctrl.editor.getEntity(ctrl.container.getEntityName()) : {};
+ // $scope.getEntity = function() {
+ // return ctrl.editor ? ctrl.editor.getEntity(ctrl.container.getEntityName()) : {};
+ // };
+
+ // Returns the original field definition from metadata
+ this.getDefn = function() {
+ return ctrl.editor ? afGui.getField(ctrl.container.getFieldEntityType(ctrl.node.name), ctrl.node.name) : {};
};
- $scope.getDefn = this.getDefn = function() {
- return ctrl.editor ? afGui.getField(ctrl.container.getFieldEntityType(), ctrl.node.name) : {};
+ $scope.getOriginalLabel = function() {
+ if (ctrl.container.getEntityName()) {
+ return ctrl.editor.getEntity(ctrl.container.getEntityName()).label + ': ' + ctrl.getDefn().label;
+ }
+ return afGui.getEntity(ctrl.container.getFieldEntityType(ctrl.node.name)).label + ': ' + ctrl.getDefn().label;
};
$scope.hasOptions = function() {
var inputType = $scope.getProp('input_type');
- return _.contains(['CheckBox', 'Radio', 'Select'], inputType) && !(inputType === 'CheckBox' && !$scope.getDefn().options);
+ return _.contains(['CheckBox', 'Radio', 'Select'], inputType) && !(inputType === 'CheckBox' && !ctrl.getDefn().options);
};
$scope.getOptions = this.getOptions = function() {
if (ctrl.node.defn && ctrl.node.defn.options) {
return ctrl.node.defn.options;
}
- return $scope.getDefn().options || ($scope.getProp('input_type') === 'CheckBox' ? null : yesNo);
+ return ctrl.getDefn().options || ($scope.getProp('input_type') === 'CheckBox' ? null : yesNo);
};
$scope.resetOptions = function() {
};
$scope.inputTypeCanBe = function(type) {
- var defn = $scope.getDefn();
+ var defn = ctrl.getDefn();
switch (type) {
case 'CheckBox':
case 'Radio':
if (typeof localDefn[item] !== 'undefined') {
return localDefn[item];
}
- return drillDown($scope.getDefn(), path)[item];
+ return drillDown(ctrl.getDefn(), path)[item];
};
// Checks for a value in either the local field defn or the base defn
};
$scope.toggleHelp = function(position) {
- getSet('help_' + position, $scope.propIsset('help_' + position) ? null : ($scope.getDefn()['help_' + position] || ts('Enter text')));
+ getSet('help_' + position, $scope.propIsset('help_' + position) ? null : (ctrl.getDefn()['help_' + position] || ts('Enter text')));
return false;
};
var path = propName.split('.'),
item = path.pop(),
localDefn = drillDown(ctrl.node, ['defn'].concat(path)),
- fieldDefn = drillDown($scope.getDefn(), path);
+ fieldDefn = drillDown(ctrl.getDefn(), path);
// Set the value if different than the field defn, otherwise unset it
if (typeof val !== 'undefined' && (val !== fieldDefn[item] && !(!val && !fieldDefn[item]))) {
localDefn[item] = val;
<af-gui-edit-options ng-if="editingOptions" class="af-gui-content-editing-area"></af-gui-edit-options>
<div ng-if="!editingOptions" class="af-gui-element af-gui-field" >
- <div class="af-gui-bar" title="{{ getEntity().label + ': ' + getDefn().label }}">
- <div class="form-inline pull-right">
- <div class="btn-group" af-gui-menu >
+ <div class="af-gui-bar" title="{{:: getOriginalLabel() }}">
+ <div class="form-inline">
+ <span ng-if="$ctrl.node.defn.label === false">{{:: getOriginalLabel() }}</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>
</button>
</div>
</div>
<label ng-style="{visibility: $ctrl.node.defn.label === false ? 'hidden' : 'visible'}" ng-class="{'af-gui-field-required': getProp('required')}" class="af-gui-node-title">
- <span af-gui-editable ng-model="getSet('label')" ng-model-options="{getterSetter: true}" default-value="getDefn().label">{{ getProp('label') }}</span>
+ <span af-gui-editable ng-model="getSet('label')" ng-model-options="{getterSetter: true}" default-value="$ctrl.getDefn().label">{{ getProp('label') }}</span>
</label>
<div class="af-gui-field-help" ng-if="propIsset('help_pre')">
- <span af-gui-editable ng-model="getSet('help_pre')" ng-model-options="{getterSetter: true}" default-value="getDefn().help_pre">{{ getProp('help_pre') }}</span>
+ <span af-gui-editable ng-model="getSet('help_pre')" ng-model-options="{getterSetter: true}" default-value="$ctrl.getDefn().help_pre">{{ getProp('help_pre') }}</span>
</div>
<div class="af-gui-field-input af-gui-field-input-type-{{ getProp('input_type').toLowerCase() }}" ng-include="'~/afGuiEditor/inputType/' + getProp('input_type') + '.html'"></div>
<div class="af-gui-field-help" ng-if="propIsset('help_post')">
- <span af-gui-editable ng-model="getSet('help_post')" ng-model-options="{getterSetter: true}" default-value="getDefn().help_post">{{ getProp('help_post') }}</span>
+ <span af-gui-editable ng-model="getSet('help_post')" ng-model-options="{getterSetter: true}" default-value="$ctrl.getDefn().help_post">{{ getProp('help_post') }}</span>
</div>
</div>
--- /dev/null
+// https://civicrm.org/licensing
+(function(angular, $, _) {
+ "use strict";
+
+ angular.module('afGuiEditor').component('afGuiSearchDisplay', {
+ templateUrl: '~/afGuiEditor/elements/afGuiSearchDisplay.html',
+ bindings: {
+ node: '='
+ },
+ controller: function($scope, afGui) {
+ var ts = $scope.ts = CRM.ts(),
+ ctrl = this;
+
+ this.$onInit = function() {
+ ctrl.display = afGui.meta.searchDisplays[ctrl.node['search-name'] + '.' + ctrl.node['display-name']];
+ };
+
+ }
+ });
+
+})(angular, CRM.$, CRM._);
--- /dev/null
+<div class="af-gui-bar">
+ <div class="form-inline">
+ <span>{{ $ctrl.display.label }}</span>
+ </div>
+</div>
+<p class="text-center af-gui-search-display">
+ <i class="crm-i fa-3x {{ $ctrl.display['type:icon'] }}"></i>
+</p>
'operators' => \CRM_Utils_Array::makeNonAssociative(self::getOperators()),
'functions' => \CRM_Api4_Page_Api4Explorer::getSqlFunctions(),
'displayTypes' => Display::getDisplayTypes(['id', 'name', 'label', 'description', 'icon']),
+ 'afformEnabled' => (bool) \CRM_Utils_Array::findAll(
+ \CRM_Extension_System::singleton()->getMapper()->getActiveModuleFiles(),
+ ['fullName' => 'org.civicrm.afform']
+ ),
+ 'afformAdminEnabled' => (bool) \CRM_Utils_Array::findAll(
+ \CRM_Extension_System::singleton()->getMapper()->getActiveModuleFiles(),
+ ['fullName' => 'org.civicrm.afform_admin']
+ ),
];
}
var ts = $scope.ts = CRM.ts(),
ctrl = $scope.$ctrl = this;
this.savedSearches = savedSearches;
+ this.afformEnabled = CRM.crmSearchAdmin.afformEnabled;
+ this.afformAdminEnabled = CRM.crmSearchAdmin.afformAdminEnabled;
+
this.entityTitles = _.transform(CRM.crmSearchAdmin.schema, function(titles, entity) {
titles[entity.name] = entity.title_plural;
}, {});
- this.searchPath = window.location.href.split('#')[0].replace('civicrm/admin/search', 'civicrm/search');
+ this.searchPath = CRM.url('civicrm/search');
+ this.newFormPath = CRM.url('civicrm/admin/afform');
this.encode = function(params) {
return encodeURI(angular.toJson(params));
savedSearches.splice(index, 1);
}
};
+
+ this.loadAfforms = function() {
+ if (ctrl.afforms || ctrl.afforms === null) {
+ return;
+ }
+ ctrl.afforms = null;
+ crmApi4('Afform', 'get', {
+ select: ['layout', 'name', 'title', 'server_route'],
+ where: [['type', '=', 'search']],
+ layoutFormat: 'html'
+ }).then(function(afforms) {
+ ctrl.afforms = {};
+ _.each(afforms, function(afform) {
+ var searchName = afform.layout.match(/<crm-search-display-[^>]+search-name[ ]*=[ ]*['"]([^"']+)/);
+ if (searchName) {
+ ctrl.afforms[searchName[1]] = ctrl.afforms[searchName[1]] || [];
+ ctrl.afforms[searchName[1]].push({
+ title: afform.title,
+ url: afform.server_route ? CRM.url(afform.server_route) : null
+ });
+ }
+ });
+ });
+ };
+
});
})(angular, CRM.$, CRM._);
<div id="bootstrap-theme" class="crm-search">
<h1 crm-page-title>{{:: ts('Saved Searches') }}</h1>
<div class="form-inline">
+ <label for="search-list-filter">{{:: ts('Filter:') }}</label>
+ <input class="form-control" type="search" id="search-list-filter" ng-model="$ctrl.searchFilter" placeholder="">
<a class="btn btn-primary pull-right" href="#/create/Contact/">
<i class="crm-i fa-plus"></i>
{{:: ts('New Search') }}
<th>{{:: ts('For') }}</th>
<th>{{:: ts('Displays') }}</th>
<th>{{:: ts('Smart Group') }}</th>
+ <th ng-if="$ctrl.afformEnabled">{{:: ts('Forms') }}</th>
<th></th>
</tr>
</thead>
<tbody>
- <tr ng-repeat="search in $ctrl.savedSearches">
+ <tr ng-repeat="search in $ctrl.savedSearches | filter:$ctrl.searchFilter">
<td>{{ search.id }}</td>
<td>{{ search.label }}</td>
<td>{{ $ctrl.entityTitles[search.api_entity] }}</td>
</button>
<ul class="dropdown-menu" ng-if=":: search.display_name.length">
<li ng-repeat="display_name in search.display_name">
- <a href="{{:: $ctrl.searchPath + '#/display/' + search.name + '/' + display_name }}"><i class="fa {{:: search.display_icon[$index] }}"></i> {{:: search.display_label[$index] }}</a>
+ <a href="{{:: $ctrl.searchPath + '#/display/' + search.name + '/' + display_name }}" target="_blank">
+ <i class="fa {{:: search.display_icon[$index] }}"></i>
+ {{:: search.display_label[$index] }}
+ </a>
</li>
</ul>
</div>
</td>
<td>{{ search.groups.join(', ') }}</td>
+ <td ng-if="$ctrl.afformEnabled">
+ <div class="btn-group">
+ <button type="button" ng-click="$ctrl.loadAfforms()" ng-if="search.display_name" class="btn btn-xs dropdown-toggle btn-primary-outline" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+ {{:: ts('Forms') }} <span class="caret"></span>
+ </button>
+ <ul class="dropdown-menu">
+ <li ng-repeat="display_name in search.display_name" ng-if="::$ctrl.afformAdminEnabled">
+ <a href="{{:: $ctrl.newFormPath + '#/create/search/' + search.name + '.' + display_name }}">
+ <i class="fa fa-plus"></i> {{:: ts('Create form for %1', {1: search.display_label[$index]}) }}
+ </a>
+ </li>
+ <li class="divider" role="separator" ng-if="::$ctrl.afformAdminEnabled"></li>
+ <li ng-if="!$ctrl.afforms || !$ctrl.afforms[search.name]" class="disabled">
+ <a href>
+ <i ng-if="!$ctrl.afforms" class="crm-i fa-spinner fa-spin"></i>
+ <em ng-if="$ctrl.afforms && !$ctrl.afforms[search.name]">{{:: ts('None Found') }}</em>
+ </a>
+ </li>
+ <li ng-if="$ctrl.afforms" ng-repeat="afform in $ctrl.afforms[search.name]" ng-class="{disabled: !afform.url}">
+ <a href="{{:: afform.url }}" target="_blank">
+ {{:: afform.title }}
+ </a>
+ </li>
+ </ul>
+ </div>
+ </td>
<td class="text-right">
<a class="btn btn-xs btn-default" href="#/edit/{{:: search.id }}">{{:: ts('Edit') }}</a>
<a class="btn btn-xs btn-default" href="#/create/{{:: search.api_entity + '?params=' + $ctrl.encode(search.api_params) }}">{{:: ts('Clone') }}</a>
right: 0;
top: 0;
}
+
+#bootstrap-theme input[type=search]::placeholder {
+ font-family: FontAwesome;
+ text-align: right;
+}
+#bootstrap-theme input[type=search]:-ms-input-placeholder {
+ font-family: FontAwesome;
+ text-align: right;
+}
+#bootstrap-theme input[type=search]::-ms-input-placeholder {
+ font-family: FontAwesome;
+ text-align: right;
+}