}
return [
'afform_type' => $afformTypes,
+ 'search_operators' => \Civi\Afform\Utils::getSearchOperators(),
];
}
'checkPermissions' => FALSE,
'loadOptions' => ['id', 'label'],
'action' => 'create',
- 'select' => ['name', 'label', 'input_type', 'input_attrs', 'required', 'options', 'help_pre', 'help_post', 'serialize', 'data_type', 'entity', 'fk_entity', 'readonly'],
+ 'select' => ['name', 'label', 'input_type', 'input_attrs', 'required', 'options', 'help_pre', 'help_post', 'serialize', 'data_type', 'entity', 'fk_entity', 'readonly', 'operators'],
'where' => [['deprecated', '=', FALSE], ['input_type', 'IS NOT NULL']],
];
if (in_array($entityName, \CRM_Contact_BAO_ContactType::basicTypes(TRUE), TRUE)) {
{{:: ts('Search by range') }}
</a>
</li>
-<li ng-if="$ctrl.isSearch()">
- <div href ng-click="$event.stopPropagation()" class="af-gui-field-select-in-dropdown">
+<li ng-if="$ctrl.isSearch()" ng-click="$event.stopPropagation()">
+ <div href class="af-gui-field-select-in-dropdown">
<label>{{:: ts('Operator:') }}</label>
- <select class="form-control" ng-model="getSet('search_operator')" ng-model-options="{getterSetter: true}" title="{{:: ts('Field type') }}">
+ <select class="form-control" ng-model="getSetOperator" ng-model-options="{getterSetter: true}" title="{{:: ts('Set the search operator for this field or allow the user to select it on the form') }}">
+ <option value="">{{:: ts('Auto') }}</option>
+ <option value="_EXPOSE_">{{:: ts('User Select') }}</option>
+ <option ng-repeat="(name, label) in $ctrl.searchOperators" value="{{ name }}">{{ label }}</option>
+ </select>
+ </div>
+ <div href class="af-gui-field-select-in-dropdown" ng-if="$ctrl.getSet('expose_operator')">
+ <label>{{:: ts('Default:') }}</label>
+ <select class="form-control" ng-model="getSet('search_operator')" ng-model-options="{getterSetter: true}" title="{{:: ts('Default search operator for the user to select') }}">
<option ng-repeat="(name, label) in $ctrl.searchOperators" value="{{ name }}">{{ label }}</option>
</select>
</div>
inputTypes.push(type);
}
});
+ this.searchOperators = CRM.afAdmin.search_operators;
+ // If field has limited operators, set appropriately
+ if (ctrl.fieldDefn.operators && ctrl.fieldDefn.operators.length) {
+ this.searchOperators = _.pick(this.searchOperators, ctrl.fieldDefn.operators);
+ }
setDateOptions();
};
}
};
- // Getter/setter for definition props
+ // Getter/setter for search_operator and expose_operator combo-field
+ // The expose_operator flag changes the behavior of the search_operator field
+ // to either set the value on the backend, or set the default value for the user-select list on the form
+ $scope.getSetOperator = function(val) {
+ if (arguments.length) {
+ // _EXPOSE_ is not a real option for search_operator, instead it sets the expose_operator boolean
+ getSet('expose_operator', val === '_EXPOSE_');
+ if (val === '_EXPOSE_') {
+ getSet('search_operator', _.keys(ctrl.searchOperators)[0]);
+ } else {
+ getSet('search_operator', val);
+ }
+ return val;
+ }
+ return getSet('expose_operator') ? '_EXPOSE_' : getSet('search_operator');
+ };
+
+ // Generic getter/setter for definition props
$scope.getSet = function(propName) {
return _.wrap(propName, getSet);
};
$scope.editingOptions = val;
};
- this.searchOperators = {
- '': ts('Auto'),
- '=': '=',
- '!=': '≠',
- '>': '>',
- '<': '<',
- '>=': '≥',
- '<=': '≤',
- 'CONTAINS': ts('Contains'),
- 'NOT CONTAINS': ts("Doesn't Contain"),
- 'IN': ts('Is One Of'),
- 'NOT IN': ts('Not One Of'),
- 'LIKE': ts('Is Like'),
- 'NOT LIKE': ts('Not Like'),
- 'REGEXP': ts('Matches Pattern'),
- 'NOT REGEXP': ts("Doesn't Match Pattern"),
- };
-
// Returns a reference to a path n-levels deep within an object
function drillDown(parent, path) {
var container = parent;
// On a search form, search_range will present a pair of fields (or possibly 3 fields for date select + range)
$isSearchRange = !empty($fieldDefn['search_range']) && \CRM_Utils_JS::decode($fieldDefn['search_range']);
+ // On a search form, the exposed operator requires a list of options.
+ if (!empty($fieldDefn['expose_operator'])) {
+ $operators = Utils::getSearchOperators();
+ // If 'operators' is present in the field definition, use it as a limiter
+ // Afform expects 'operators' in the fieldDefn to be associative key/label, not just a flat array
+ // like it is in the schema.
+ if (!empty($fieldInfo['operators'])) {
+ $operators = array_intersect_key($operators, array_flip($fieldInfo['operators']));
+ }
+ $fieldDefn['operators'] = \CRM_Utils_JS::encode($operators);
+ }
+ unset($fieldInfo['operators']);
+
// Default placeholder for select inputs
if ($inputType === 'Select' || $inputType === 'ChainSelect') {
$fieldInfo['input_attrs']['placeholder'] = E::ts('Select');
if ($action === 'get' && strpos($fieldName, '.')) {
$namesToMatch[] = substr($fieldName, 0, strrpos($fieldName, '.'));
}
+ $select = ['name', 'label', 'input_type', 'input_attrs', 'help_pre', 'help_post', 'options', 'fk_entity', 'required'];
+ if ($action === 'get') {
+ $select[] = 'operators';
+ }
$params = [
'action' => $action,
'where' => [['name', 'IN', $namesToMatch]],
- 'select' => ['name', 'label', 'input_type', 'input_attrs', 'help_pre', 'help_post', 'options', 'fk_entity', 'required'],
+ 'select' => $select,
'loadOptions' => ['id', 'label'],
// If the admin included this field on the form, then it's OK to get metadata about the field regardless of user permissions.
'checkPermissions' => FALSE,
return $sorter->sort();
}
+ /**
+ * Subset of APIv4 operators that are appropriate for use on Afforms
+ *
+ * This list may be further reduced by fields which declare a limited number of
+ * operators in their metadata.
+ *
+ * @return array
+ */
+ public static function getSearchOperators() {
+ return [
+ '=' => '=',
+ '!=' => '≠',
+ '>' => '>',
+ '<' => '<',
+ '>=' => '≥',
+ '<=' => '≤',
+ 'CONTAINS' => ts('Contains'),
+ 'NOT CONTAINS' => ts("Doesn't Contain"),
+ 'IN' => ts('Is One Of'),
+ 'NOT IN' => ts('Not One Of'),
+ 'LIKE' => ts('Is Like'),
+ 'NOT LIKE' => ts('Not Like'),
+ 'REGEXP' => ts('Matches Pattern'),
+ 'NOT REGEXP' => ts("Doesn't Match Pattern"),
+ ];
+ }
+
}
namePrefix = this.fieldName.substr(0, this.fieldName.length - this.defn.name.length);
}
+ if (this.defn.search_operator) {
+ this.search_operator = this.defn.search_operator;
+ }
+
// is_primary field - watch others in this afRepeat block to ensure only one is selected
if (ctrl.fieldName === 'is_primary' && 'repeatIndex' in $scope.dataProvider) {
$scope.$watch('dataProvider.afRepeat.getEntityController().getData()', function (items, prev) {
};
};
+ this.onChangeOperator = function() {
+ $scope.dataProvider.getFieldData()[ctrl.fieldName] = {};
+ };
+
// Getter/Setter function for most fields (except select & entityRef)
$scope.getSetValue = function(val) {
var currentVal = $scope.dataProvider.getFieldData()[ctrl.fieldName];
// Setter
if (arguments.length) {
- if (ctrl.defn.search_operator) {
+ if (ctrl.search_operator) {
if (typeof currentVal !== 'object') {
$scope.dataProvider.getFieldData()[ctrl.fieldName] = {};
}
- return ($scope.dataProvider.getFieldData()[ctrl.fieldName][ctrl.defn.search_operator] = val);
+ return ($scope.dataProvider.getFieldData()[ctrl.fieldName][ctrl.search_operator] = val);
}
return ($scope.dataProvider.getFieldData()[ctrl.fieldName] = val);
}
// Getter
- if (ctrl.defn.search_operator) {
- return (currentVal || {})[ctrl.defn.search_operator];
+ if (ctrl.search_operator) {
+ return (currentVal || {})[ctrl.search_operator];
}
return currentVal;
};
else if (ctrl.defn.search_range) {
return ($scope.dataProvider.getFieldData()[ctrl.fieldName]['>='] = val);
}
- else if (ctrl.defn.search_operator) {
+ else if (ctrl.search_operator) {
if (typeof currentVal !== 'object') {
$scope.dataProvider.getFieldData()[ctrl.fieldName] = {};
}
- return ($scope.dataProvider.getFieldData()[ctrl.fieldName][ctrl.defn.search_operator] = val);
+ return ($scope.dataProvider.getFieldData()[ctrl.fieldName][ctrl.search_operator] = val);
}
return ($scope.dataProvider.getFieldData()[ctrl.fieldName] = val);
}
else if (ctrl.defn.search_range) {
return currentVal['>='];
}
- else if (ctrl.defn.search_operator) {
- return (currentVal || {})[ctrl.defn.search_operator];
+ else if (ctrl.search_operator) {
+ return (currentVal || {})[ctrl.search_operator];
}
return currentVal;
};
<span class="crm-marker" title="{{:: ts('Required') }}" ng-if=":: $ctrl.defn.required">*</span>
</label>
<p class="crm-af-field-help-pre" ng-if=":: $ctrl.defn.help_pre">{{:: $ctrl.defn.help_pre }}</p>
-<div class="crm-af-field" ng-include="'~/af/fields/' + $ctrl.defn.input_type + '.html'"></div>
+<div class="crm-af-field" ng-if="!$ctrl.defn.expose_operator" ng-include="'~/af/fields/' + $ctrl.defn.input_type + '.html'"></div>
+<div class="input-group" ng-include="'~/af/afFieldWithSearchOperator.html'"></div>
<p class="crm-af-field-help-post" ng-if=":: $ctrl.defn.help_post">{{:: $ctrl.defn.help_post }}</p>
--- /dev/null
+<select class="form-control" crm-ui-select ng-model="$ctrl.search_operator" ng-change="$ctrl.onChangeOperator()">
+ <option ng-repeat="(name, label) in $ctrl.defn.operators" value="{{ name }}">{{ label }}</option>
+</select>
+<div class="crm-af-field" ng-include="'~/af/fields/' + $ctrl.defn.input_type + '.html'"></div>
display: block;
}
+#bootstrap-theme .input-group .crm-af-field {
+ display: inline-block;
+}
+
[af-repeat-item] {
position: relative;
}