[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