Skip to content

Index filtering added to index views, addition of SearchFilterTrait (CFM-58) #21

Merged
merged 2 commits into from
Apr 25, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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($attribute) {
Copy link
Contributor

@benno benno Apr 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be getLabel(string $attribute).

Actually, strictly speaking it should be getLabel(string $attribute): string. I won't notate all the other functions, but they should also indicate their return types.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed and pushed: the attribute and return types are now there.

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() {
// 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) {
$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) {
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