Skip to content

Commit

Permalink
Add filter selection UI to the top-filters on an index view (CFM-296) (
Browse files Browse the repository at this point in the history
  • Loading branch information
arlen committed Oct 6, 2023
1 parent 18d0893 commit 6ca2ac7
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 26 deletions.
6 changes: 6 additions & 0 deletions app/resources/locales/en_US/menu.po
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
msgid "artifacts"
msgstr "Available {0} Artifacts"

msgid "available.filters"
msgstr "Available Filters"

msgid "co.artifacts"
msgstr "Artifacts"

Expand Down Expand Up @@ -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"

Expand Down
53 changes: 52 additions & 1 deletion app/src/Lib/Traits/SearchFilterTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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
Expand All @@ -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.
Expand Down
28 changes: 28 additions & 0 deletions app/src/Model/Table/PeopleTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
88 changes: 71 additions & 17 deletions app/templates/element/filter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 '<div class="top-filters-fields-date">';
print '<div class="top-filters-fields-date ' . $wrapperCssClass . '">';
print $this->Form->label($key, !empty($columns[$key]['label']) ? $columns[$key]['label'] : $options['label']);
print '<div class="d-flex">';
print $this->Form->text($key, $opts) . $this->element('datePicker', $date_args);
Expand Down Expand Up @@ -219,26 +231,36 @@
$formParams['empty'] = true;
}

$wrapperCssClass = 'filter-active';
if(empty($options['active'])) {
$wrapperCssClass = 'filter-inactive';
$inactiveFiltersCount++;
}

if($options['type'] != 'date') {
print '<div class="' . $wrapperCssClass . '">';
print $this->Form->control($key, $formParams);
print '</div>';
}
}
?>
<?php if(!empty($field_booleans_columns)): ?>
<div class="top-search-checkboxes input">
<div class="top-search-checkbox-fields">
<div class="top-filters-checkboxes input">
<div class="top-filters-checkbox-fields">
<?php foreach($field_booleans_columns as $key => $options): ?>
<div class="form-check form-check-inline">
<?php
print $this->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
]);
?>
<div class="<?= empty($options['active']) ? 'filter-active' : 'filter-inactive' ?>">
<div class="form-check form-check-inline">
<?php
print $this->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
]);
?>
</div>
</div>
<?php endforeach; ?>
</div>
Expand Down Expand Up @@ -332,26 +354,58 @@
</div>
<?php endif; ?>

<?php $rebalanceColumns = ((count($vv_searchable_attributes)) % 2 != 0) ? ' class="tss-rebalance"' : ''; ?>
<?php $rebalanceColumns = ((count($vv_searchable_attributes) - $inactiveFiltersCount) % 2 != 0) ? ' class="tss-rebalance"' : ''; ?>
<div id="top-filters-submit"<?php print $rebalanceColumns ?>>

<?php
$args = array();
// Order of the submit 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);
?>

<?php if(!empty($vv_searchable_attributes)): ?>
<div id="top-filters-options-container">
<button id="top-filters-options-button" class="btn btn-default options-button dropdown-toggle"
type="button" data-bs-toggle="dropdown" aria-expanded="false">
<?= __d('menu', 'options') ?>
</button>
<div class="dropdown-menu dropdown-menu-lg-end" aria-labelledby="top-filters-options-button">
<h4><?= __d('menu','available.filters') ?></h4>
<div id="top-filters-options">
<?php foreach($vv_searchable_attributes as $key => $options): ?>
<?php if($options['type'] == 'timestamp') continue; // skip timestamp types ?>
<div class="form-check filter-selector">
<input class="form-check-input" type="checkbox" value="<?= Cake\Utility\Inflector::dasherize($key) ?>" id="filter-selector-<?= $key ?>"<?= !empty($options['active']) ? ' checked' : '' ?>>
<label class="form-check-label" for="filter-selector-<?= $key ?>">
<?= $options['label'] ?>
</label>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
</fieldset>
</div>

<?= $this->Form->end(); ?>

<!--<pre>-->
<!-- --><?//= print_r($vv_searchable_attributes) ?>
<!--</pre>-->
26 changes: 26 additions & 0 deletions app/templates/element/javascript.php
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
34 changes: 29 additions & 5 deletions app/webroot/css/co-base.css
Original file line number Diff line number Diff line change
Expand Up @@ -774,7 +774,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;
Expand Down Expand Up @@ -819,9 +819,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],
Expand Down Expand Up @@ -851,14 +855,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;
}
Expand Down Expand Up @@ -901,6 +921,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: ":";
}
Expand Down
Loading

0 comments on commit 6ca2ac7

Please sign in to comment.