Skip to content

Commit

Permalink
Merge pull request #26496 from colemanw/afformExposeOperators
Browse files Browse the repository at this point in the history
Afform - Enable search operators to be exposed on the form
  • Loading branch information
colemanw authored Jun 12, 2023
2 parents 7fb40a5 + 64afffa commit fc76317
Show file tree
Hide file tree
Showing 10 changed files with 107 additions and 33 deletions.
3 changes: 2 additions & 1 deletion ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public static function getAdminSettings() {
}
return [
'afform_type' => $afformTypes,
'search_operators' => \Civi\Afform\Utils::getSearchOperators(),
];
}

Expand Down Expand Up @@ -88,7 +89,7 @@ public static function getFields($entityName, $params = []) {
'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)) {
Expand Down
14 changes: 11 additions & 3 deletions ext/afform/admin/ang/afGuiEditor/elements/afGuiField-menu.html
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,18 @@
{{:: 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>
Expand Down
42 changes: 23 additions & 19 deletions ext/afform/admin/ang/afGuiEditor/elements/afGuiField.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@
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();
};

Expand Down Expand Up @@ -290,7 +295,24 @@
}
};

// 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);
};
Expand Down Expand Up @@ -344,24 +366,6 @@
$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;
Expand Down
13 changes: 13 additions & 0 deletions ext/afform/core/Civi/Afform/AfformMetadataInjector.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,19 @@ public static function setFieldMetadata(\DOMElement $afField, array $fieldInfo):
// 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');
Expand Down
6 changes: 5 additions & 1 deletion ext/afform/core/Civi/Afform/FormDataModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,14 @@ public static function getField(string $entityName, string $fieldName, string $a
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,
Expand Down
27 changes: 27 additions & 0 deletions ext/afform/core/Civi/Afform/Utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,31 @@ public static function getEntityWeights($formEntities, $entityValues) {
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"),
];
}

}
24 changes: 16 additions & 8 deletions ext/afform/core/ang/af/afField.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
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) {
Expand Down Expand Up @@ -226,22 +230,26 @@
};
};

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;
};
Expand All @@ -261,11 +269,11 @@
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);
}
Expand All @@ -277,8 +285,8 @@
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;
};
Expand Down
3 changes: 2 additions & 1 deletion ext/afform/core/ang/af/afField.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
<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>
4 changes: 4 additions & 0 deletions ext/afform/core/ang/af/afFieldWithSearchOperator.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<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>
4 changes: 4 additions & 0 deletions ext/afform/core/ang/afCore.css
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ af-form {
display: block;
}

#bootstrap-theme .input-group .crm-af-field {
display: inline-block;
}

[af-repeat-item] {
position: relative;
}
Expand Down

0 comments on commit fc76317

Please sign in to comment.