From b80af80969a4fcb07c3ca59bd250032bc4d8fdc3 Mon Sep 17 00:00:00 2001 From: Arlen Johnson Date: Mon, 25 Apr 2022 10:49:03 -0400 Subject: [PATCH] Index filtering added to index views, addition of SearchFilterTrait (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) --- app/src/Controller/StandardController.php | 15 ++ app/src/Lib/Traits/SearchFilterTrait.php | 149 ++++++++++++++++++ app/src/Model/Table/CousTable.php | 6 + app/src/Model/Table/TypesTable.php | 6 + app/templates/Cous/columns.inc | 3 + app/templates/Standard/index.php | 6 +- app/templates/Types/columns.inc | 3 + .../element/{search.php => filter.php} | 32 ++-- app/templates/element/javascript.php | 28 ++-- app/webroot/css/co-base.css | 56 ++++--- app/webroot/css/co-responsive.css | 4 +- 11 files changed, 244 insertions(+), 64 deletions(-) create mode 100644 app/src/Lib/Traits/SearchFilterTrait.php rename app/templates/element/{search.php => filter.php} (81%) diff --git a/app/src/Controller/StandardController.php b/app/src/Controller/StandardController.php index da6309a85..8ddb7eeff 100644 --- a/app/src/Controller/StandardController.php +++ b/app/src/Controller/StandardController.php @@ -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 diff --git a/app/src/Lib/Traits/SearchFilterTrait.php b/app/src/Lib/Traits/SearchFilterTrait.php new file mode 100644 index 000000000..6a13472f9 --- /dev/null +++ b/app/src/Lib/Traits/SearchFilterTrait.php @@ -0,0 +1,149 @@ +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; + } +} diff --git a/app/src/Model/Table/CousTable.php b/app/src/Model/Table/CousTable.php index 055fbf5c9..082ad40d6 100644 --- a/app/src/Model/Table/CousTable.php +++ b/app/src/Model/Table/CousTable.php @@ -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; @@ -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) diff --git a/app/src/Model/Table/TypesTable.php b/app/src/Model/Table/TypesTable.php index 54b6952a0..856662495 100644 --- a/app/src/Model/Table/TypesTable.php +++ b/app/src/Model/Table/TypesTable.php @@ -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; @@ -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) diff --git a/app/templates/Cous/columns.inc b/app/templates/Cous/columns.inc index 72f9f5fd3..d2b50c09b 100644 --- a/app/templates/Cous/columns.inc +++ b/app/templates/Cous/columns.inc @@ -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' diff --git a/app/templates/Standard/index.php b/app/templates/Standard/index.php index b51b3b200..9a10ee5ca 100644 --- a/app/templates/Standard/index.php +++ b/app/templates/Standard/index.php @@ -143,9 +143,9 @@ function _column_key($modelsName, $c, $tz=null) { - - element('search'); ?> - + + element('filter'); ?> +
diff --git a/app/templates/Types/columns.inc b/app/templates/Types/columns.inc index b0aeca57e..34a9deb23 100644 --- a/app/templates/Types/columns.inc +++ b/app/templates/Types/columns.inc @@ -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', diff --git a/app/templates/element/search.php b/app/templates/element/filter.php similarity index 81% rename from app/templates/element/search.php rename to app/templates/element/filter.php index fad23108c..33c25a691 100644 --- a/app/templates/element/search.php +++ b/app/templates/element/filter.php @@ -1,6 +1,6 @@ Form->create(null, [ - 'id' => 'top-search-form', + 'id' => 'top-filters-form', 'type' => 'get' ]); @@ -55,15 +55,15 @@ $hasActiveFilters = false; ?> -