From c6c2af7adc4f9381a5f715efefa55fcd7a3a52ce Mon Sep 17 00:00:00 2001 From: Arlen Johnson Date: Fri, 29 Sep 2023 11:04:33 -0400 Subject: [PATCH] Add filter selection UI to the top-filters on an index view (CFM-296) (#130) --- app/resources/locales/en_US/menu.po | 6 ++ app/src/Lib/Traits/SearchFilterTrait.php | 53 +++++++++++++- app/src/Model/Table/PeopleTable.php | 28 ++++++++ app/templates/element/filter.php | 88 +++++++++++++++++++----- app/templates/element/javascript.php | 26 +++++++ app/webroot/css/co-base.css | 34 +++++++-- app/webroot/css/co-responsive.css | 23 ++++++- 7 files changed, 232 insertions(+), 26 deletions(-) diff --git a/app/resources/locales/en_US/menu.po b/app/resources/locales/en_US/menu.po index 60bdcfedc..72149952c 100644 --- a/app/resources/locales/en_US/menu.po +++ b/app/resources/locales/en_US/menu.po @@ -27,6 +27,9 @@ msgid "artifacts" msgstr "Available {0} Artifacts" +msgid "available.filters" +msgstr "Available Filters" + msgid "co.artifacts" msgstr "Artifacts" @@ -201,6 +204,9 @@ msgstr "User Menu" msgid "menu.toggle" msgstr "Toggle menu collapse button" +msgid "options" +msgstr "Options" + msgid "registries" msgstr "Available {0} Registries" diff --git a/app/src/Lib/Traits/SearchFilterTrait.php b/app/src/Lib/Traits/SearchFilterTrait.php index 00d52957c..2dff433ce 100644 --- a/app/src/Lib/Traits/SearchFilterTrait.php +++ b/app/src/Lib/Traits/SearchFilterTrait.php @@ -35,6 +35,19 @@ trait SearchFilterTrait { // Array (and configuration) of permitted search filters private $searchFilters = array(); + // Optional filter configuration that dictates display state and allows for related models + private $filterConfig = array(); + + /** + * Get explicilty defined filter configuration defined in the table class. + * + * @since COmanage Registry v5.0.0 + */ + + public function getFilterConfig(): array { + return $this->filterConfig; + } + /** * Obtain the set of permitted search attributes. * @@ -44,14 +57,42 @@ trait SearchFilterTrait { public function getSearchableAttributes(string $controller, string $vv_tz=null): array { $modelname = Inflector::classify(Inflector::underscore($controller)); + $filterConfig = $this->getFilterConfig(); + + // Gather up related models defined in the $filterConfig + // XXX For now, we'll list these first - but we should probably provide a better way to order these. + foreach ($filterConfig as $field => $f) { + if($f['type'] == 'relatedModel') { + $fieldName = Inflector::classify(Inflector::underscore($field)); + $this->searchFilters[$field] = [ + 'type' => 'string', // XXX for now - this needs to be looked up. + 'label' => \App\Lib\Util\StringUtilities::columnKey($fieldName, $field, $vv_tz, true), + 'active' => isset($f['active']) ? $f['active'] : true + ]; + } + } + foreach ($this->filterMetadataFields() as $column => $type) { // If the column is an array then we are accessing the Metadata fields. Skip if(is_array($type)) { continue; } + + // Set defaults + $fieldIsActive = true; + + // Gather filter configurations, if any, for local table fields. + // An active field is visible in the filter form. An inactive field is not but can be enabled. + if(!empty($filterConfig[$column])) { + if(isset($filterConfig[$column]['active'])) { + $fieldIsActive = $filterConfig[$column]['active']; + } + } + $this->searchFilters[$column] = [ 'type' => $type, - 'label' => \App\Lib\Util\StringUtilities::columnKey($modelname, $column, $vv_tz, true) + 'label' => \App\Lib\Util\StringUtilities::columnKey($modelname, $column, $vv_tz, true), + 'active' => $fieldIsActive ]; // For the date fields we search ranges @@ -63,6 +104,16 @@ public function getSearchableAttributes(string $controller, string $vv_tz=null): return $this->searchFilters ?? []; } + + /** + * Set explicilty defined filter configuration defined in the table class. + * + * @since COmanage Registry v5.0.0 + */ + + public function setFilterConfig(array $filterConfig): void { + $this->filterConfig = $filterConfig; + } /** * Build a query where() clause for the configured attribute. diff --git a/app/src/Model/Table/PeopleTable.php b/app/src/Model/Table/PeopleTable.php index 066393201..7347a53b8 100644 --- a/app/src/Model/Table/PeopleTable.php +++ b/app/src/Model/Table/PeopleTable.php @@ -154,6 +154,34 @@ public function initialize(array $config): void { ] ]); + // XXX expand/revise this as needed to work best with looking up the related models + $this->setFilterConfig([ + 'family' => [ + 'type' => 'relatedModel', + 'model' => 'names', + 'active' => true + ], + 'given' => [ + 'type' => 'relatedModel', + 'model' => 'names', + 'active' => true + ], + 'mail' => [ + 'type' => 'relatedModel', + 'model' => 'email_addresses', + 'active' => true + ], + 'identifier' => [ + 'type' => 'relatedModel', + 'model' => 'identifiers', + 'active' => true + ], + 'timezone' => [ + 'type' => 'field', + 'active' => false + ] + ]); + $this->setPermissions([ // Actions that operate over an entity (ie: require an $id) // See also CFM-126 diff --git a/app/templates/element/filter.php b/app/templates/element/filter.php index c316e9e0f..a748cc452 100644 --- a/app/templates/element/filter.php +++ b/app/templates/element/filter.php @@ -145,12 +145,17 @@ $field_booleans_columns = []; $field_datetime_columns = []; + $inactiveFiltersCount = 0; // for re-balancing the columns and submit buttons + if(!empty($columns)) { // To make our filters consistently ordered with the index columns, sort the $vv_searchable_attributes // by the keys of columns.inc $indexColumns (passed in to this View element as $indexColumns and referenced // as "$columns"). The fields found in $columns will be placed first in the resulting array. // The result should only include fields that exist in the original $vv_searchable_attributes array. - $vv_searchable_attributes = array_intersect_key(array_replace(array_flip(array_keys($columns)), $vv_searchable_attributes), $vv_searchable_attributes); + + // XXX Turn this off. We should have more arbitrary control over the order of the filters, and for the moment + // it is better (at least for now) to let the explicitly defined filters come first. + // $vv_searchable_attributes = array_intersect_key(array_replace(array_flip(array_keys($columns)), $vv_searchable_attributes), $vv_searchable_attributes); } foreach($vv_searchable_attributes as $key => $options) { @@ -188,8 +193,15 @@ 'pickerType' => \App\Lib\Enum\DateTypeEnum::DateOnly, 'pickerFloor' => $pickerFloor ]; + + $wrapperCssClass = 'filter-active'; + if(empty($options['active'])) { + $wrapperCssClass = 'filter-inactive'; + $inactiveFiltersCount++; + } + // Create a text field to hold our date value. - print '
'; + print '
'; print $this->Form->label($key, !empty($columns[$key]['label']) ? $columns[$key]['label'] : $options['label']); print '
'; print $this->Form->text($key, $opts) . $this->element('datePicker', $date_args); @@ -219,26 +231,36 @@ $formParams['empty'] = true; } + $wrapperCssClass = 'filter-active'; + if(empty($options['active'])) { + $wrapperCssClass = 'filter-inactive'; + $inactiveFiltersCount++; + } + if($options['type'] != 'date') { + print '
'; print $this->Form->control($key, $formParams); + print '
'; } } ?> -
-
+
+
$options): ?> -
- Form->label(!empty($columns[$key]['label']) ? $columns[$key]['label'] : $key); - print $this->Form->checkbox($key, [ - 'id' => str_replace("_", "-", $key), - 'class' => 'form-check-input', - 'checked' => $query[$key] ?? 0, - 'hiddenField' => false, - 'required' => false - ]); - ?> +
+
+ Form->label(!empty($columns[$key]['label']) ? $columns[$key]['label'] : $key); + print $this->Form->checkbox($key, [ + 'id' => str_replace("_", "-", $key), + 'class' => 'form-check-input', + 'checked' => $query[$key] ?? 0, + 'hiddenField' => false, + 'required' => false + ]); + ?> +
@@ -332,26 +354,58 @@
- +
> + 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); ?> + + +
+ + +
+
Form->end(); ?> + + + + diff --git a/app/templates/element/javascript.php b/app/templates/element/javascript.php index 38efe508e..edb71822a 100644 --- a/app/templates/element/javascript.php +++ b/app/templates/element/javascript.php @@ -149,6 +149,32 @@ $("#top-filters-toggle .top-filters-active-filter").hide(); $("#top-filters-clear").click(); }); + + // Hide and show filter fields using the Available Filters menu + $('#top-filters-form .filter-selector').click(function(e) { + e.stopPropagation(); + + // Get the checkbox and the target filter id held in the checkbox value + let cb = $(this).find('input'); + let target = $(cb).val(); + + // Toggle the active and inactive state of the target filter + if(cb.prop('checked')) { + $('#' + target).closest('.filter-inactive').removeClass('filter-inactive').addClass('filter-active'); + } else { + $('#' + target).closest('.filter-active').removeClass('filter-active').addClass('filter-inactive'); + } + + // Toggle the submit container rebalance class so long as there are no datetime pickers + if(!$('.top-filters-fields-dates').length) { + // Count the number of visible fields, and apply the rebalance class on an odd number + if($('#top-filters-fields .filter-active').length % 2 == 1) { + $('#top-filters-submit').addClass("tss-rebalance"); + } else { + $('#top-filters-submit').removeClass("tss-rebalance"); + } + } + }); // Make all submit buttons pretty (Bootstrap) $("input:submit").addClass("spin submit-button btn btn-primary"); diff --git a/app/webroot/css/co-base.css b/app/webroot/css/co-base.css index 331af0b29..3dae0eb30 100644 --- a/app/webroot/css/co-base.css +++ b/app/webroot/css/co-base.css @@ -771,7 +771,7 @@ ul.form-list li.alert-banner .co-alert { #search-results .accordion-body { padding: 1em; } -/* TOP FILTER */ +/* TOP FILTERS */ .top-filters { margin-top: 0.5em; padding: 0; @@ -816,9 +816,13 @@ ul.form-list li.alert-banner .co-alert { display: none; padding: 0.25em 0.5em; } +#top-filters-fields .filter-inactive { + display: none; +} #top-filters-submit { - float: right; - width: 200px; + display: flex; + flex-direction: row-reverse; + justify-content: right; margin-bottom: 0.75em; } .top-filters input[type=text], @@ -848,14 +852,30 @@ ul.form-list li.alert-banner .co-alert { background-color: var(--cmg-color-highlight-002); } .top-filters .submit-button, -.top-filters .clear-button { - float: right; +.top-filters .clear-button, +.top-filters .options-button { font-size: 0.9em; width: 80px; height: 28px; margin: 1em 0.5em; padding: 0; } +.top-filters .options-button { + width: 90px; +} +#top-filters-options { + font-size: 0.9em; + padding: 0 1em 0.5em 1em; +} +#top-filters-options-container .dropdown-menu { + padding: 0; +} +#top-filters-options-container h4 { + font-size: 0.9em; + background-color: var(--cmg-color-bg-005); + text-align: center; + padding: 0.4em 0; +} .top-filters .top-filters-checkboxes { margin-top: 0.5em; } @@ -898,6 +918,10 @@ ul.form-list li.alert-banner .co-alert { background-color: var(--cmg-color-bg-006); color: var(--cmg-color-btn-bg-002) !important; } +.top-filters-active-filter.btn.btn-sm:focus { + border: 1px dotted var(--cmg-color-btn-bg-002); + box-shadow: 0 0 0 .25rem rgba(13,110,253,.25); +} .top-filters-active-filter-value::before { content: ":"; } diff --git a/app/webroot/css/co-responsive.css b/app/webroot/css/co-responsive.css index c18959aeb..44e9cd472 100644 --- a/app/webroot/css/co-responsive.css +++ b/app/webroot/css/co-responsive.css @@ -323,7 +323,7 @@ .call-to-action { margin-bottom: 0; } - /* TOP SEARCH */ + /* TOP FILTERS */ .top-filters-fields-subgroups { display: grid; grid-template-columns: 1fr 1fr; @@ -331,12 +331,23 @@ padding: 0 0.5em; align-items: center; } - .top-search-checkboxes { + .top-filters-checkbox-fields { + display: flex; + } + .top-filters-checkboxes { padding: 0.5em 0 0; } + #top-filters-submit { + position: relative; + } + .top-filters .submit-button, + .top-filters .clear-button, + .top-filters .options-button { + margin: 1em 0.25em; + } #top-filters-submit.tss-rebalance { margin-top: -3.5em; - position: relative; + float: right; } /* CO CONFIGURATION DASHBOARD */ .config-menu { @@ -418,6 +429,12 @@ ul.form-list li .field { column-gap: 2em; } + /* TOP FILTERS */ + .top-filters .submit-button, + .top-filters .clear-button, + .top-filters .options-button { + margin: 1em 0.5em; + } /* DATE / TIME PICKER */ .cm-time-picker-panel { right: -250px;