Skip to content

Commit

Permalink
Index filtering added to index views, addition of SearchFilterTrait (…
Browse files Browse the repository at this point in the history
…CFM-58) (#21)

* First commit for index filtering: filtering added to index views, addition of SearchFilterTrait (CFM-58)

* Ensure both attribute and return types are stated in SearchFilterTrait (CFM-58)
  • Loading branch information
arlen authored Apr 25, 2022
1 parent fe8bb14 commit b80af80
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 64 deletions.
15 changes: 15 additions & 0 deletions app/src/Controller/StandardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,21 @@ public function index() {
&& $table->getIndexContains()) {
$query->contain($table->getIndexContains());
}

// SearchFilterTrait
if(method_exists($table, "getSearchableAttributes")) {
$searchableAttributes = $table->getSearchableAttributes();

if(!empty($searchableAttributes)) {
foreach(array_keys($searchableAttributes) as $attribute) {
if(!empty($this->request->getQuery($attribute))) {
$query = $table->whereFilter($query, $attribute, $this->request->getQuery($attribute));
}
}

$this->set('vv_searchable_attributes', $searchableAttributes);
}
}

// The Cake documents describe $this->paginate (which worked in Cake 2),
// but it doesn't seem to work in Cake 4. So we just use $this->pagination
Expand Down
149 changes: 149 additions & 0 deletions app/src/Lib/Traits/SearchFilterTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php
/**
* COmanage Registry Search Filter Trait
*
* Portions licensed to the University Corporation for Advanced Internet
* Development, Inc. ("UCAID") under one or more contributor license agreements.
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* UCAID licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @link https://www.internet2.edu/comanage COmanage Project
* @package registry
* @since COmanage Registry v5.0.0
* @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/

declare(strict_types = 1);

namespace App\Lib\Traits;

trait SearchFilterTrait {
// Array (and configuration) of permitted search filters
private $searchFilters = array();

/**
* Determine the UI label for the specified attribute.
*
* @since COmanage Registry v5.0.0
* @param string $attribute Attribute
* @return string Label
* @todo Merge this with _column_key from index.ctp
*/

public function getLabel(string $attribute): string {
if(isset($this->searchFilters[$attribute]['label'])
&& $this->searchFilters[$attribute]['label'] !== null) {
return $this->searchFilters[$attribute]['label'];
}

// Try to construct a label from the language key.
$l = __d('field', $attribute);

if($l != $attribute) {
return $l;
}

// If we make it here, just return $attribute
return $attribute;
}

/**
* Obtain the set of permitted search attributes.
*
* @since COmanage Registry v5.0.0
* @return array Array of permitted search attributes and configuration elements needed for display
*/

public function getSearchableAttributes(): array {
// Not every configuration element is necessary for the search form, and
// some need to be calculated, so we do that work here.

$ret = [];

foreach(array_keys($this->searchFilters) as $attr) {
$ret[ $attr ] = [
'label' => $this->getLabel($attr)
];
}

return $ret;
}

/**
* Add a permitted search filters.
*
* @since COmanage Registry v5.0.0
* @param string $attribute Attribute that filtering is permitted on (database name)
* @param bool $caseSensitive Whether this attribute is case sensitive
* @param string $label Label for this search field, or null to autocalculate
* @param bool $substring Whether substring searching is permitted for this attribute
*/

public function setSearchFilter(string $attribute,
bool $caseSensitive=false,
string $label=null,
bool $substring=true): void {
$this->searchFilters[$attribute] = compact('caseSensitive', 'label', 'substring');
}

/**
* Build a query where() clause for the configured attribute.
*
* @since COmanage Registry v5.0.0
* @param \Cake\ORM\Query $query Cake ORM Query object
* @param string $attribute Attribute to filter on (database name)
* @param string $q Value to filter on
* @return \Cake\ORM\Query Cake ORM Query object
*/

public function whereFilter(\Cake\ORM\Query $query, string $attribute, string $q): object {
if(!empty($this->searchFilters[$attribute])) {
$cs = (isset($this->searchFilters[$attribute]['caseSensitive'])
&& $this->searchFilters[$attribute]['caseSensitive']);

$sub = (isset($this->searchFilters[$attribute]['substring'])
&& $this->searchFilters[$attribute]['substring']);

$search = $q;

if($sub) {
// Substring
// note, for now at least, a user may infix their own %
$search .= "%";
}

if($cs) {
// Case sensitive
$query->where([$attribute => $search]);
} else {
// Case insensitive
$query->where(function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) use ($attribute, $search, $sub) {
$lower = $query->func()->lower([
// https://book.cakephp.org/3/en/orm/query-builder.html#function-arguments
$attribute => 'identifier'
]);
if($sub) {
return $exp->like($lower, strtolower($search));
} else {
return $exp->eq($lower, strtolower($search));
}
});
}
}
// else not a permitted attribute

return $query;
}
}
6 changes: 6 additions & 0 deletions app/src/Model/Table/CousTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class CousTable extends Table {
use \App\Lib\Traits\CoLinkTrait;
use \App\Lib\Traits\PermissionsTrait;
use \App\Lib\Traits\PrimaryLinkTrait;
use \App\Lib\Traits\SearchFilterTrait;
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\ValidationTrait;

Expand Down Expand Up @@ -71,6 +72,11 @@ public function initialize(array $config): void {

$this->setPrimaryLink('co_id');
$this->setRequiresCO(true);

// Set up the fields that may be filtered in the index view
$this->setSearchFilter('name', false, null, true);
$this->setSearchFilter('parent_id', true, null, false);
$this->setSearchFilter('description', false, null, true);

$this->setPermissions([
// Actions that operate over an entity (ie: require an $id)
Expand Down
6 changes: 6 additions & 0 deletions app/src/Model/Table/TypesTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class TypesTable extends Table {
use \App\Lib\Traits\CoLinkTrait;
use \App\Lib\Traits\PermissionsTrait;
use \App\Lib\Traits\PrimaryLinkTrait;
use \App\Lib\Traits\SearchFilterTrait;
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\ValidationTrait;

Expand Down Expand Up @@ -112,6 +113,11 @@ public function initialize(array $config): void {
'class' => 'SuspendableStatusEnum'
]
]);

// Set up the fields that may be filtered in the index view
$this->setSearchFilter('display_name', false, null, true);
$this->setSearchFilter('attribute', true, null, false);
$this->setSearchFilter('statuses', false, null, true);

$this->setPermissions([
// Actions that operate over an entity (ie: require an $id)
Expand Down
3 changes: 3 additions & 0 deletions app/templates/Cous/columns.inc
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
* @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/

// Turn on the search/filter box for this index view
$enableFiltering = true;

$indexColumns = [
'name' => [
'type' => 'link'
Expand Down
6 changes: 3 additions & 3 deletions app/templates/Standard/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,9 @@ function _column_key($modelsName, $c, $tz=null) {
<?php endif; // $banners ?>

<!-- Search block -->
<?php if(!empty($enableSearch)): ?>
<?= $this->element('search'); ?>
<?php endif; // $enableSearch ?>
<?php if(!empty($enableFiltering)): ?>
<?= $this->element('filter'); ?>
<?php endif; // $enableFiltering ?>

<!-- Index table -->
<div class="table-container">
Expand Down
3 changes: 3 additions & 0 deletions app/templates/Types/columns.inc
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
* @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/

// Turn on the search/filter box for this index view
$enableFiltering = true;

$indexColumns = [
'display_name' => [
'type' => 'link',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php
/**
* COmanage Registry Search Element
* COmanage Registry Filter Element - for index view filtering
*
* Portions licensed to the University Corporation for Advanced Internet
* Development, Inc. ("UCAID") under one or more contributor license agreements.
Expand Down Expand Up @@ -37,7 +37,7 @@

// Begin the form
print $this->Form->create(null, [
'id' => 'top-search-form',
'id' => 'top-filters-form',
'type' => 'get'
]);

Expand All @@ -55,15 +55,15 @@
$hasActiveFilters = false;
?>

<div id="<?= $modelName . ucfirst($this->request->getParam('action')); ?>Search" class="top-search">
<fieldset onclick="event.stopPropagation();">
<legend id="top-search-toggle">
<div id="<?= $modelName . ucfirst($this->request->getParam('action')); ?>Search" class="top-filters">
<fieldset>
<legend id="top-filters-toggle">
<em class="material-icons">search</em>
<?= __d('operation', 'filter'); ?>


<?php if(!empty($search_params)):?>
<span id="top-search-active-filters">
<span id="top-filters-active-filters">
<?php foreach($search_params as $key => $params): ?>
<?php
// Construct aria-controls string
Expand All @@ -72,27 +72,27 @@
// We have active filters - not just a sort.
$hasActiveFilters = true;
?>
<button class="top-search-active-filter deletebutton spin btn btn-default btn-sm" type="button" aria-controls="<?php print $aria_controls; ?>" title="<?= __d('operation', 'clear.filters',[2]); ?>">
<button class="top-filters-active-filter deletebutton spin btn btn-default btn-sm" type="button" aria-controls="<?php print $aria_controls; ?>" title="<?= __d('operation', 'clear.filters',[2]); ?>">
<em class="material-icons">cancel</em>
<span class="top-search-active-filter-title">
<span class="top-filters-active-filter-title">
<?= $vv_searchable_attributes[$key]['label']; ?>
</span>
<span class="top-search-active-filter-value">
<span class="top-filters-active-filter-value">
<?= filter_var($search_params[$key], FILTER_SANITIZE_SPECIAL_CHARS); ?>
</span>
</button>
<?php endforeach; ?>
<?php if($hasActiveFilters): ?>
<button id="top-search-clear-all-button" class="filter-clear-all-button spin btn" type="button" aria-controls="top-search-clear" onclick="event.stopPropagation()">
<button id="top-filters-clear-all-button" class="filter-clear-all-button spin btn" type="button" aria-controls="top-filters-clear" onclick="event.stopPropagation()">
<?= __d('operation', 'clear.filters',[2]); ?>
</button>
<?php endif; ?>
</span>
<?php endif; ?>
<button class="cm-toggle nospin" aria-expanded="false" aria-controls="top-search-fields" type="button"><em class="material-icons drop-arrow">arrow_drop_down</em></button>
<button class="cm-toggle nospin" aria-expanded="false" aria-controls="top-filters-fields" type="button"><em class="material-icons drop-arrow">arrow_drop_down</em></button>
</legend>
<div id="top-search-fields">
<div id="top-search-fields-subgroups">
<div id="top-filters-fields">
<div id="top-filters-fields-subgroups">
<?php
$field_subgroup_columns = array();

Expand All @@ -119,17 +119,17 @@
?>
</div>
<?php $rebalanceColumns = ((count($vv_searchable_attributes)) % 2 != 0) ? ' class="tss-rebalance"' : ''; ?>
<div id="top-search-submit"<?php print $rebalanceColumns ?>>
<div id="top-filters-submit"<?php print $rebalanceColumns ?>>
<?php
$args = array();
// search button (submit)
$args['id'] = 'top-search-filter-button';
$args['id'] = 'top-filters-filter-button';
$args['aria-label'] = __d('operation', 'filter');
$args['class'] = 'submit-button spin btn btn-primary';
print $this->Form->submit(__d('operation', 'filter'),$args);

// clear button
$args['id'] = 'top-search-clear';
$args['id'] = 'top-filters-clear';
$args['class'] = 'clear-button spin btn btn-default';
$args['aria-label'] = __d('operation', 'clear');
$args['onclick'] = 'clearTopSearch(this.form)';
Expand Down
28 changes: 14 additions & 14 deletions app/templates/element/javascript.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,31 +99,31 @@

// TOP SEARCH FILTER FORM
// Send only non-empty fields in the form
$("#top-search-form").submit(function() {
$("#top-search-form *").filter(':input').each(function () {
$("#top-filters-form").submit(function() {
$("#top-filters-form *").filter(':input').each(function () {
if($(this).val() == '') {
$(this).prop('disabled',true);
}
});
});

// Toggle the top search filter box
$("#top-search-toggle, #top-search-toggle button.cm-toggle").click(function(e) {
$("#top-filters-toggle, #top-filters-toggle button.cm-toggle").click(function(e) {
e.preventDefault();
e.stopPropagation();
if ($("#top-search-fields").is(":visible")) {
$("#top-search-fields").hide();
$("#top-search-toggle button.cm-toggle").attr("aria-expanded","false");
$("#top-search-toggle button.cm-toggle .drop-arrow").text("arrow_drop_down");
if ($("#top-filters-fields").is(":visible")) {
$("#top-filters-fields").hide();
$("#top-filters-toggle button.cm-toggle").attr("aria-expanded","false");
$("#top-filters-toggle button.cm-toggle .drop-arrow").text("arrow_drop_down");
} else {
$("#top-search-fields").show();
$("#top-search-toggle button.cm-toggle").attr("aria-expanded","true");
$("#top-search-toggle button.cm-toggle .drop-arrow").text("arrow_drop_up");
$("#top-filters-fields").show();
$("#top-filters-toggle button.cm-toggle").attr("aria-expanded","true");
$("#top-filters-toggle button.cm-toggle .drop-arrow").text("arrow_drop_up");
}
});

// Clear a specific top search filter by clicking the filter button
$("#top-search-toggle button.top-search-active-filter").click(function(e) {
$("#top-filters-toggle button.top-filters-active-filter").click(function(e) {
e.preventDefault();
e.stopPropagation();
$(this).hide();
Expand All @@ -133,12 +133,12 @@
});

// Clear all top filters from the filter bar
$("#top-search-clear-all-button").click(function(e) {
$("#top-filters-clear-all-button").click(function(e) {
e.preventDefault();
e.stopPropagation();
$(this).hide();
$("#top-search-toggle .top-search-active-filter").hide();
$("#top-search-clear").click();
$("#top-filters-toggle .top-filters-active-filter").hide();
$("#top-filters-clear").click();
});

// Make all submit buttons pretty (Bootstrap)
Expand Down
Loading

0 comments on commit b80af80

Please sign in to comment.