Skip to content

Commit

Permalink
Merge pull request #19959 from colemanw/afFilterRange
Browse files Browse the repository at this point in the history
SearchKit - Support implied operators in exposed search forms
  • Loading branch information
eileenmcnaughton authored Apr 5, 2021
2 parents 3346877 + 0a8585d commit b07e0ca
Show file tree
Hide file tree
Showing 33 changed files with 497 additions and 91 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 @@ -234,7 +234,8 @@ public static function getGuiSettings() {
];
}

$data['dateRanges'] = \CRM_Utils_Array::makeNonAssociative(\CRM_Core_OptionGroup::values('relative_date_filters'), 'id', 'label');
$dateRanges = \CRM_Utils_Array::makeNonAssociative(\CRM_Core_OptionGroup::values('relative_date_filters'), 'id', 'label');
$data['dateRanges'] = array_merge([['id' => '{}', 'label' => E::ts('Choose Date Range')]], $dateRanges);

return $data;
}
Expand Down
1 change: 1 addition & 0 deletions ext/afform/admin/ang/afGuiEditor.css
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@
#afGuiEditor .af-gui-layout.af-layout-inline > div {
display: inline-block;
width: 300px;
vertical-align: top;
}

#afGuiEditor .af-gui-button {
Expand Down
11 changes: 7 additions & 4 deletions ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
editor.layout = {'#children': []};
$scope.entities = {};

if ($scope.afform.type === 'form') {
if (editor.getFormType() === 'form') {
editor.allowEntityConfig = true;
editor.layout['#children'] = afGui.findRecursive($scope.afform.layout, {'#tag': 'af-form'})[0]['#children'];
$scope.entities = _.mapValues(afGui.findRecursive(editor.layout['#children'], {'#tag': 'af-entity'}, 'name'), backfillEntityDefaults);
Expand All @@ -63,7 +63,7 @@
}
}

if ($scope.afform.type === 'block') {
else if (editor.getFormType() === 'block') {
editor.layout['#children'] = $scope.afform.layout;
editor.blockEntity = $scope.afform.join || $scope.afform.block;
$scope.entities[editor.blockEntity] = backfillEntityDefaults({
Expand All @@ -73,9 +73,8 @@
});
}

if ($scope.afform.type === 'search') {
else if (editor.getFormType() === '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
Expand All @@ -85,6 +84,10 @@
}, true);
}

this.getFormType = function() {
return $scope.afform.type;
};

$scope.updateLayoutHtml = function() {
$scope.layoutHtml = '...Loading...';
crmApi4('Afform', 'convert', {layout: $scope.afform.layout, from: 'deep', to: 'html', formatWhitespace: true})
Expand Down
15 changes: 10 additions & 5 deletions ext/afform/admin/ang/afGuiEditor/afGuiEntity.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
label: ts('%1 Fields', {1: $scope.getMeta().label}),
fields: filterFields($scope.getMeta().fields)
});

// Add fields for af-join blocks
_.each(afGui.meta.entities, function(entity, entityName) {
if (check(ctrl.editor.layout['#children'], {'af-join': entityName})) {
$scope.fieldList.push({
Expand All @@ -69,13 +69,18 @@
function filterFields(fields) {
return _.transform(fields, function(fieldList, field) {
if (!search || _.contains(field.name, search) || _.contains(field.label.toLowerCase(), search)) {
fieldList.push({
"#tag": "af-field",
name: field.name
});
fieldList.push(fieldDefaults(field));
}
}, []);
}

function fieldDefaults(field) {
var tag = {
"#tag": "af-field",
name: field.name
};
return tag;
}
}

function buildBlockList(search) {
Expand Down
20 changes: 16 additions & 4 deletions ext/afform/admin/ang/afGuiEditor/afGuiSearch.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,25 @@
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
});
fieldList.push(fieldDefaults(field, prefix));
}
}, []);
}

function fieldDefaults(field, prefix) {
var tag = {
"#tag": "af-field",
name: (prefix ? prefix + '.' : '') + field.name
};
if (field.input_type === 'Select') {
tag.defn = {input_attrs: {multiple: true}};
} else if (field.input_type === 'Date') {
tag.defn = {input_type: 'Select', search_range: true};
} else if (field.options) {
tag.defn = {input_type: 'Select', input_attrs: {multiple: true}};
}
return tag;
}
}

function buildElementList(search) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
</div>
</li>
<li role="separator" class="divider"></li>
<li><a href ng-click="$ctrl.deleteThis()"><span class="text-danger">{{:: ts('Delete this button') }}</span></a></li>
<li><a href ng-click="$ctrl.deleteThis()"><span class="text-danger"><i class="crm-i fa-trash"></i> {{:: ts('Delete this button') }}</span></a></li>
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@
<li><af-gui-menu-item-border node="$ctrl.node"></af-gui-menu-item-border></li>
<li><af-gui-menu-item-background node="$ctrl.node"></af-gui-menu-item-background></li>
<li role="separator" class="divider"></li>
<li><a href ng-click="$ctrl.deleteThis()"><span class="text-danger">{{ !block ? ts('Delete this container') : ts('Delete this block') }}</span></a></li>
<li><a href ng-click="$ctrl.deleteThis()"><span class="text-danger"><i class="crm-i fa-trash"></i> {{ !block ? ts('Delete this container') : ts('Delete this block') }}</span></a></li>
23 changes: 18 additions & 5 deletions ext/afform/admin/ang/afGuiEditor/elements/afGuiField-menu.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,41 @@
</li>
<li>
<a href ng-click="toggleRequired(); $event.stopPropagation();" title="{{:: ts('Require this field') }}">
<i class="crm-i" ng-class="{'fa-square-o': !getProp('required'), 'fa-check-square-o': getProp('required')}"></i>
<i class="crm-i fa-{{ getProp('required') ? 'check-' : '' }}square-o"></i>
{{:: ts('Required') }}
</a>
</li>
<li>
<a href ng-click="toggleLabel(); $event.stopPropagation();" title="{{:: ts('Show field label') }}">
<i class="crm-i" ng-class="{'fa-square-o': $ctrl.node.defn.title === false, 'fa-check-square-o': $ctrl.node.defn.title !== false}"></i>
<i class="crm-i fa-{{ $ctrl.node.defn.label === false ? '' : 'check-' }}square-o"></i>
{{:: ts('Label') }}
</a>
</li>
<li>
<a href ng-click="toggleHelp('pre'); $event.stopPropagation();" title="{{:: ts('Show help text above this field') }}">
<i class="crm-i" ng-class="{'fa-square-o': !propIsset('help_pre'), 'fa-check-square-o': propIsset('help_pre')}"></i>
<i class="crm-i fa-{{ propIsset('help_pre') ? 'check-' : '' }}square-o"></i>
{{:: ts('Pre help text') }}
</a>
</li>
<li>
<a href ng-click="toggleHelp('post'); $event.stopPropagation();" title="{{:: ts('Show help text below this field') }}">
<i class="crm-i" ng-class="{'fa-square-o': !propIsset('help_post'), 'fa-check-square-o': propIsset('help_post')}"></i>
<i class="crm-i fa-{{ propIsset('help_post') ? 'check-' : '' }}square-o" ></i>
{{:: ts('Post help text') }}
</a>
</li>
<li role="separator" class="divider" ng-if="$ctrl.canBeRange() || $ctrl.canBeMultiple()"></li>
<li ng-if="$ctrl.canBeMultiple()" ng-click="$event.stopPropagation()">
<a href ng-click="toggleMultiple()" title="{{:: ts('Search multiple values') }}">
<i class="crm-i fa-{{ !$ctrl.node.defn.input_attrs.multiple ? '' : 'check-' }}square-o"></i>
{{:: ts('Multi-Select') }}
</a>
</li>
<li ng-if="$ctrl.canBeRange()" ng-click="$event.stopPropagation()">
<a href ng-click="toggleSearchRange()" title="{{:: ts('Search between low & high values') }}">
<i class="crm-i fa-{{ !$ctrl.node.defn.search_range ? '' : 'check-' }}square-o"></i>
{{:: ts('Search by range') }}
</a>
</li>
<li role="separator" class="divider" ng-if="hasOptions()"></li>
<li ng-if="hasOptions()" ng-click="$event.stopPropagation()">
<a href ng-click="resetOptions()" title="{{:: ts('Reset the option list for this field') }}">
Expand All @@ -46,6 +59,6 @@
<li role="separator" class="divider"></li>
<li>
<a href ng-click="$ctrl.deleteThis()" title="{{:: ts('Remove field from form') }}">
<span class="text-danger">{{:: ts('Delete this field') }}</span>
<span class="text-danger"><i class="crm-i fa-trash"></i> {{:: ts('Delete this field') }}</span>
</a>
</li>
72 changes: 64 additions & 8 deletions ext/afform/admin/ang/afGuiEditor/elements/afGuiField.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,57 @@
var yesNo = [
{id: '1', label: ts('Yes')},
{id: '0', label: ts('No')}
];
],
singleElement = [''],
// When search-by-range is enabled the second element gets a suffix for some properties like "placeholder2"
rangeElements = ['', '2'],
dateRangeElements = ['1', '2'],
relativeDatesWithPickRange = CRM.afGuiEditor.dateRanges,
relativeDatesWithoutPickRange = relativeDatesWithPickRange.slice(1);

this.$onInit = function() {
$scope.meta = afGui.meta;
};

this.isSearch = function() {
return !_.isEmpty($scope.meta.searchDisplays);
return ctrl.editor.getFormType() === 'search';
};

this.canBeRange = function() {
// Range search only makes sense for search display forms
return this.isSearch() &&
// Hack for postal code which is not stored as a number but can act like one
(ctrl.node.name.substr(-11) === 'postal_code' || (
// Multiselects cannot use range search
!ctrl.getDefn().input_attrs.multiple &&
// DataType & inputType must make sense for a range
_.includes(['Date', 'Timestamp', 'Integer', 'Float'], ctrl.getDefn().data_type) &&
_.includes(['Date', 'Number', 'Select'], $scope.getProp('input_type'))
));
};

this.canBeMultiple = function() {
return this.isSearch() &&
!_.includes(['Date', 'Timestamp'], ctrl.getDefn().data_type) &&
$scope.getProp('input_type') === 'Select';
};

this.getRangeElements = function(type) {
if (!$scope.getProp('search_range') || (type === 'Select' && ctrl.getDefn().input_type === 'Date')) {
return singleElement;
}
return type === 'Date' ? dateRangeElements : rangeElements;
};

// Returns the original field definition from metadata
this.getDefn = function() {
var defn = afGui.getField(ctrl.container.getFieldEntityType(ctrl.node.name), ctrl.node.name);
return defn || {
defn = defn || {
label: ts('Untitled'),
requred: false,
input_attrs: []
required: false
};
defn.input_attrs = _.isEmpty(defn.input_attrs) ? {} : defn.input_attrs;
return defn;
};

$scope.getOriginalLabel = function() {
Expand All @@ -52,12 +85,12 @@
return _.contains(['CheckBox', 'Radio', 'Select'], inputType) && !(inputType === 'CheckBox' && !ctrl.getDefn().options);
};

$scope.getOptions = this.getOptions = function() {
this.getOptions = function() {
if (ctrl.node.defn && ctrl.node.defn.options) {
return ctrl.node.defn.options;
}
if (_.includes(['Date', 'Timestamp'], $scope.getProp('data_type'))) {
return CRM.afGuiEditor.dateRanges;
return $scope.getProp('search_range') ? relativeDatesWithPickRange : relativeDatesWithoutPickRange;
}
return ctrl.getDefn().options || ($scope.getProp('input_type') === 'CheckBox' ? null : yesNo);
};
Expand Down Expand Up @@ -122,6 +155,20 @@
}
};

$scope.toggleMultiple = function() {
var newVal = getSet('input_attrs.multiple', !getSet('input_attrs.multiple'));
if (newVal && getSet('search_range')) {
getSet('search_range', false);
}
};

$scope.toggleSearchRange = function() {
var newVal = getSet('search_range', !getSet('search_range'));
if (newVal && getSet('input_attrs.multiple')) {
getSet('input_attrs.multiple', false);
}
};

$scope.toggleRequired = function() {
getSet('required', !getSet('required'));
return false;
Expand Down Expand Up @@ -151,6 +198,10 @@
delete localDefn[item];
clearOut(ctrl.node, ['defn'].concat(path));
}
// When changing input_type
if (propName === 'input_type' && ctrl.node.defn && ctrl.node.defn.search_range && !ctrl.canBeRange()) {
delete ctrl.node.defn.search_range;
}
return val;
}
return $scope.getProp(propName);
Expand All @@ -171,10 +222,15 @@
return container;
}

// Returns true only if value is [], {}, '', null, or undefined.
function isEmpty(val) {
return typeof val !== 'boolean' && typeof val !== 'number' && _.isEmpty(val);
}

// Recursively clears out empty arrays and objects
function clearOut(parent, path) {
var item;
while (path.length && _.every(drillDown(parent, path), _.isEmpty)) {
while (path.length && _.every(drillDown(parent, path), isEmpty)) {
item = path.pop();
delete drillDown(parent, path)[item];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
<li><af-gui-menu-item-background node="$ctrl.node"></af-gui-menu-item-background></li>
<li role="separator" class="divider"></li>
<li>
<a href ng-click="$ctrl.deleteThis()"><span class="text-danger">{{:: ts('Delete this content') }}</span></a>
<a href ng-click="$ctrl.deleteThis()"><span class="text-danger"><i class="crm-i fa-trash"></i> {{:: ts('Delete this content') }}</span></a>
</li>
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@
</li>
<li role="separator" class="divider"></li>
<li>
<a href ng-click="$ctrl.deleteThis()"><span class="text-danger">{{:: ts('Delete this text') }}</span></a>
<a href ng-click="$ctrl.deleteThis()"><span class="text-danger"><i class="crm-i fa-trash"></i> {{:: ts('Delete this text') }}</span></a>
</li>
8 changes: 4 additions & 4 deletions ext/afform/admin/ang/afGuiEditor/inputType/CheckBox.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<ul class="crm-checkbox-list" id="{{ fieldId }}" ng-if="getOptions()">
<li ng-repeat="opt in getOptions()" >
<input type="checkbox" disabled />
<ul class="crm-checkbox-list" ng-if="$ctrl.getOptions()">
<li ng-repeat="opt in $ctrl.getOptions()" >
<input type="checkbox" disabled >
<label>{{ opt.label }}</label>
</li>
</ul>
<input type="checkbox" disabled ng-if="!getOptions()" />
<input type="checkbox" disabled ng-if="!$ctrl.getOptions()" >
9 changes: 6 additions & 3 deletions ext/afform/admin/ang/afGuiEditor/inputType/Date.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<div class="form-inline">
<input autocomplete="off" class="form-control crm-form-date crm-placeholder-icon" placeholder="&#xF073" ng-model="getSet('input_attrs.placeholder')" ng-model-options="{getterSetter: true}" type="text" title="{{:: ts('Click to add placeholder text') }}" />
<span class="addon fa fa-calendar"></span>
<input autocomplete="off" ng-if="getProp('input_attrs.time')" placeholder="&#xF017" class="form-control crm-form-time crm-placeholder-icon" ng-model="getSet('input_attrs.timePlaceholder')" ng-model-options="{getterSetter: true}" type="text" title="{{:: ts('Click to add placeholder text') }}" />
<div class="form-group" ng-repeat="i in $ctrl.getRangeElements('Date')">
<span class="af-field-range-sep" ng-if="i">-</span>
<input autocomplete="off" class="form-control crm-form-date crm-placeholder-icon" placeholder="&#xF073" ng-model="getSet('input_attrs.placeholder' + i)" ng-model-options="{getterSetter: true}" type="text" title="{{:: ts('Click to add placeholder text') }}" />
<span class="addon fa fa-calendar"></span>
<input autocomplete="off" ng-if="getProp('input_attrs.time')" placeholder="&#xF017" class="form-control crm-form-time crm-placeholder-icon" ng-model="getSet('input_attrs.timePlaceholder' + i)" ng-model-options="{getterSetter: true}" type="text" title="{{:: ts('Click to add placeholder text') }}" />
</div>
</div>
7 changes: 6 additions & 1 deletion ext/afform/admin/ang/afGuiEditor/inputType/Number.html
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
<input autocomplete="off" class="form-control" ng-model="getSet('input_attrs.placeholder')" ng-model-options="{getterSetter: true}" type="text" title="{{:: ts('Click to add placeholder text') }}" />
<div class="form-inline">
<div class="form-group" ng-repeat="i in $ctrl.getRangeElements('Number')">
<span class="af-field-range-sep" ng-if="i">-</span>
<input autocomplete="off" class="form-control" ng-model="getSet('input_attrs.placeholder' + i)" ng-model-options="{getterSetter: true}" type="text" title="{{:: ts('Click to add placeholder text') }}"/>
</div>
</div>
2 changes: 1 addition & 1 deletion ext/afform/admin/ang/afGuiEditor/inputType/Radio.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div class="form-inline">
<label ng-repeat="opt in getOptions()" class="radio" >
<label ng-repeat="opt in $ctrl.getOptions()" class="radio" >
<input class="crm-form-radio" type="radio" disabled />
{{ opt.label }}
</label>
Expand Down
22 changes: 13 additions & 9 deletions ext/afform/admin/ang/afGuiEditor/inputType/Select.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
<div class="form-inline">
<div class="input-group">
<input autocomplete="off" class="form-control" placeholder="{{:: ts('Select') }}" title="{{:: ts('Click to add placeholder text') }}" ng-model="getSet('input_attrs.placeholder')" ng-model-options="{getterSetter: true}" type="text" />
<div class="input-group-btn" af-gui-menu>
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="crm-i fa-caret-down"></i></button>
<ul class="dropdown-menu" ng-if="menu.open">
<li ng-repeat="opt in getOptions()" >
<a href>{{ opt.label }}</a>
</li>
</ul>
<div class="form-group" ng-repeat="i in $ctrl.getRangeElements('Select')">
<span class="af-field-range-sep" ng-if="i">-</span>
<div class="input-group">
<input autocomplete="off" class="form-control" placeholder="{{:: ts('Select') }}" title="{{:: ts('Click to add placeholder text') }}" ng-model="getSet('input_attrs.placeholder' + i)" ng-model-options="{getterSetter: true}" type="text" />
<div class="input-group-btn" af-gui-menu>
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="crm-i fa-caret-down"></i></button>
<ul class="dropdown-menu" ng-if="menu.open">
<li ng-repeat="opt in $ctrl.getOptions()" >
<a href>{{ opt.label }}</a>
</li>
</ul>
</div>
</div>
</div>
<div ng-if="getProp('search_range') && $ctrl.getDefn().input_type === 'Date'" class="form-group" ng-include="'~/afGuiEditor/inputType/Date.html'"></div>
</div>
Loading

0 comments on commit b07e0ca

Please sign in to comment.