Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Matchgrid Records: implement filtering/search on index view (CO-1871)
arlen committed Sep 23, 2021
1 parent 8a6c157 commit b95c469
Showing 8 changed files with 431 additions and 6 deletions.
21 changes: 21 additions & 0 deletions app/src/Locale/en_US/default.po
@@ -324,6 +324,9 @@ msgstr "Requested Reference ID must already be in use, or be the keyword 'new'"
msgid "match.fd.action"
msgstr "Action"

msgid "match.fd.all"
msgstr "All"

msgid "match.fd.alphanumeric"
msgstr "Alphanumeric"

@@ -462,9 +465,15 @@ msgstr "(Please select a value)"
msgid "match.fd.sor"
msgstr "System of Record"

msgid "match.fd.sor.abbr"
msgstr "SOR"

msgid "match.fd.sorid"
msgstr "System of Record ID"

msgid "match.fd.sorid.abbr"
msgstr "SOR ID"

msgid "match.fd.status"
msgstr "Status"

@@ -502,6 +511,9 @@ msgstr "There are no matchgrids currently defined."
msgid "match.in.pagination.format"
msgstr "Page {{page}} of {{pages}}, Viewing {{start}}-{{end}} of {{count}}"

msgid "match.in.records.none"
msgstr "No records to display"

### Operations (Commands)
msgid "match.op.add.a"
msgstr "Add New {0}"
@@ -518,6 +530,12 @@ msgstr "Build"
msgid "match.op.build.confirm"
msgstr "Are you sure you wish to (re)build this matchgrid?"

msgid "match.op.clear"
msgstr "Clear"

msgid "match.op.clear.filters"
msgstr "{0,plural,=1{Clear Filter} other{Clear Filters}}"

msgid "match.op.configure.a"
msgstr "Configure {0}"

@@ -545,6 +563,9 @@ msgstr "Edit {0}"
msgid "match.op.first"
msgstr "First"

msgid "match.op.filter"
msgstr "Filter"

msgid "match.op.go"
msgstr "Go"

44 changes: 44 additions & 0 deletions app/src/Template/Element/javascript.ctp
@@ -118,6 +118,50 @@
$("#user-panel").hide();
}
});

// TOP SEARCH FILTER FORM
// Send only non-empty fields in the form
$("#top-search-form").submit(function() {
$("#top-search-form *").filter(':input').each(function () {
if($(this).val() == '') {
$(this).prop('disabled',true);
}
});
});

// Toggle the top search filter box
$("#top-search-toggle, #top-search-toggle button.cm-toggle").click(function(e) {
e.preventDefault();
e.stopPropagation();
if ($("#top-search-fields").is(":visible")) {
$("#top-search-fields").hide();
$("#top-search-toggle button.cm-toggle").attr("aria-expanded","false");
$("#top-search-toggle button.cm-toggle .drop-arrow").text("arrow_drop_down");
} else {
$("#top-search-fields").show();
$("#top-search-toggle button.cm-toggle").attr("aria-expanded","true");
$("#top-search-toggle button.cm-toggle .drop-arrow").text("arrow_drop_up");
}
});

// Clear a specific top search filter by clicking the filter button
$("#top-search-toggle button.top-search-active-filter").click(function(e) {
e.preventDefault();
e.stopPropagation();
$(this).hide();
filterId = '#' + $(this).attr("aria-controls");
$(filterId).val("");
$(this).closest('form').submit();
});

// Clear all top filters from the filter bar
$("#top-search-clear-all-button").click(function(e) {
e.preventDefault();
e.stopPropagation();
$(this).hide();
$("#top-search-toggle .top-search-active-filter").hide();
$("#top-search-clear").click();
});

// Accordion
$(".accordion").accordion();
178 changes: 178 additions & 0 deletions app/src/Template/Element/search.ctp
@@ -0,0 +1,178 @@
<?php
/**
* COmanage Match Search 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 http://www.internet2.edu/comanage COmanage Project
* @package match
* @since COmanage Match v1.0.0
* @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/

use \Cake\Utility\Inflector;

// Globals
global $cm_lang, $cm_texts;

// Get a pointer to our controller
$controller = $this->request->getParam('controller');
$req = Inflector::singularize($controller);

// Get the query string and separate the search params from the non-search params
$query = $this->request->getQueryParams();
$non_search_params = array_diff_key($query, $vv_search_fields);
$search_params = array_intersect_key($query, $vv_search_fields);

// Begin the form
print $this->Form->create($req, [
'id' => 'top-search-form',
'type' => 'get',
'url' => ['action' => $this->request->action]
]);

// 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";
}
}
}

// Boolean to distinguish between search filters and sort parameters
// XXX may no longer need this
$hasActiveFilters = false;
?>

<div id="<?php print $req . ucfirst($this->request->action); ?>Search" class="top-search">
<fieldset onclick="event.stopPropagation();">
<legend id="top-search-toggle">
<em class="material-icons">search</em>
<?= __('match.op.filter');?>

<?php if(!empty($search_params)):?>
<span id="top-search-active-filters">
<?php foreach($search_params as $key => $params): ?>
<?php
// Construct aria-controls string
$key_fields = explode('.', $key);
$aria_controls = $key_fields[0] . ucfirst($key_fields[1]);

// We have active filters - not just a sort.
$hasActiveFilters = true;
?>
<button class="top-search-active-filter deletebutton spin" type="button" aria-controls="<?php print $aria_controls; ?>" title="<?= __('match.op.clear.filters',[1]);?>">
<span class="top-search-active-filter-title"><?php print $vv_search_fields[$key]['label']; ?></span>
<span class="top-search-active-filter-value">
<?php
$value = $params;
// Get user friendly name from an Enumerator Class - XXX will need updating for Match if it becomes used
// XXX How should we handle dynamic Enumerator lists?
if(isset($vv_search_fields[$key]['enum'])
&& isset($cm_texts[ $cm_lang ][$vv_search_fields[$key]['enum']][$params])) {
$value = $cm_texts[ $cm_lang ][$vv_search_fields[$key]['enum']][$params];
print filter_var($value, FILTER_SANITIZE_SPECIAL_CHARS);
continue;
}
// Get user friendly name from the dropdown Select List
// XXX Currently we do not have a use case where the grouping name would create a namespace
if (isset($vv_search_fields[$key]['options'])) {
// Outside of any groups
if (isset($vv_search_fields[$key]['options'][$value])) {
print filter_var($vv_search_fields[$key]['options'][$value], FILTER_SANITIZE_SPECIAL_CHARS);
} else {
// Inside a group
foreach(array_keys($vv_search_fields[$key]['options']) as $optgroup) {
if( is_array($vv_search_fields[$key]['options'][$optgroup])
&& isset($vv_search_fields[$key]['options'][$optgroup][$value]) ) {
print filter_var($vv_search_fields[$key]['options'][$optgroup][$value], FILTER_SANITIZE_SPECIAL_CHARS);
print $this->Html->tag('span','(' . $optgroup . ')', array('class' => 'ml-1') );
break;
}
}
}
} else {
print filter_var($value, FILTER_SANITIZE_SPECIAL_CHARS);
}
?>
</span>
</button>
<?php endforeach; ?>
<?php if($hasActiveFilters): ?>
<button id="top-search-clear-all-button" class="filter-clear-all-button spin btn" type="button" aria-controls="top-search-clear" onclick="event.stopPropagation()">
<?= __('match.op.clear.filters',[2]);?>
</button>
<?php endif; ?>
</span>
<?php endif; ?>
<button class="cm-toggle" aria-expanded="false" aria-controls="top-search-fields" type="button"><em class="material-icons drop-arrow">arrow_drop_down</em></button>
</legend>
<div id="top-search-fields">
<div id="top-search-fields-subgroups">
<?php
$i = 0;
$field_subgroup_columns = array();
$skippedFields = 0;
foreach($vv_search_fields as $key => $options) {
// Exclude explicitly skipped fields
if(array_key_exists('searchSkip',$options)) {
$skippedFields++;
continue;
}
$formParams = array(
'label' => !empty($options['searchLabel']) ? $options['searchLabel'] : $options['label'],
'type' => !empty($options['searchType']) ? $options['searchType'] : 'text',
'value' => (!empty($query[$key]) ? $query[$key] : ''),
'required' => false,
);
if(isset($options['searchEmpty'])) {
$formParams['empty'] = $options['searchEmpty'];
}
if(isset($options['searchOptions'])) {
$formParams['options'] = $options['searchOptions'];
}

print $this->Form->input($key, $formParams);
}
?>
</div>
<?php $rebalanceColumns = ((count($vv_search_fields)-$skippedFields) % 2 != 0) ? ' class="tss-rebalance"' : ''; ?>
<div id="top-search-submit"<?php print $rebalanceColumns ?>>
<?php
$args = array();
// search button (submit)
$args['id'] = 'top-search-filter-button';
$args['aria-label'] = __('match.op.filter');
$args['class'] = 'submit-button spin btn btn-primary';
print $this->Form->submit(__('match.op.filter'),$args);

// clear button
$args['id'] = 'top-search-clear';
$args['class'] = 'clear-button spin btn btn-default';
$args['aria-label'] = __('match.op.clear');
$args['onclick'] = 'clearTopSearch(this.form)';
print $this->Form->button(__('match.op.clear'),$args);
?>
</div>
</div>
</fieldset>
</div>

<?php print $this->Form->end();?>
28 changes: 23 additions & 5 deletions app/src/Template/MatchgridRecords/columns.inc
@@ -28,21 +28,38 @@
// We need to add matchgrid ID to all links (since ID is unique only within a matchgrid)
// eg: matchgrid-records/edit/48?matchgrid_id=3
$forcePrimaryLink = true;

// Turn on the search/filter box for this index view
// If $enableSearch is true, search input labels will be 'label' unless overridden by 'searchLabel'.
// If $enableSearch is true, a specific field can be excluded from the search by adding 'searchSkip' => true.
$enableSearch = true;

// Note we don't need to do any special filtering, since MatchgridRecordsController will
// force the model's table only to the one associated with the matchgrid
$indexColumns = [
'id' => [
'type' => 'echo'
'label' => __('match.fd.id'),
'type' => 'echo',
'searchSkip' => true
],
'sor' => [
'type' => 'echo'
'label' => __('match.fd.sor.abbr'),
'type' => 'echo',
'searchType' => 'select',
'searchLabel' => __('match.fd.sor'),
'searchEmpty' => __('match.fd.all'),
'searchOptions' => $sor
],
'sorid' => [
'type' => 'echo'
'label' => __('match.fd.sorid.abbr'),
'type' => 'echo',
'searchType' => 'text',
'searchLabel' => __('match.fd.sorid')
],
'referenceid' => [
'type' => 'echo'
'label' => __('match.fd.referenceid'),
'type' => 'echo',
'searchType' => 'text'
]
];

@@ -51,7 +68,8 @@ foreach($attributes as $attr) {
if($attr->index_display === true) {
$indexColumns[$attr->name] = [
'label' => $attr->name,
'type' => 'echo'
'type' => 'echo',
'searchType' => 'text'
];
}
}
16 changes: 15 additions & 1 deletion app/src/Template/Standard/index.ctp
@@ -42,6 +42,10 @@ $modelsName = $this->name;
// $tablefk = model_id
$tableFK = Inflector::singularize($vv_tablename) . "_id";

// Do we have records for this index? This will be set to true during render if we do.
// Otherwise, we'll print out a "no records" message.
$recordsExist = false;

// Our default link actions, in order of preference, unless the column config overrides it
$linkActions = ['edit', 'view'];

@@ -132,7 +136,13 @@ function _column_key($modelsName, $c, $tz=null) {
</div>
<?php endforeach; // $banners ?>
<?php endif; // $banners ?>


<!-- Search block -->
<?php if(!empty($enableSearch)): ?>
<?= $this->element('search', ['vv_search_fields' => $indexColumns]); ?>
<?php endif; // $enableSearch ?>

<!-- Index table -->
<div class="table-container">
<table id="<?= $vv_tablename . '-table'; ?>">
<tr>
@@ -316,7 +326,11 @@ function _column_key($modelsName, $c, $tz=null) {
?>
</td>
</tr>
<?php $recordsExist = true; ?>
<?php endforeach; // $$vv_tablename ?>
<?php if(!$recordsExist): ?>
<tr><td colspan="<?= count($indexColumns); ?>"><?= __('match.in.records.none') ?></td></tr>
<?php endif; ?>
</table>
</div>

129 changes: 129 additions & 0 deletions app/webroot/css/co-base.css
@@ -507,6 +507,135 @@ body.logged-in #top-menu {
clear: both;
margin-top: 1em;
}
/* TOP SEARCH */
.top-search {
margin-top: 0.5em;
padding: 0;
}
.top-search legend .material-icons {
vertical-align: middle;
}
.top-search legend button.cm-toggle {
position: absolute;
right: 0.5em;
border: none;
background-color: transparent;
}
#content .top-search legend .material-icons {
font-size: 20px;
}
#content .top-search legend button.cm-toggle .material-icons {
font-size: 34px;
line-height: 17px;
}
.top-search fieldset {
clear: both;
position: relative;
padding: 0.5em 0.5em 0;
margin: 0;
background-color: #f5f5f5;
}
.top-search.open fieldset {
padding-bottom: 0.5em;
}
.top-search legend {
width: 100%;
background-color: #f5f5f5;
margin: 0 -0.5em;
padding: 0.5em 0.5em 0;
line-height: 1.8em;
cursor: pointer;
font-size: 1em;
box-sizing: content-box;
}
#top-search-fields {
display: none;
padding: 0.25em 0.5em;
}
#top-search-submit {
float: right;
width: 200px;
margin-bottom: 0.75em;
}
.top-search input[type=text],
.top-search select,
.side-search input[type=text],
.side-search select {
width: 100%;
box-sizing: border-box;
margin: 0 0 0.5em 0;
height: 28px;
padding: 2px 4px;
border: 1px solid #ddd;
background-color: #fff;
}
.top-search label {
margin-bottom: 0;
}
::-webkit-input-placeholder,
::-moz-placeholder,
:-ms-input-placeholder,
:-moz-placeholder {
opacity: 0.2;
}
.top-search input[type=text]:focus,
.side-search input[type=text]:focus {
background-color: #ffd;
}
.top-search .submit-button,
.top-search .clear-button,
.side-search .submit-button,
.side-search .clear-button {
float: right;
font-size: 0.9em;
width: 80px;
line-height: 28px;
height: 28px;
margin: 1em 0.5em;
padding: 0;
}
.top-search .top-search-checkboxes {
margin-top: 0.5em;
}
.top-search .filter-clear-all-button {
font-size: 0.9em;
width: auto;
height: auto;
line-height: unset;
margin: 0;
padding: 0 1em;
}
.top-search .filter-clear-all-button:hover {
background-color: unset;
}
.top-search.top-search-hide-fields label,
.side-search label {
display: none;
}
#top-search-active-filters {
margin-left: 1em;
padding: 0.5em 0;
}
.top-search-active-filter {
margin: 0 0.25em 0 0;
white-space: nowrap;
/*color: #b00;*/
font-style: italic;
}
.top-search-active-filter-title::after {
content: ": ";
}
.top-search-active-filter-title.no-value::after {
content: none;
}
.top-search-active-filters-remove button {
margin-left: 2em;
font-size: 0.9em;
background-color: #f5f5f5;
}
.top-search-active-filters-remove button:hover {
background-color: #eee;
}
/* RECONCILE TABLE */
#reconcile-table {
width: auto;
11 changes: 11 additions & 0 deletions app/webroot/css/co-responsive.css
@@ -191,6 +191,17 @@
.call-to-action {
margin-bottom: 0;
}
/* TOP SEARCH */
#top-search-fields-subgroups {
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 1em;
padding: 0 0.5em;
}
#top-search-submit.tss-rebalance {
margin-top: -3.5em;
position: relative;
}
/* MATCHGRID MANAGEMENT */
#matchgrid-config-menu {
display: grid;
10 changes: 10 additions & 0 deletions app/webroot/js/comanage.js
@@ -226,3 +226,13 @@ function limitPage(pageLimit,recordCount,currentPage) {
window.location = currentUrl;
}

// Clear the top search form for index views
// formObj - form object (DOM form obj, required)
function clearTopSearch(formObj) {
for (var i=0; i<formObj.elements.length; i++) {
if(formObj.elements[i].type != 'hidden') {
formObj.elements[i].disabled = true;
}
}
formObj.submit();
}

0 comments on commit b95c469

Please sign in to comment.