Skip to content

Matchgrid Records: implement filtering/search on index view (CO-1871) #12

wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Matchgrid Records: implement filtering/search on index view (CO-1871)
arlen committed Sep 23, 2021
commit b95c469079b7a4edd0e434c7b843fef8bbba897c
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>