[Rt-commit] rt 04/04: Initial ticket search filter support
sunnavy
sunnavy at bestpractical.com
Fri Jul 9 16:32:51 EDT 2021
This is an automated email from the git hooks/post-receive script.
sunnavy pushed a commit to branch 5.0/search-filter
in repository rt.
commit e9b5785e133289bc66227a16edef7814e678fa02
Author: sunnavy <sunnavy at bestpractical.com>
AuthorDate: Fri Jun 25 06:59:08 2021 +0800
Initial ticket search filter support
Currently the following fields are supported: Subject, Status, Queue,
Owner, Creator and LastUpdatedBy.
---
lib/RT/Interface/Web/MenuBuilder.pm | 2 +-
share/html/Elements/CollectionAsTable/Header | 28 +++-
share/html/Helpers/SearchFilter | 224 +++++++++++++++++++++++++++
share/html/Search/Results.html | 6 +-
share/static/js/util.js | 68 ++++++++
5 files changed, 319 insertions(+), 9 deletions(-)
diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm
index 3b3b67c554..2adcd08353 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -617,7 +617,7 @@ sub BuildMainNav {
map {
my $p = $_;
$p => $HTML::Mason::Commands::DECODED_ARGS->{$p} || $current_search->{$p}
- } qw(Query Format OrderBy Order Page Class ObjectType ResultPage ExtraQueryParams),
+ } qw(BaseQuery Query Format OrderBy Order Page Class ObjectType ResultPage ExtraQueryParams),
),
);
diff --git a/share/html/Elements/CollectionAsTable/Header b/share/html/Elements/CollectionAsTable/Header
index d9fc8555e0..fcbeda8d7d 100644
--- a/share/html/Elements/CollectionAsTable/Header
+++ b/share/html/Elements/CollectionAsTable/Header
@@ -135,16 +135,18 @@ foreach my $col ( @Format ) {
$loc_title = loc($m->comp('/Elements/ScrubHTML', Content => $title));
}
- if ( $AllowSorting and $col->{'attribute'}
- and my $attr = $m->comp(
+ my $attribute;
+ if ( $col->{'attribute'} ) {
+ $attribute = $m->comp(
"/Elements/ColumnMap",
Class => $column_map_class,
Name => $col->{'attribute'},
Attr => 'attribute'
- )
- )
- {
- $attr = ProcessColumnMapValue( $attr, Arguments => [ $col->{'attribute'} ], Escape => 0 );
+ );
+ }
+
+ if ( $AllowSorting and $attribute ) {
+ my $attr = ProcessColumnMapValue( $attribute, Arguments => [ $col->{'attribute'} ], Escape => 0 );
my $new_order = 'ASC';
$new_order = ($Order[0] // '') eq 'ASC'? 'DESC': 'ASC'
@@ -162,6 +164,20 @@ foreach my $col ( @Format ) {
else {
$m->out( $loc_title );
}
+
+ if ( $Class eq 'RT::Tickets' ) {
+ my $attr = ProcessColumnMapValue( $attribute, Arguments => [ $col->{'attribute'} ], Escape => 1 );
+ if ( ( $attr || '' ) =~ /^(?:Subject|Status|Queue|Owner|Creator|LastUpdatedBy)$/ ) {
+ my $base_query = $ARGS{BaseQuery} || $ARGS{Query};
+ my $query = $ARGS{Query};
+ RT::Interface::Web::EscapeHTML( \$base_query );
+ RT::Interface::Web::EscapeHTML( \$query );
+
+ $m->out(
+ qq{ <a href="javascript:void(0)" class="search-filter" data-attribute="$attr" data-query="$query" data-base-query="$base_query" class="btn btn-primary button"><span class="fas fa-filter"></span></a>}
+ );
+ }
+ }
$m->out('</th>');
}
</%PERL>
diff --git a/share/html/Helpers/SearchFilter b/share/html/Helpers/SearchFilter
new file mode 100644
index 0000000000..1c0d7e8fd2
--- /dev/null
+++ b/share/html/Helpers/SearchFilter
@@ -0,0 +1,224 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2021 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<div class="modal-dialog" role="document">
+ <form name="search-results-filter" id="search-results-filter">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h5 class="modal-title"><&|/l&>Filter Results</&></h5>
+ <a href="javascript:void(0)" class="close" data-dismiss="modal" aria-label="Close">
+ <span aria-hidden="true">×</span>
+ </a>
+ </div>
+ <div class="modal-body">
+ <div class="form-row <% $Attribute eq 'Subject' ? '' : 'hidden' %>">
+ <div class="label col-3">
+ <&|/l&>Subject</&>:
+ </div>
+ <div class="value col-9">
+ <input class="form-control" name="Subject" value="<% $filter{Subject} // '' %>" />
+ </div>
+ </div>
+ <div class="form-row <% $Attribute eq 'Status' ? '' : 'hidden' %>">
+ <div class="label col-3">
+ <&|/l&>Status</&>:
+ </div>
+ <div class="value col-9">
+ <ul class="list-group list-group-compact">
+% for my $status ( sort { lc $a cmp lc $b } keys %status ) {
+ <li class="list-group-item">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox" id="Status-<% $status %>" name="Status" class="custom-control-input" value="<% $status %>" <% $filter{Status}{$status} ? 'checked="checked"' : '' |n %> />
+ <label class="custom-control-label" for="Status-<% $status %>"><% $status %></label>
+ </div>
+ </li>
+% }
+ </ul>
+ </div>
+ </div>
+ <div class="form-row <% $Attribute eq 'Queue' ? '' : 'hidden' %>">
+ <div class="label col-3">
+ <&|/l&>Queue</&>:
+ </div>
+ <div class="value col-9">
+ <ul class="list-group list-group-compact">
+% for my $queue ( sort { lc $a->Name cmp lc $b->Name } @queues ) {
+ <li class="list-group-item">
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox" id="Queue-<% $queue->Id %>" name="Queue" class="custom-control-input" value="<% $queue->Id %>" <% $filter{Queue}{$queue->Id} ? 'checked="checked"' : '' |n %> />
+ <label class="custom-control-label" for="Queue-<% $queue->Id %>"><% $queue->Name %></label>
+ </div>
+ </li>
+% }
+ </ul>
+ </div>
+ </div>
+ <div class="form-row <% $Attribute eq 'Owner' ? '' : 'hidden' %>">
+ <div class="label col-3">
+ <&|/l&>Owner</&>:
+ </div>
+ <div class="value col-9">
+ <& /Elements/SelectOwner, Name => 'Owner', Default => $filter{Owner} &>
+ </div>
+ </div>
+
+% for my $field ( qw/Creator LastUpdatedBy/ ) {
+ <div class="form-row <% $Attribute eq $field ? '' : 'hidden' %>">
+ <div class="label col-3">
+ <% loc($field) %>:
+ </div>
+ <div class="value col-9">
+ <input class="form-control" data-autocomplete="Users" name="<% $field %>" value="<% $filter{$field} %>" data-autocomplete-return="Name" />
+ </div>
+ </div>
+% }
+
+ </div>
+ <div class="modal-footer">
+ <div class="form-row justify-content-end">
+ <div class="col-auto">
+ <input type="button" class="button btn btn-primary" data-dismiss="modal" name="Apply" value="<% loc('Cancel') %>" />
+ </div>
+ <div class="col-auto">
+ <input type="button" class="button btn btn-primary" onclick="filterSearchResults()" name="Apply" value="<% loc('Apply') %>" />
+ </div>
+ </div>
+ </div>
+ </div>
+ </form>
+</div>
+
+<%INIT>
+return unless $ARGS{Query};
+
+my $tickets = RT::Tickets->new($session{CurrentUser});
+my ( $ok ) = $tickets->FromSQL($ARGS{Query});
+return unless $ok && ( $ARGS{ BaseQuery } || $tickets->Count );
+
+my @queues;
+
+{
+ my $tree = RT::Interface::Web::QueryBuilder::Tree->new;
+ $tree->ParseSQL( Query => $ARGS{BaseQuery} || $ARGS{Query}, CurrentUser => $session{'CurrentUser'} );
+ my $referenced_queues = $tree->GetReferencedQueues;
+ for my $name_or_id ( keys %$referenced_queues ) {
+ my $queue = RT::Queue->new($session{CurrentUser});
+ $queue->Load($name_or_id);
+ if ( $queue->id ) {
+ push @queues, $queue;
+ }
+ }
+}
+
+my %status;
+my @lifecycles;
+
+if ( @queues ) {
+ my %lifecycle;
+ for my $queue ( @queues ) {
+ next if $lifecycle{$queue->Lifecycle}++;
+ push @lifecycles, $queue->LifecycleObj;
+ }
+}
+else {
+ @lifecycles = map { RT::Lifecycle->Load(Type => 'ticket', Name => $_) } RT::Lifecycle->List('ticket');
+}
+
+for my $lifecycle ( @lifecycles ) {
+ $status{$_} = 1 for $lifecycle->Valid;
+}
+delete $status{deleted};
+
+if ( !@queues ) {
+ my $queues = RT::Queues->new( $session{CurrentUser} );
+ $queues->UnLimit;
+
+ while ( my $queue = $queues->Next ) {
+ push @queues, $queue;
+ last if @queues == 10; # TODO make a config for it
+ }
+}
+
+my %filter;
+
+if ( $ARGS{BaseQuery} && $ARGS{BaseQuery} ne $ARGS{Query} ) {
+ my $query = $ARGS{Query};
+ $query =~ s!^\s*\(?\s*\Q$ARGS{BaseQuery}\E\s*\)? AND !!;
+ my $tree = RT::Interface::Web::QueryBuilder::Tree->new;
+ $tree->ParseSQL( Query => $query, CurrentUser => $session{'CurrentUser'} );
+ $tree->traverse(
+ sub {
+ my $node = shift;
+
+ return if $node->isRoot;
+ return unless $node->isLeaf;
+
+ my $clause = $node->getNodeValue();
+ if ( $clause->{Key} =~ /Queue/ ) {
+ my $queue = RT::Queue->new($session{CurrentUser});
+ $queue->Load($clause->{Value});
+ if ( $queue->id ) {
+ $filter{ $clause->{Key} }{ $queue->id } = 1;
+ }
+ }
+ elsif ( $clause->{Key} =~ /Status|SLA/ ) {
+ $filter{ $clause->{Key} }{ $clause->{Value} } = 1;
+ }
+ else {
+ my $key = $clause->{Key};
+ my $value = $clause->{Value};
+ $value =~ s!\\([\\"])!$1!g;
+ $filter{$key} = $value;
+ }
+ }
+ );
+}
+</%INIT>
+
+<%ARGS>
+$Attribute => ''
+</%ARGS>
diff --git a/share/html/Search/Results.html b/share/html/Search/Results.html
index 27737ec2b4..29830d73b7 100644
--- a/share/html/Search/Results.html
+++ b/share/html/Search/Results.html
@@ -75,13 +75,14 @@
SavedSearchId => $ARGS{'SavedSearchId'},
SavedChartSearchId => $ARGS{'SavedChartSearchId'},
ObjectType => $ObjectType,
+ BaseQuery => $BaseQuery || $Query,
@ExtraQueryParams ? ( map { $_ => $ARGS{$_} } grep { defined $ARGS{$_} } 'ExtraQueryParams', @ExtraQueryParams ) : (),
- PassArguments => [qw(Query Format Rows Page Order OrderBy SavedSearchId SavedChartSearchId Class ObjectType ExtraQueryParams), @ExtraQueryParams],
+ PassArguments => [qw(BaseQuery Query Format Rows Page Order OrderBy SavedSearchId SavedChartSearchId Class ObjectType ExtraQueryParams), @ExtraQueryParams],
&>
% }
% $m->callback( ARGSRef => \%ARGS, CallbackName => 'AfterResults' );
-% my %hiddens = (Query => $Query, Format => $Format, Rows => $Rows, OrderBy => $OrderBy, Order => $Order, HideResults => $HideResults, Page => $Page, SavedChartSearchId => $SavedChartSearchId );
+% my %hiddens = (BaseQuery => $BaseQuery || $Query, Query => $Query, Format => $Format, Rows => $Rows, OrderBy => $OrderBy, Order => $Order, HideResults => $HideResults, Page => $Page, SavedChartSearchId => $SavedChartSearchId );
<div align="right" class="refresh">
<form method="get" action="<%RT->Config->Get('WebPath')%>/Search/Results.html">
% foreach my $key (keys(%hiddens)) {
@@ -293,6 +294,7 @@ $session{$session_name}->PrepForSerialization();
</%CLEANUP>
<%ARGS>
$Query => undef
+$BaseQuery => undef
$Format => undef
$HideResults => 0
$Rows => undef
diff --git a/share/static/js/util.js b/share/static/js/util.js
index 38b9105c73..2d3bcabfbd 100644
--- a/share/static/js/util.js
+++ b/share/static/js/util.js
@@ -827,8 +827,76 @@ jQuery(function() {
});
});
}
+
+ jQuery(".search-filter").click(function(ev){
+ ev.preventDefault();
+ jQuery.get(
+ RT.Config.WebHomePath + "/Helpers/SearchFilter",
+ {
+ BaseQuery: jQuery(this).data('base-query'),
+ Query: jQuery(this).data('query'),
+ Attribute: jQuery(this).data('attribute')
+ },
+ showModal
+ );
+ });
});
+function filterSearchResults () {
+ var clauses = [];
+
+ var queue_clauses = [];
+ jQuery('#search-results-filter input[name=Queue]:checked').each( function() {
+ queue_clauses.push( 'Queue = ' + '"' + jQuery(this).val() + '"' );
+ });
+
+ if ( queue_clauses.length ) {
+ clauses.push( '( ' + queue_clauses.join( ' OR ' ) + ' )' );
+ }
+
+ var status_clauses = [];
+ jQuery('#search-results-filter input[name=Status]:checked').each( function() {
+ status_clauses.push('Status = ' + '"' + jQuery(this).val() + '"' );
+ });
+
+ if ( status_clauses.length ) {
+ clauses.push( '( ' + status_clauses.join( ' OR ' ) + ' )' );
+ }
+
+ var subject = jQuery('#search-results-filter input[name=Subject]').val();
+ if ( subject && subject.match(/\S/) ) {
+ clauses.push( '( Subject LIKE "' + subject.replace(/(["\\])/g, "\\$1") + '" )' );
+ }
+
+ [ 'Owner', 'Creator', 'LastUpdatedBy' ].forEach( function(role) {
+ var value = jQuery('#search-results-filter :input[name=' + role + ']').val();
+ if ( value && value.match(/\S/) ) {
+ var subs = [];
+ clauses.push( role + ' = "' + value + '"' );
+ }
+ });
+
+ var refresh_form = jQuery('div.refresh form');
+ var base_query = refresh_form.find('input[name=BaseQuery]').val();
+
+ var query;
+ if ( clauses.length ) {
+ if ( base_query.match(/^\s*\(.+\)\s*$/) ) {
+ query = base_query + " AND " + clauses.join( ' AND ' );
+ }
+ else {
+ query = '( ' + base_query + " ) AND " + clauses.join( ' AND ' );
+ }
+ }
+ else {
+ query = base_query;
+ }
+
+ refresh_form.find('input[name=Query]').val(query);
+ refresh_form.submit();
+ return false;
+};
+
/* inline edit */
jQuery(function () {
var inlineEditEnabled = true;
--
To stop receiving notification emails like this one, please contact
sysadmin at bestpractical.com.
More information about the rt-commit
mailing list