$scope.getField = afGui.getField;
+ // Live results for the select2 of filter fields
+ this.getFilterFields = function() {
+ var fieldGroups = [],
+ entities = getEntities();
+ if (ctrl.display.calc_fields && ctrl.display.calc_fields.length) {
+ fieldGroups.push({
+ text: ts('Calculated Fields'),
+ children: _.transform(ctrl.display.calc_fields, function(fields, el) {
+ fields.push({id: el.name, text: el.defn.label, disabled: ctrl.fieldInUse(el.name)});
+ }, [])
+ });
+ }
+ _.each(entities, function(entity) {
+ fieldGroups.push({
+ text: entity.label,
+ children: _.transform(entity.fields, function(fields, field) {
+ fields.push({id: entity.prefix + field.name, text: entity.label + ' ' + field.label, disabled: ctrl.fieldInUse(entity.prefix + field.name)});
+ }, [])
+ });
+ });
+ return {results: fieldGroups};
+ };
+
this.buildPaletteLists = function() {
var search = $scope.controls.fieldSearch ? $scope.controls.fieldSearch.toLowerCase() : null;
buildCalcFieldList(search);
buildElementList(search);
};
+ // Gets the name of the entity a field belongs to
+ this.getFieldEntity = function(fieldName) {
+ if (fieldName.indexOf('.') < 0) {
+ return ctrl.display['saved_search_id.api_entity'];
+ }
+ var alias = fieldName.split('.')[0],
+ entity;
+ _.each(ctrl.display['saved_search_id.api_params'].join, function(join) {
+ var joinInfo = join[0].split(' AS ');
+ if (alias === joinInfo[1]) {
+ entity = joinInfo[0];
+ return false;
+ }
+ });
+ return entity || ctrl.display['saved_search_id.api_entity'];
+ };
+
function buildCalcFieldList(search) {
$scope.calcFieldList.length = 0;
_.each(_.cloneDeep(ctrl.display.calc_fields), function(field) {
});
}
- function buildFieldList(search) {
- $scope.fieldList.length = 0;
- var entity = afGui.getEntity(ctrl.display['saved_search_id.api_entity']),
- entityCount = {};
- entityCount[entity.entity] = 1;
- $scope.fieldList.push({
- entityType: entity.entity,
- label: ts('%1 Fields', {1: entity.label}),
- fields: filterFields(entity.fields)
- });
+ // Fetch all entities used in search (main entity + joins)
+ function getEntities() {
+ var
+ mainEntity = afGui.getEntity(ctrl.display['saved_search_id.api_entity']),
+ entityCount = {},
+ entities = [{
+ name: mainEntity.entity,
+ prefix: '',
+ label: mainEntity.label,
+ fields: mainEntity.fields
+ }];
+ entityCount[mainEntity.entity] = 1;
_.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];
+ entity = afGui.getEntity(joinInfo[0]);
entityCount[entity.entity] = (entityCount[entity.entity] || 0) + 1;
+ entities.push({
+ name: entity.entity,
+ prefix: joinInfo[1] + '.',
+ label: entity.label + (entityCount[entity.entity] > 1 ? ' ' + entityCount[entity.entity] : ''),
+ fields: entity.fields,
+ });
+ });
+
+ return entities;
+ }
+
+ function buildFieldList(search) {
+ $scope.fieldList.length = 0;
+ var entities = getEntities();
+ _.each(entities, function(entity) {
$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)
+ entityType: entity.name,
+ label: ts('%1 Fields', {1: entity.label}),
+ fields: filterFields(entity.fields, entity.prefix)
});
});
function fieldDefaults(field, prefix) {
var tag = {
"#tag": "af-field",
- name: (prefix ? prefix + '.' : '') + field.name
+ name: prefix + field.name
};
if (field.input_type === 'Select' || field.input_type === 'ChainSelect') {
tag.defn = {input_attrs: {multiple: true}};
});
};
- // 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});
+ // Checks if a field is on the form or set as a filter
+ this.fieldInUse = function(fieldName) {
+ if (_.findIndex(ctrl.filters, {name: fieldName}) >= 0) {
+ return true;
+ }
+ return !!getElement(ctrl.editor.layout['#children'], {'#tag': 'af-field', name: fieldName});
};
// Checks if fields in a block are already in use on the form.
// Note that if a block contains no fields it can be used repeatedly, so this will always return false for those.
$scope.blockInUse = function(block) {
if (block['af-join']) {
- return check(ctrl.editor.layout['#children'], {'af-join': block['af-join']});
+ return !!getElement(ctrl.editor.layout['#children'], {'af-join': block['af-join']});
}
var fieldsInBlock = _.pluck(afGui.findRecursive(afGui.meta.blocks[block['#tag']].layout, {'#tag': 'af-field'}), 'name');
- return check(ctrl.editor.layout['#children'], function(item) {
+ return !!getElement(ctrl.editor.layout['#children'], function(item) {
return item['#tag'] === 'af-field' && _.includes(fieldsInBlock, item.name);
});
};
- // Check for a matching item for this entity
+ function getSearchDisplayElement() {
+ return getElement(ctrl.editor.layout['#children'], {'#tag': ctrl.display['type:name'], 'display-name': ctrl.display.name, 'search-name': ctrl.display['saved_search_id.name']});
+ }
+
+ // Return an item matching criteria
// Recursively checks the form layout, including block directives
- function check(group, criteria, found) {
+ function getElement(group, criteria, found) {
if (!found) {
found = {};
}
- if (_.find(group, criteria)) {
- found.match = true;
- return true;
+ var match = _.find(group, criteria);
+ if (match) {
+ found.match = match;
+ return match;
}
_.each(group, function(item) {
if (found.match) {
if (_.isPlainObject(item)) {
// Recurse through everything
if (item['#children']) {
- check(item['#children'], criteria, found);
+ getElement(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);
+ getElement(afGui.meta.blocks[item['#tag']].layout, criteria, found);
}
}
});
return found.match;
}
+ function filtersToArray() {
+ return _.transform(ctrl.display.filters, function(result, value, key) {
+ var info = {
+ name: key,
+ mode: value.indexOf('routeParams') === 0 ? 'url' : 'val'
+ };
+ // Object dot notation
+ if (info.mode === 'url' && value.indexOf('routeParams.') === 0) {
+ info.value = value.replace('routeParams.', '');
+ }
+ // Object bracket notation
+ else if (info.mode === 'url') {
+ info.value = decode(value.substring(value.indexOf('[') + 1, value.lastIndexOf(']')));
+ }
+ // Literal value
+ else {
+ info.value = decode(value);
+ }
+ result.push(info);
+ }, []);
+ }
+
+ // Convert javascript notation to value
+ function decode(encoded) {
+ // Single-quoted string
+ if (encoded.indexOf("'") === 0 && encoded.charAt(encoded.length - 1) === "'") {
+ return encoded.substring(1, encoded.length - 1);
+ }
+ // Anything else
+ return JSON.parse(encoded);
+ }
+
+ // Convert value to javascript notation
+ function encode(value) {
+ var encoded = JSON.stringify(value),
+ split = encoded.split('"');
+ // Convert double-quotes to single-quotes if possible
+ if (split.length === 3 && split[0] === '' && split[2] === '' && encoded.indexOf("'") < 0) {
+ return "'" + split[1] + "'";
+ }
+ return encoded;
+ }
+
+ // Append a search filter
+ this.addFilter = function(fieldName) {
+ ctrl.filters.push({
+ name: fieldName,
+ value: fieldName,
+ mode: 'url'
+ });
+ };
+
+ // Respond to changing a filter field name
+ this.onChangeFilter = function(index) {
+ var filter = ctrl.filters[index];
+ if (filter.name) {
+ filter.mode = 'url';
+ filter.value = filter.name;
+ } else {
+ ctrl.filters.splice(index, 1);
+ }
+ };
+
+ // Convert filters array to js notation & add to crm-search-display element
+ function writeFilters() {
+ var element = getSearchDisplayElement(),
+ output = [];
+ if (!ctrl.filters.length) {
+ delete element.filters;
+ return;
+ }
+ _.each(ctrl.filters, function(filter) {
+ var keyVal = [
+ // Enclose the key in quotes unless it is purely alphanumeric
+ filter.name.match(/\W/) ? encode(filter.name) : filter.name,
+ ];
+ // Object dot notation
+ if (filter.mode === 'url' && !filter.value.match(/\W/)) {
+ keyVal.push('routeParams.' + filter.value);
+ }
+ // Object bracket notation
+ else if (filter.mode === 'url') {
+ keyVal.push('routeParams[' + encode(filter.value) + ']');
+ }
+ // Literal value
+ else {
+ keyVal.push(encode(filter.value));
+ }
+ output.push(keyVal.join(': '));
+ });
+ element.filters = '{' + output.join(', ') + '}';
+ }
+
this.$onInit = function() {
- // When a new block is saved, update the list
this.meta = afGui.meta;
+ this.filters = filtersToArray();
+ $scope.$watch('$ctrl.filters', writeFilters, true);
+ // When a new block is saved, update the list
$scope.$watchCollection('$ctrl.meta.blocks', function() {
$scope.controls.fieldSearch = '';
ctrl.buildPaletteLists();
-<div>
+<div class="af-gui-columns crm-flex-box">
+ <fieldset class="af-gui-entity-values">
+ <legend>{{:: ts('Filters:') }}</legend>
+ <div class="form-inline" ng-repeat="filter in $ctrl.filters">
+ <input class="form-control" ng-model="filter.name" ng-change="$ctrl.onChangeFilter($index)" crm-ui-select="{data: $ctrl.getFilterFields, placeholder: ' '}" />
+ <div class="input-group">
+ <div class="input-group-btn">
+ <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+ {{ filter.mode === 'url' ? ts('Url') : ts('Value') }}
+ <span class="caret"></span>
+ </button>
+ <ul class="dropdown-menu">
+ <li>
+ <a href ng-click="$ctrl.onChangeFilter($index)">{{:: ts('Url variable') }}</a>
+ </li>
+ <li>
+ <a href ng-click="filter.mode = 'val'; filter.value = ''">{{:: ts('Fixed value') }}</a>
+ </li>
+ </ul>
+ </div>
+ <input ng-if="filter.mode === 'url'" class="form-control" ng-model="filter.value" />
+ <span ng-if="filter.mode === 'val'">
+ <input class="form-control" af-gui-field-value="getField($ctrl.getFieldEntity(filter.name), filter.name)" ng-model="filter.value" />
+ </span>
+ </div>
+ </div>
+ <hr />
+ <div class="form-inline">
+ <input class="form-control" on-crm-ui-select="$ctrl.addFilter(selection)" crm-ui-select="{data: $ctrl.getFilterFields, placeholder: ts('Add filter')}" />
+ </div>
+ </fieldset>
+
<fieldset class="af-gui-entity-palette">
<legend class="form-inline">
{{:: ts('Add:') }}
<div ng-if="calcFieldList.length">
<label>{{:: ts('Calculated Fields') }}</label>
<div ui-sortable="$ctrl.editor.getSortableOptions()" ui-sortable-update="buildPaletteLists" ng-model="calcFieldList">
- <div ng-repeat="field in calcFieldList" ng-class="{disabled: fieldInUse(field.name)}">
+ <div ng-repeat="field in calcFieldList" ng-class="{disabled: $ctrl.fieldInUse(field.name)}">
<div class="af-gui-palette-item">{{:: field.defn.label }}</div>
</div>
</div>
<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)}">
+ <div ng-repeat="field in fieldGroup.fields" ng-class="{disabled: $ctrl.fieldInUse(field.name)}">
{{:: getField(fieldGroup.entityType, field.name).label }}
</div>
</div>