From fe2090dd2b8c2b7d32de04df3a019fc9a3e559cb Mon Sep 17 00:00:00 2001
From: Ioannis Igoumenos <ioigoume@gmail.com>
Date: Fri, 3 May 2024 17:25:51 +0300
Subject: [PATCH] Refactored filter.php.Created elements and FilterHelper.
 (#190)

---
 app/src/View/Helper/FieldHelper.php           |  21 +-
 app/src/View/Helper/FilterHelper.php          | 160 +++++++++++++
 app/templates/Standard/index.php              |   8 +-
 app/templates/element/filter/checkboxes.php   |  63 +++++
 app/templates/element/filter/dateSingle.php   |  64 +++++
 ...{dateTimeFilters.php => datetimeGroup.php} |  20 +-
 app/templates/element/filter/default.php      |  61 +++++
 app/templates/element/filter/filter.php       | 220 +++---------------
 .../element/filter/footerButtons.php          |  79 +++++++
 .../{topFiltersToggle.php => legend.php}      |  13 +-
 .../filter/{filterOptions.php => options.php} |   2 +
 .../{activeTopButton.php => topButtons.php}   |   2 +
 12 files changed, 504 insertions(+), 209 deletions(-)
 create mode 100644 app/src/View/Helper/FilterHelper.php
 create mode 100644 app/templates/element/filter/checkboxes.php
 create mode 100644 app/templates/element/filter/dateSingle.php
 rename app/templates/element/filter/{dateTimeFilters.php => datetimeGroup.php} (86%)
 create mode 100644 app/templates/element/filter/default.php
 create mode 100644 app/templates/element/filter/footerButtons.php
 rename app/templates/element/filter/{topFiltersToggle.php => legend.php} (90%)
 rename app/templates/element/filter/{filterOptions.php => options.php} (99%)
 rename app/templates/element/filter/{activeTopButton.php => topButtons.php} (98%)

diff --git a/app/src/View/Helper/FieldHelper.php b/app/src/View/Helper/FieldHelper.php
index 60355b63c..958d17005 100644
--- a/app/src/View/Helper/FieldHelper.php
+++ b/app/src/View/Helper/FieldHelper.php
@@ -172,9 +172,8 @@ public function calculateLabelAndDescription(string $fieldName): array
    * Emit a date/time form control.
    * This is a wrapper function for $this->control()
    *
-   * @param   string       $fieldName    Form field
-   * @param   string       $dateType     Standard, DateOnly, FromTime, ThroughTime
-   * @param   array|null   $queryParams  Request Query parameters used by the filtering Blocks to get the date values
+   * @param   string       $fieldName  Form field
+   * @param   string       $dateType   Standard, DateOnly, FromTime, ThroughTime
    * @param   string|null  $label
    *
    * @return string HTML element
@@ -183,24 +182,16 @@ public function calculateLabelAndDescription(string $fieldName): array
 
   public function dateField(string $fieldName,
                             string $dateType=DateTypeEnum::Standard,
-                            array $queryParams=null,
                             string $label=null): string
   {
     // Initialize
     $dateFormat = $dateType === DateTypeEnum::DateOnly ? 'yyyy-MM-dd' : 'yyyy-MM-dd HH:mm:ss';
     $dateTitle = $dateType === DateTypeEnum::DateOnly ? 'datepicker.enterDate' : 'datepicker.enterDateTime';
     $datePattern = $dateType === DateTypeEnum::DateOnly ? '\d{4}-\d{2}-\d{2}' : '\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}';
-    $date_object = null;
-
-    if(isset($queryParams)) {
-      if(!empty($queryParams[$fieldName])) {
-        $date_object = FrozenTime::parse($queryParams[$fieldName]);
-      }
-    } else {
-      // This is an entity view. We are getting the data from the object
-      $entity = $this->getView()->get('vv_obj');
-      $date_object = $entity->$fieldName;
-    }
+    $queryParams = $this->getView()->getRequest()->getQueryParams();
+    $date_object = !empty($queryParams[$fieldName])
+                   ? FrozenTime::parse($queryParams[$fieldName])
+                   : $this->getEntity()?->$fieldName;
 
     // Create the options array for the (text input) form control
     $coptions = [];
diff --git a/app/src/View/Helper/FilterHelper.php b/app/src/View/Helper/FilterHelper.php
new file mode 100644
index 000000000..1cd59b602
--- /dev/null
+++ b/app/src/View/Helper/FilterHelper.php
@@ -0,0 +1,160 @@
+<?php
+/**
+ * COmanage Registry Filter Helper
+ *
+ * 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\View\Helper;
+
+use Cake\Collection\Collection;
+use Cake\Utility\{Inflector, Hash};
+use Cake\View\Helper;
+
+class FilterHelper extends Helper
+{
+  /**
+   * Calculate Form Default Field Options
+   *
+   * @param   string  $columnName
+   * @param   string  $label
+   *
+   * @return array
+   */
+  public function calculateFieldParams(string $columnName, string $label): array{
+    $queryParameters = $this->getView()->getRequest()->getQueryParams();
+    $searchableAttributesExtras = $this->getView()->get('vv_searchable_attributes_extras') ?? [];
+    $populatedVarData = $this->getView()->get(
+    // The populated variables are in plural while the column names are singular
+    // Convention: It is a prerequisite that the vvar should be the plural of the column name
+      lcfirst(Inflector::pluralize(Inflector::camelize($columnName)))
+    );
+
+    // Field options
+    $formParams = [
+      'label' => $label,
+      'type' => isset($populatedVarData) ? 'select' : 'text',
+      // Options will be ignored for non-select fields
+      'options' => $populatedVarData,
+      'value' => $queryParameters[$columnName] ?? '',
+      'required' => false,
+      'class' => 'form-control',
+      // Empty will be ignored for non-select fields
+      'empty' => true
+    ];
+
+    // Custom/Additional option items defined in the ModelTable::initialize::setFilterConfig
+    // Example: CousTable
+    if(isset($searchableAttributesExtras[$columnName]['options'])) {
+      // Flatten the custom options
+      $customOptionsFlattened = Hash::flatten($searchableAttributesExtras[$columnName]['options']);
+      // Get the key of the placeholder string
+      $dataKey = array_search('@DATA@', $customOptionsFlattened, true);
+      if($dataKey !== false) {
+        $customOptionsFlattened[$dataKey] = $formParams['options'];
+        $formParams['options'] = Hash::expand($customOptionsFlattened);
+      }
+    }
+
+    return $formParams;
+  }
+
+  /**
+   *
+   * @return array[]   [search_params, $field_booleans_columns, $field_datetime_columns, $field_generic_columns]
+   */
+  public function explodeFieldsByType(): array
+  {
+     // Get the query string and separate the search params from the non-search params
+    $queryParameters = $this->getView()->getRequest()->getQueryParams();
+    $searchableAttributes = $this->getView()->get('vv_searchable_attributes') ?? [];
+
+    // Filter the search params and take params with aliases into consideration
+    $search_params = [];
+    $field_booleans_columns = [];
+    $field_datetime_columns = [];
+    $field_generic_columns = [];
+    foreach ($searchableAttributes as $attr => $value) {
+      if($value['type'] == 'boolean') {
+        $field_booleans_columns[$attr] = $value;
+      } elseif ($value['type'] == 'timestamp') {
+        $field_datetime_columns[$attr] = $value;
+      } else {
+        $field_generic_columns[$attr] = $value;
+      }
+
+      if(isset($queryParameters[$attr])) {
+        $search_params[$attr] = $queryParameters[$attr];
+        continue;
+      }
+
+      if(isset($value['alias']) && is_array($value['alias'])) {
+        foreach ($value['alias'] as $alias_key) {
+          if(isset($queryParameters[$alias_key])) {
+            $search_params[$attr][$alias_key] = $queryParameters[$alias_key];
+          }
+        }
+      }
+    }
+
+    return [
+        $search_params,
+        $field_booleans_columns,
+        $field_datetime_columns,
+        $field_generic_columns,
+    ];
+  }
+
+  /**
+   * Return an array of the Form hidden fields and values
+   *
+   * @return array
+   */
+  public function getHiddenFields(): array
+  {
+    // Get the query string and separate the search params from the non-search params
+    $queryParameters = $this->getView()->getRequest()->getQueryParams();
+    $searchableAttributes = $this->getView()->get('vv_searchable_attributes') ?? [];
+
+    // Search attributes collection
+    $alias_params = (new Collection($searchableAttributes))
+      ->filter(fn ($val, $attr) => (\is_array($val) && \array_key_exists('alias', $val)) )
+      ->extract('alias')
+      ->unfold()
+      ->toArray();
+
+    // For the non-search params, we need to search the alias params as well
+    $searchable_parameters = [
+      ...array_keys($searchableAttributes),
+      ...$alias_params
+    ];
+
+    // Pass back the non-search params as hidden fields, but always exclude the page parameter
+    // because we need to start new searches on-page one (or we're likely to end up with a 404).
+    return (new Collection($queryParameters))
+      ->filter(fn($value, $key) => !\in_array($key, $searchable_parameters, true) && $key != 'page')
+      ->toArray();
+  }
+}
\ No newline at end of file
diff --git a/app/templates/Standard/index.php b/app/templates/Standard/index.php
index a262d8d08..d29a64b71 100644
--- a/app/templates/Standard/index.php
+++ b/app/templates/Standard/index.php
@@ -50,14 +50,18 @@
 $linkActions = ['edit', 'view'];
 
 // $vv_template_path will be set for plugins
-$templatePath = $vv_template_path ?? ROOT . DS . "templates" . DS . $modelsName;
+$templatePath = $vv_template_path ?? ROOT . DS . 'templates' . DS . $modelsName;
 
 // Read the index configuration ($indexColumns) and the associated actions for this model
-$incFile = $templatePath . DS . "columns.inc";
+$incFile = $templatePath . DS . 'columns.inc';
+
 if(!is_readable($incFile)) {
   throw new \InvalidArgumentException("$incFile is not readable");
 }
 include($incFile);
+if(isset($indexColumns)) {
+  $this->set('vv_indexColumns', $indexColumns);
+}
 
 // $linkFilter is used for models that belong to a specific parent model (eg: co_id)
 $linkFilter = [];
diff --git a/app/templates/element/filter/checkboxes.php b/app/templates/element/filter/checkboxes.php
new file mode 100644
index 000000000..d39d6f4a9
--- /dev/null
+++ b/app/templates/element/filter/checkboxes.php
@@ -0,0 +1,63 @@
+<?php
+/**
+ * COmanage Registry Top Filters Checkboxes
+ *
+ * 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)
+ */
+
+
+/*
+ * Parameters:
+ * $field_booleans_columns  : array, required
+ */
+
+declare(strict_types = 1);
+
+// Get the query string and separate the search params from the non-search params
+$query = $this->request->getQueryParams();
+
+?>
+
+
+<?php if(!empty($field_booleans_columns)): ?>
+  <div class="top-filters-checkboxes input">
+    <div class="top-filters-checkbox-fields">
+      <?php foreach($field_booleans_columns as $key => $options): ?>
+        <div class="filter-boolean <?= empty($options['active']) ? 'filter-inactive' : 'filter-active' ?>">
+          <div class="form-check form-check-inline">
+            <?php
+            print $this->Form->label($options['label'] ?? $key);
+            print $this->Form->checkbox($key, [
+              'id' => str_replace("_", "-", $key),
+              'class' => 'form-check-input',
+              'checked' => $query[$key] ?? 0,
+              'hiddenField' => false,
+              'required' => false
+            ]);
+            ?>
+          </div>
+        </div>
+      <?php endforeach; ?>
+    </div>
+  </div>
+<?php endif; ?>
diff --git a/app/templates/element/filter/dateSingle.php b/app/templates/element/filter/dateSingle.php
new file mode 100644
index 000000000..dc1aa11bb
--- /dev/null
+++ b/app/templates/element/filter/dateSingle.php
@@ -0,0 +1,64 @@
+<?php
+/**
+ * COmanage Registry Top Filters Checkboxes
+ *
+ * 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)
+ */
+
+
+/*
+ * Parameters:
+ * $columns                 : array, required
+ * $key                     : string, required
+ * $options                 : array, required
+ * $query                   : array, required
+ */
+
+declare(strict_types = 1);
+
+use Cake\Utility\Inflector;
+use App\Lib\Enum\DateTypeEnum;
+
+// $columns = the passed parameter $indexColumns as found in columns.inc;
+// provides overrides for labels and sorting.
+$columns = $vv_indexColumns;
+
+$wrapperCssClass = 'filter-active';
+if(empty($options['active'])) {
+  $wrapperCssClass = 'filter-inactive';
+}
+
+$label = Inflector::humanize(
+  Inflector::underscore(
+    $options['label'] ?? $columns[$key]['label']
+  )
+);
+
+?>
+
+<div class="top-filters-fields-date filter-standard <?= $wrapperCssClass ?>">
+  <?= $this->Form->label($key, $label) ?>
+  <div class="d-flex">
+    <?= $this->Field->dateField($key, DateTypeEnum::DateOnly) ?>
+  </div>
+</div>
diff --git a/app/templates/element/filter/dateTimeFilters.php b/app/templates/element/filter/datetimeGroup.php
similarity index 86%
rename from app/templates/element/filter/dateTimeFilters.php
rename to app/templates/element/filter/datetimeGroup.php
index fb38f0f50..29fcc95de 100644
--- a/app/templates/element/filter/dateTimeFilters.php
+++ b/app/templates/element/filter/datetimeGroup.php
@@ -25,12 +25,24 @@
  * @license       Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
  */
 
+/*
+ * Parameters:
+ * $field_datetime_columns : array, required
+ */
+
+declare(strict_types = 1);
+
+
 use App\Lib\Enum\DateTypeEnum;
 use Cake\Utility\Inflector;
 
+// $columns = the passed parameter $indexColumns as found in columns.inc;
+// provides overrides for labels and sorting.
+$columns = $vv_indexColumns;
+
 ?>
 
-<div class="top-filters-fields-subgroups">
+<?php if(!empty($field_datetime_columns)): ?>
   <?php foreach($field_datetime_columns as $key => $options): ?>
     <div class="input">
       <div class="top-search-date-label">
@@ -42,7 +54,7 @@
           <?php
           // Create a text field to hold our value.
           print $this->Form->label("{$key}_starts_at", __d('field', 'starts_at'), ['class' => 'filter-datepicker-lbl']);
-          print $this->Field->dateField("{$key}_starts_at", DateTypeEnum::DateOnly, $query);
+          print $this->Field->dateField("{$key}_starts_at", DateTypeEnum::DateOnly);
           ?>
         </div>
         <!--     Ends at       -->
@@ -50,10 +62,10 @@
           <?php
           // Create a text field to hold our value.
           print $this->Form->label("{$key}_ends_at", __d('field','ends_at'), ['class' => 'filter-datepicker-lbl']);
-          print $this->Field->dateField("{$key}_ends_at", DateTypeEnum::DateOnly, $query);
+          print $this->Field->dateField("{$key}_ends_at", DateTypeEnum::DateOnly);
           ?>
         </div>
       </div>
     </div>
   <?php endforeach; ?>
-</div>
+<?php endif; ?>
diff --git a/app/templates/element/filter/default.php b/app/templates/element/filter/default.php
new file mode 100644
index 000000000..40aa654df
--- /dev/null
+++ b/app/templates/element/filter/default.php
@@ -0,0 +1,61 @@
+<?php
+/**
+ * COmanage Registry Top Filters Checkboxes
+ *
+ * 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)
+ */
+
+
+/*
+ * Parameters:
+ * $columns                 : array, required
+ * $key                     : string, required
+ * $options                 : array, required
+ */
+
+declare(strict_types = 1);
+
+use Cake\Utility\{Inflector};
+
+// $columns = the passed parameter $indexColumns as found in columns.inc; provides overrides for labels and sorting.
+$columns = $vv_indexColumns;
+
+$wrapperCssClass = 'filter-active';
+if(empty($options['active'])) {
+  $wrapperCssClass = 'filter-inactive';
+}
+
+$label = Inflector::humanize(
+  Inflector::underscore(
+    $options['label'] ?? $columns[$key]['label']
+  )
+);
+
+// Get the Field configuration
+$formParams = $this->Filter->calculateFieldParams($key, $label);
+
+?>
+
+<div class="filter-standard <?= $wrapperCssClass ?>">
+  <?= $this->Form->control($key, $formParams) ?>
+</div>
\ No newline at end of file
diff --git a/app/templates/element/filter/filter.php b/app/templates/element/filter/filter.php
index f0df46ea6..aba4149e8 100644
--- a/app/templates/element/filter/filter.php
+++ b/app/templates/element/filter/filter.php
@@ -24,51 +24,22 @@
  * @since         COmanage Registry v5.0.0
  * @license       Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
  */
-use Cake\Collection\Collection;
-use Cake\Utility\{Inflector, Hash};
-use App\Lib\Enum\DateTypeEnum;
+
+declare(strict_types = 1);
+
+use Cake\Utility\Inflector;
 
 
 // $this->name = Models
 $modelsName = $this->name;
 // $modelName = Model
 $modelName = Inflector::singularize($modelsName);
-// $columns = the passed parameter $indexColumns as found in columns.inc; provides overrides for labels and sorting. 
-$columns = $indexColumns;
-
-// Get the query string and separate the search params from the non-search params
-$query = $this->request->getQueryParams();
-// Search attributes collection
-$search_attributes_collection = new Collection($vv_searchable_attributes);
-$alias_params = $search_attributes_collection->filter(fn ($val, $attr) => (is_array($val) && array_key_exists('alias', $val)) )
-                                             ->extract('alias')
-                                             ->unfold()
-                                             ->toArray();
-// For the non search params we need to search the alias params as well
-$searchable_parameters = [
-  ...array_keys($vv_searchable_attributes),
-  ...$alias_params
-  ];
-$non_search_params = (new Collection($query))->filter( fn($value, $key) => !in_array($key, $searchable_parameters) )
-                                             ->toArray();
 
-// Filter the search params and take params with aliases into consideration
-$search_params = [];
-foreach ($vv_searchable_attributes as $attr => $value) {
-  if(isset($query[$attr])) {
-    $search_params[$attr] = $query[$attr];
-    continue;
-  }
-
-  if(isset($value['alias'])
-     && is_array($value['alias'])) {
-    foreach ($value['alias'] as $alias_key) {
-      if(isset($query[$alias_key])) {
-        $search_params[$attr][$alias_key] = $query[$alias_key];
-      }
-    }
-  }
-}
+// Group Fields by Type
+[ $search_params,
+  $field_booleans_columns,
+  $field_datetime_columns,
+  $field_generic_columns ] = $this->Filter->explodeFieldsByType();
 
 // Begin the form
 print $this->Form->create(null, [
@@ -76,171 +47,50 @@
   'type' => 'get'
 ]);
 
-// Pass back the non-search params as hidden fields, but always exclude the page parameter
-// because we need to start new searches on page one (or we're likely to end up with a 404).
-if(!empty($non_search_params)) {
-  foreach($non_search_params as $param => $value) {
-    if($param != 'page') {
-      print $this->Form->hidden(filter_var($param, FILTER_SANITIZE_SPECIAL_CHARS), array('default' => filter_var($value, FILTER_SANITIZE_SPECIAL_CHARS))) . "\n";
-    }
-  }
+// Hidden Form fields
+foreach($this->Filter->getHiddenFields() as $param => $value) {
+  print $this->Form->hidden(
+      filter_var($param, FILTER_SANITIZE_SPECIAL_CHARS),
+      array('default' => filter_var($value, FILTER_SANITIZE_SPECIAL_CHARS))) . PHP_EOL;
 }
 
-// Boolean to distinguish between search filters and sort parameters
-$hasActiveFilters = false;
 ?>
 
 <div id="<?= $modelName . ucfirst($this->request->getParam('action')) ?>Search" class="top-filters">
   <fieldset>
-    <!--  Top Filters toggle legend  -->
-    <?= $this->element('filter/topFiltersToggle', compact('search_params')) ?>
+    <!--  Filter Legend  -->
+    <?= $this->element('filter/legend', compact('search_params')) ?>
+
+    <!--  Search TextBoxes/DropDowns/e.t.c.  -->
     <div id="top-filters-fields">
+      <!--   Single Search Fields   -->
       <div class="top-filters-fields-subgroups">
       <?php
-        $field_booleans_columns = [];
-        $field_datetime_columns = [];
-        
-        $inactiveFiltersCount = 0; // for re-balancing the columns and submit buttons
-
-        foreach($vv_searchable_attributes as $key => $options) {
-          if($options['type'] == 'boolean') {
-            $field_booleans_columns[$key] = $options;
-            continue;
-          } elseif ($options['type'] == 'timestamp') {
-            $field_datetime_columns[$key] = $options;
-            continue;
-          }
-
-          $wrapperCssClass = 'filter-active';
-          if(empty($options['active'])) {
-            $wrapperCssClass = 'filter-inactive';
-            $inactiveFiltersCount++;
-          }
-
-
-          $label = Inflector::humanize(
-            Inflector::underscore(
-              $options['label'] ?? $columns[$key]['label']
-            )
-          );
-
-          if($options['type'] == 'date') {
-            // Create a text field to hold our date value.
-            print '<div class="top-filters-fields-date filter-standard ' . $wrapperCssClass . '">';
-            print $this->Form->label($key, $label);
-            print '<div class="d-flex">';
-            print $this->Field->dateField($key, DateTypeEnum::DateOnly, $query);
-            print '</div>';
-            print '</div>';
-          } else {
-            // text input
-            $formParams = [
-              'label' => $label,
-              // The default type is text, but we might convert to select below
-              'type' => 'text',
-              'value' => (!empty($query[$key]) ? $query[$key] : ''),
-              'required' => false,
-              'class' => 'form-control'
-            ];
-          }
-          
-          // The populated variables are in plural while the column names are singular
-          // Convention: It is a prerequisite that the vvar should be the plural of the column name
-          $populated_vvar = lcfirst(Inflector::pluralize(Inflector::camelize($key)));
-          if(isset($$populated_vvar)) {
-            // If we have an AutoViewVar matching the name of this key,
-            // convert to a select
-            $formParams['type'] = 'select';
-            $formParams['options'] = $$populated_vvar;
-            if(isset($vv_searchable_attributes_extras[$key]['options'])) {
-              // Flatten the custom options
-              $customOptionsFlattened = Hash::flatten($vv_searchable_attributes_extras[$key]['options']);
-              // Get the key of the place holder string
-              $dataKey = array_search('@DATA@', $customOptionsFlattened, true);
-              if($dataKey !== false) {
-                $customOptionsFlattened[$dataKey] = $formParams['options'];
-                $formParams['options'] = Hash::expand($customOptionsFlattened);
-              }
-            }
-            // Allow empty so a filter doesn't require (eg) SOR
-            $formParams['empty'] = true;
-          }
-          
-          if($options['type'] != 'date') {
-            print '<div class="filter-standard ' . $wrapperCssClass . '">';
-            print $this->Form->control($key, $formParams);
-            print '</div>';
-          }
+        foreach($field_generic_columns as $key => $options) {
+          $elementArguments = compact('options', 'key');
+          print match($options['type']) {
+            'date'      => $this->element('filter/dateSingle', $elementArguments),
+            default     => $this->element('filter/default', $elementArguments),
+          };
         }
       ?>
-      <?php if(!empty($field_booleans_columns)): ?>
-        <div class="top-filters-checkboxes input">
-          <div class="top-filters-checkbox-fields">
-            <?php foreach($field_booleans_columns as $key => $options): ?>
-              <div class="filter-boolean <?= empty($options['active']) ? 'filter-inactive' : 'filter-active' ?>">
-                <div class="form-check form-check-inline">
-                  <?php
-                    print $this->Form->label($options['label'] ?? $key);
-                    print $this->Form->checkbox($key, [
-                      'id' => str_replace("_", "-", $key),
-                      'class' => 'form-check-input',
-                      'checked' => $query[$key] ?? 0,
-                      'hiddenField' => false,
-                      'required' => false
-                    ]);
-                  ?>
-                </div>
-              </div>
-            <?php endforeach; ?>
-          </div>
-        </div>
-      <?php endif; ?>
+
+      <!--   Checkboxes   -->
+      <?= $this->element('filter/checkboxes', compact('field_booleans_columns')) ?>
       </div>
 
-      <?php
-      // Date Time filtering block
-      if (!empty($field_datetime_columns)) {
-        print $this->element(
-          'filter/dateTimeFilters',
-          compact('field_datetime_columns', 'query')
-        );
-      }
+      <!--    Date/ Time search textboxes Group -->
+      <div class="top-filters-fields-subgroups">
+      <?= $this->element('filter/datetimeGroup', compact('field_datetime_columns'))
       ?>
+      </div>
 
-      <?php $rebalanceColumns = (((count($vv_searchable_attributes) - $inactiveFiltersCount) % 2 == 1) && empty($field_booleans_columns)) ? ' class="tss-rebalance"' : ''; ?>
-      <div id="top-filters-submit"<?= $rebalanceColumns ?>>
-        
-      <?php
-        // Order of the submitted buttons is important here: the Enter key will submit the first (and we want the tab order to follow suit).
-        // We reverse the visual order of all these buttons with CSS (flex-direction: row-reverse;).
-
-        // search button (submit)
-        $args = array();
-        $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 = array();
-        $args['id'] = 'top-filters-clear';
-        $args['class'] = 'clear-button spin btn btn-default';
-        $args['aria-label'] = __d('operation', 'clear');
-        $args['onclick'] = 'clearTopSearch(this.form)';
-        print $this->Form->button(__d('operation', 'clear'),$args);
-
-        // Options dropdown list
-        if(!empty($vv_searchable_attributes)) {
-          print $this->element(
-            'filter/filterOptions',
-            compact('vv_searchable_attributes', 'field_booleans_columns')
-          );
-        }
+      <!--    Footer / Submit Buttons    -->
+      <?= $this->element('filter/footerButtons', compact('field_booleans_columns'))
       ?>
-      </div>
     </div>
   </fieldset>
 </div>
 
-<?= $this->Form->end(); ?>
+<?= $this->Form->end() ?>
 
diff --git a/app/templates/element/filter/footerButtons.php b/app/templates/element/filter/footerButtons.php
new file mode 100644
index 000000000..168895bcb
--- /dev/null
+++ b/app/templates/element/filter/footerButtons.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * COmanage Registry Top Filters Submit area buttons Element
+ *
+ * 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)
+ */
+
+
+/*
+ * Parameters:
+ * $field_booleans_columns : array, required
+ */
+
+declare(strict_types = 1);
+
+use Cake\Collection\Collection;
+
+$classes = '';
+$inactiveFiltersCount = (new Collection($vv_searchable_attributes))->filter(fn($col) => boolval($col['active']) === false)
+                                                                   ->count();
+if ((count($vv_searchable_attributes) - $inactiveFiltersCount) % 2 === 1
+    &&
+    empty($field_booleans_columns)
+) {
+  $classes .= ' class="tss-rebalance';
+}
+
+?>
+
+<div id="top-filters-submit"<?= $classes ?>>
+
+  <?php
+  // Order of the submitted buttons is important here: the Enter key will submit the first (and we want the tab order to follow suit).
+  // We reverse the visual order of all these buttons with CSS (flex-direction: row-reverse;).
+
+  // search button (submit)
+  $args = array();
+  $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 = array();
+  $args['id'] = 'top-filters-clear';
+  $args['class'] = 'clear-button spin btn btn-default';
+  $args['aria-label'] = __d('operation', 'clear');
+  $args['onclick'] = 'clearTopSearch(this.form)';
+  print $this->Form->button(__d('operation', 'clear'), $args);
+
+  // Options dropdown list
+  if(!empty($vv_searchable_attributes)) {
+    print $this->element(
+      'filter/options',
+      compact('field_booleans_columns')
+    );
+  }
+  ?>
+</div>
\ No newline at end of file
diff --git a/app/templates/element/filter/topFiltersToggle.php b/app/templates/element/filter/legend.php
similarity index 90%
rename from app/templates/element/filter/topFiltersToggle.php
rename to app/templates/element/filter/legend.php
index 442b474e4..06a84d9c8 100644
--- a/app/templates/element/filter/topFiltersToggle.php
+++ b/app/templates/element/filter/legend.php
@@ -25,7 +25,14 @@
  * @license       Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
  */
 
-// $search_params passed as a parameter
+
+/*
+ * Parameters:
+ * $search_params : array, required
+ */
+
+declare(strict_types = 1);
+
 
 $hasActiveFilters = false;
 
@@ -44,12 +51,12 @@
         foreach($search_params as $key => $params) {
           // We have active filters - not just a sort.
           $hasActiveFilters = true;
-          print $this->element('filter/activeTopButton', compact('key', 'params'));
+          print $this->element('filter/topButtons', compact('key', 'params'));
         }
         ?>
       <?php if($hasActiveFilters): ?>
         <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]); ?>
+            <?= __d('operation', 'clear.filters',[2]) ?>
           </button>
       <?php endif; ?>
       </span>
diff --git a/app/templates/element/filter/filterOptions.php b/app/templates/element/filter/options.php
similarity index 99%
rename from app/templates/element/filter/filterOptions.php
rename to app/templates/element/filter/options.php
index 327f456db..0337d5f6b 100644
--- a/app/templates/element/filter/filterOptions.php
+++ b/app/templates/element/filter/options.php
@@ -25,6 +25,8 @@
  * @license       Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
  */
 
+declare(strict_types = 1);
+
 ?>
 
 <div id="top-filters-options-container">
diff --git a/app/templates/element/filter/activeTopButton.php b/app/templates/element/filter/topButtons.php
similarity index 98%
rename from app/templates/element/filter/activeTopButton.php
rename to app/templates/element/filter/topButtons.php
index 2c3895d95..93baf9a05 100644
--- a/app/templates/element/filter/activeTopButton.php
+++ b/app/templates/element/filter/topButtons.php
@@ -25,6 +25,8 @@
  * @license       Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
  */
 
+declare(strict_types = 1);
+
 use Cake\Utility\{Inflector, Hash};
 
 // Construct aria-controls string