Skip to content

Add filter selection UI to the top-filters on an index view (CFM-296) #130

Merged
Show file tree
Hide file tree
Changes from all commits
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
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 @@ -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;
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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: ":";
}
Expand Down
Loading