import rt 3.6.4
[freeside.git] / rt / html / Search / Build.html
1 %# BEGIN BPS TAGGED BLOCK {{{
2 %# 
3 %# COPYRIGHT:
4 %#  
5 %# This software is Copyright (c) 1996-2007 Best Practical Solutions, LLC 
6 %#                                          <jesse@bestpractical.com>
7 %# 
8 %# (Except where explicitly superseded by other copyright notices)
9 %# 
10 %# 
11 %# LICENSE:
12 %# 
13 %# This work is made available to you under the terms of Version 2 of
14 %# the GNU General Public License. A copy of that license should have
15 %# been provided with this software, but in any event can be snarfed
16 %# from www.gnu.org.
17 %# 
18 %# This work is distributed in the hope that it will be useful, but
19 %# WITHOUT ANY WARRANTY; without even the implied warranty of
20 %# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 %# General Public License for more details.
22 %# 
23 %# You should have received a copy of the GNU General Public License
24 %# along with this program; if not, write to the Free Software
25 %# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 %# 02110-1301 or visit their web page on the internet at
27 %# http://www.gnu.org/copyleft/gpl.html.
28 %# 
29 %# 
30 %# CONTRIBUTION SUBMISSION POLICY:
31 %# 
32 %# (The following paragraph is not intended to limit the rights granted
33 %# to you to modify and distribute this software under the terms of
34 %# the GNU General Public License and is only of importance to you if
35 %# you choose to contribute your changes and enhancements to the
36 %# community by submitting them to Best Practical Solutions, LLC.)
37 %# 
38 %# By intentionally submitting any modifications, corrections or
39 %# derivatives to this work, or any other work intended for use with
40 %# Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 %# you are the copyright holder for those contributions and you grant
42 %# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 %# royalty-free, perpetual, license to use, copy, create derivative
44 %# works based on those contributions, and sublicense and distribute
45 %# those contributions and any derivatives thereof.
46 %# 
47 %# END BPS TAGGED BLOCK }}}
48 %#
49 %# Data flow here:
50 %#   The page receives a Query from the previous page, and maybe arguments
51 %#   corresponding to actions.  (If it doesn't get a Query argument, it pulls
52 %#   one out of the session hash.  Also, it could be getting just a raw query from
53 %#   Build/Edit.html (Advanced).)
54 %#
55 %#   After doing some stuff with default arguments and saved searches, the ParseQuery
56 %#   function (which is similar to, but not the same as, _parser in RT/Tickets_Overlay_SQL)
57 %#   converts the Query into a RT::Interface::Web::QueryBuilder::Tree.  This mason file
58 %#   then adds stuff to or modifies the tree based on the actions that had been requested
59 %#   by clicking buttons.  It then calls GetQueryAndOptionList on the tree to generate
60 %#   the SQL query (which is saved as a hidden input) and the option list for the Clauses
61 %#   box in the top right corner.
62 %#
63 %#   Worthwhile refactoring: the tree manipulation code for the actions could use some cleaning
64 %#   up.  The node-adding code is different in the "add" actions from in ParseQuery, which leads
65 %#   to things like ParseQuery correctly not quoting numbers in numerical fields, while the "add"
66 %#   action does quote it (this breaks SQLite).
67 %#
68 <& /Elements/Header, Title => $title &>
69 <& /Ticket/Elements/Tabs, 
70     current_tab => "Search/Build.html".$QueryString, 
71     Title => $title,
72     Format => $Format,
73     Query => $Query,
74     Order => $Order,
75     OrderBy => $OrderBy,
76     Rows => $RowsPerPage
77 &>
78
79 <form method="post" action="Build.html" name="BuildQuery">
80 <input type="hidden" class="hidden" name="SearchId" value="<%$SearchId%>" />
81 <input type="hidden" class="hidden" name="Query" value="<%$Query%>" />
82 <input type="hidden" class="hidden" name="Format" value="<%$Format%>" />
83 <table width="100%" border="0" cellpadding="5">
84 <tr valign="top">
85 <td class="boxcontainer" rowspan="2" width="65%">
86 <& Elements/PickCriteria, query => $Query, cfqueues => $queues &>
87 <& /Elements/Submit, Caption => loc('Add these terms to your search'), Label => loc('Add'), Name => 'AddClause'&>
88 </td>
89
90 <td>
91 <& Elements/EditQuery,
92     %ARGS,
93     actions => \@actions,
94     optionlist => $optionlist,
95     Description => $Description &>
96 <& /Elements/Submit, Label => loc('Add and Search'), Name => 'DoSearch'&>
97 </td>
98 </tr>
99
100 <tr valign="top">
101 <td>
102 <& Elements/EditSearches, CurrentSearch => $search_hash, Dirty => $dirty, SearchId => $SearchId &>
103 </td>
104 </tr>
105
106 <tr>
107 <td colspan="2" class="boxcontainer">
108
109 <& Elements/DisplayOptions, %ARGS, Format=> $Format,
110 AvailableColumns => $AvailableColumns,  CurrentFormat => $CurrentFormat, RowsPerPage => $RowsPerPage, OrderBy => $OrderBy, Order => $Order &>
111 <& /Elements/Submit, Label => loc('Add and Search'), Name => 'DoSearch'&>
112 </td>
113 </tr>
114 </table>
115 </form>
116
117 <%INIT>
118 use RT::Interface::Web::QueryBuilder;
119 use RT::Interface::Web::QueryBuilder::Tree;
120
121 my $search_hash = {};
122 my $search;
123 my $title = loc("Query Builder");
124
125 # {{{ Clear out unwanted data
126 if ( $NewQuery or $ARGS{'Delete'} ) {
127
128     # Wipe all data-carrying variables clear if we want a new
129     # search, or we're deleting an old one..
130     $Query       = '';
131     $Format      = '';
132     $Description = '';
133     $SearchId    = '';
134     $Order       = '';
135     $OrderBy     = '';
136     $RowsPerPage = undef;
137
138     # ($search hasn't been set yet; no need to clear)
139
140     # ..then wipe the session out..
141     undef $session{'CurrentSearchHash'};
142
143     # ..and the search results.
144     $session{'tickets'}->CleanSlate() if defined $session{'tickets'};
145 }
146
147 # }}}
148
149 if (ref $OrderBy eq "ARRAY") {
150     $OrderBy = join("|", @$OrderBy);
151 }
152 if (ref $Order eq "ARRAY") {
153     $Order = join("|", @$Order);
154 }
155
156 # {{{ Attempt to load what we can from the session, set defaults
157
158 # We don't read or write to the session again until the end
159 $search_hash = $session{'CurrentSearchHash'};
160
161 # Read from user preferences
162 my $prefs = $session{'CurrentUser'}->UserObj->Preferences("SearchDisplay") || {};
163
164 # These variables are what define a search_hash; this is also
165 # where we give sane defaults.
166 $Query       ||= $search_hash->{'Query'};
167 $Format      ||= $search_hash->{'Format'} || $prefs->{'Format'};
168 $Description ||= $search_hash->{'Description'};
169 $SearchId    ||= $search_hash->{'SearchId'} || 'new';
170 $Order       ||= $search_hash->{'Order'} || $prefs->{'Order'} || 'ASC';
171 $OrderBy     ||= $search_hash->{'OrderBy'} || $prefs->{'OrderBy'} || 'id';
172
173 unless ( defined $RowsPerPage ) {
174     if ( defined $search_hash->{'RowsPerPage'} ) {
175         $RowsPerPage = $search_hash->{'RowsPerPage'};
176     }
177     elsif ( defined $prefs->{'RowsPerPage'} ) {
178         $RowsPerPage = $prefs->{'RowsPerPage'};
179     }
180     else {
181         $RowsPerPage = 50;
182     }
183 }
184
185   $search ||= $search_hash->{'Object'};
186
187 # }}}
188
189 my @actions = ();
190
191 # Clean unwanted junk from the format
192 $Format = $m->comp( '/Elements/ScrubHTML', Content => $Format ) if ($Format);
193
194 # {{{ If we're asked to delete the current search, make it go away and reset the search parameters
195 if ( $ARGS{'Delete'} ) {
196
197     # We set $SearchId to 'new' above already, so peek into the %ARGS
198     my ($container_object, $search_id) = _parse_saved_search ($ARGS{'SearchId'});
199     if ($container_object && $container_object->id) {
200         # We have the object the entry is an attribute on; delete the
201         # entry..
202         $container_object->Attributes->DeleteEntry(
203             Name => 'SavedSearch',
204             id   => $search_id
205             );
206     }
207 }
208
209 # }}}
210
211 # {{{ If the user wants to copy a search, uncouple from the one that this was based on, but don't erase the $Query or $Format
212 if ( $ARGS{'CopySearch'} ) {
213     $SearchId    = 'new';
214     $search      = undef;
215     $Description = loc( "[_1] copy", $Description );
216 }
217
218 # }}}
219
220 # {{{ if we're asked to revert the current search, we just want to load it
221 if ( $ARGS{'Revert'} ) {
222     $ARGS{'LoadSavedSearch'} = $SearchId;
223 }
224
225 # }}}
226
227 # {{{ if we're asked to load a search, load it.
228
229 if ( my ($container_object, $search_id ) = _parse_saved_search ($ARGS{'LoadSavedSearch'})) {
230     $search = $container_object->Attributes->WithId($search_id);
231
232     # We have a $search and now; import the others
233     $SearchId    = $ARGS{'LoadSavedSearch'};
234     $Description = $search->Description;
235     $Format      = $search->SubValue('Format');
236     $Query       = $search->SubValue('Query');
237     $Order       = $search->SubValue('Order');
238     $OrderBy     = $search->SubValue('OrderBy');
239     $RowsPerPage = $search->SubValue('RowsPerPage');
240 }
241
242 # }}}
243
244 # {{{ if we're asked to save the current search, save it
245 if ( $ARGS{'Save'} ) {
246     if ( $search && $search->id ) {
247         # permission check
248         if ($search->Object->isa('RT::System')) {
249             unless ($session{'CurrentUser'}->HasRight( Object=> $RT::System, Right => 'SuperUser')) {
250                 Abort("No permission to save system-wide searches");
251             }
252         }
253
254         # This search is based on a previously loaded search -- so
255         # just update the current search object with new values
256         $search->SetSubValues(
257             Format      => $Format,
258             Query       => $Query,
259             Order       => $Order,
260             OrderBy     => $OrderBy,
261             RowsPerPage => $RowsPerPage,
262         );
263         $search->SetDescription($Description);
264
265     }
266     elsif ( $SearchId eq 'new' ) {
267         my $saved_search = RT::SavedSearch->new( $session{'CurrentUser'} );
268         my ( $ok, $search_msg ) = $saved_search->Save(
269             Privacy      => $ARGS{'Owner'},
270             Name         => $Description,
271             SearchParams => {
272                 Format      => $Format,
273                 Query       => $Query,
274                 Order       => $Order,
275                 OrderBy     => $OrderBy,
276                 RowsPerPage => $RowsPerPage } );
277
278         if ($ok) {
279             $search = $session{'CurrentUser'}->UserObj->Attributes->WithId($saved_search->Id);
280             # Build new SearchId
281             $SearchId =
282                     ref( $session{'CurrentUser'}->UserObj ) . '-'
283                         . $session{'CurrentUser'}->UserObj->Id
284                         . '-SavedSearch-'
285                         . $search->Id;
286         }
287         else {
288             push @actions, [ loc("Can't find a saved search to work with").': '.loc($search_msg), 0 ];
289         }
290     }
291     else {
292         push @actions, [ loc("Can't save this search"), 0 ];
293     }
294
295 }
296
297 # }}}
298
299
300 # {{{ Parse the query
301 use Regexp::Common qw /delimited/;
302
303 # States
304 use constant VALUE   => 1;
305 use constant AGGREG  => 2;
306 use constant OP      => 4;
307 use constant PAREN   => 8;
308 use constant KEYWORD => 16;
309
310 my $_match = sub {
311
312     # Case insensitive equality
313     my ( $y, $x ) = @_;
314     return 1 if $x =~ /^$y$/i;
315
316     #  return 1 if ((lc $x) eq (lc $y)); # Why isnt this equiv?
317     return 0;
318 };
319
320 my $ParseQuery = sub {
321     my $string  = shift;
322     my $tree    = shift;
323     my $actions = shift;
324     my $want    = KEYWORD | PAREN;
325     my $last    = undef;
326
327     my $depth = 1;
328
329     # make a tree root
330     $$tree = RT::Interface::Web::QueryBuilder::Tree->new;
331     my $root       = RT::Interface::Web::QueryBuilder::Tree->new( 'AND', $$tree );
332     my $parentnode = $root;
333
334     # on new searches, we're passed undef but still need to construct the
335     # RT::Interface::Web::QueryBuilder::Tree.  Quiet warning
336     return unless defined $string;
337
338     # get the FIELDS from Tickets_Overlay
339     my $tickets = new RT::Tickets( $session{'CurrentUser'} );
340     my %FIELDS  = %{ $tickets->FIELDS };
341
342     # Lower Case version of FIELDS, for case insensitivity
343     my %lcfields = map { ( lc($_) => $_ ) } ( keys %FIELDS );
344
345     my @tokens     = qw[VALUE AGGREG OP PAREN KEYWORD];
346     my $re_aggreg  = qr[(?i:AND|OR)];
347     my $re_value   = qr[$RE{delimited}{-delim=>qq{\'\"}}|\d+];
348     my $re_keyword = qr[$RE{delimited}{-delim=>qq{\'\"}}|(?:\{|\}|\w|\.)+];
349     my $re_op      =
350       qr[=|!=|>=|<=|>|<|(?i:IS NOT)|(?i:IS)|(?i:NOT LIKE)|(?i:LIKE)]
351       ;    # long to short
352     my $re_paren = qr'\(|\)';
353
354     # assume that $ea is AND if it is not set
355     my ( $ea, $key, $op, $value ) = ( "AND", "", "", "" );
356
357     # order of matches in the RE is important.. op should come early,
358     # because it has spaces in it.  otherwise "NOT LIKE" might be parsed
359     # as a keyword or value.
360
361     while (
362         $string =~ /(
363                       $re_aggreg
364                       |$re_op
365                       |$re_keyword
366                       |$re_value
367                       |$re_paren
368                      )/igx
369       )
370     {
371         my $val     = $1;
372         my $current = 0;
373
374         # Highest priority is last
375         $current = OP    if $_match->( $re_op,    $val );
376         $current = VALUE if $_match->( $re_value, $val );
377         $current = KEYWORD
378           if $_match->( $re_keyword, $val ) && ( $want & KEYWORD );
379         $current = AGGREG if $_match->( $re_aggreg, $val );
380         $current = PAREN  if $_match->( $re_paren,  $val );
381
382         unless ( $current && $want & $current ) {
383
384             # Error
385             # FIXME: I will only print out the highest $want value
386             my $token = $tokens[ ( ( log $want ) / ( log 2 ) ) ];
387             push @$actions,
388               [
389                 loc("Error near ->[_1]<- expecting a [_2] in '[_3]'",
390                                   $val,              $token, $string ),
391                 -1
392               ];
393         }
394
395         # State Machine:
396         my $parentdepth = $depth;
397
398         # Parens are highest priority
399         if ( $current & PAREN ) {
400             if ( $val eq "(" ) {
401                 $depth++;
402
403                 # make a new node that the clauses can be children of
404                 $parentnode = RT::Interface::Web::QueryBuilder::Tree->new( $ea, $parentnode );
405             }
406             else {
407                 $depth--;
408                 $parentnode = $parentnode->getParent();
409             }
410
411             $want = KEYWORD | PAREN | AGGREG;
412         }
413         elsif ( $current & AGGREG ) {
414             $ea   = $val;
415             $parentnode->setNodeValue($ea);
416             $want = KEYWORD | PAREN;
417         }
418         elsif ( $current & KEYWORD ) {
419             $key  = $val;
420             $want = OP;
421         }
422         elsif ( $current & OP ) {
423             $op   = $val;
424             $want = VALUE;
425         }
426         elsif ( $current & VALUE ) {
427             $value = $val;
428
429             # Remove surrounding quotes from $key, $val
430             # (in future, simplify as for($key,$val) { action on $_ })
431             if ( $key =~ /$RE{delimited}{-delim=>qq{\'\"}}/ ) {
432                 substr( $key, 0,  1 ) = "";
433                 substr( $key, -1, 1 ) = "";
434             }
435             if ( $val =~ /$RE{delimited}{-delim=>qq{\'\"}}/ ) {
436                 substr( $val, 0,  1 ) = "";
437                 substr( $val, -1, 1 ) = "";
438             }
439
440             # Unescape escaped characters
441             $key =~ s!\\(.)!$1!g;
442             $val =~ s!\\(.)!$1!g;
443
444             my $class;
445
446             my ($key_base, $subkey)  = split(/\./,$key,2);
447             $key_base =~ s/\..*$//; # Strip off .EmailAddress, for example
448
449             if ( exists $lcfields{lc $key_base } ) {
450                 $key   = $lcfields{lc $key_base } . (defined $subkey ? '.'.$subkey : '');
451                 $class = $FIELDS{$key_base}->[0];
452             }
453             elsif ( $key =~ /^C(?:ustom)?F(?:ield)?\.{(.*)}$/i ) {
454                 $class = $FIELDS{'CF'}->[0];
455             }
456
457             if ( $class ne 'INT' ) {
458                 $val = "'$val'";
459             }
460
461             push @$actions, [ loc("Unknown field: $key"), -1 ] unless $class;
462
463             $want = PAREN | AGGREG;
464         }
465         else {
466             push @$actions, [ loc("I'm lost"), -1 ];
467         }
468
469         if ( $current & VALUE ) {
470             if ( $key =~ /^CF./ ) {
471                 $key = "'" . $key . "'";
472             }
473             my $clause = {
474                 Key   => $key,
475                 Op    => $op,
476                 Value => $val
477             };
478
479             # explicity add a child to it
480             RT::Interface::Web::QueryBuilder::Tree->new( $clause, $parentnode );
481
482             ( $ea, $key, $op, $value ) = ( "", "", "", "" );
483
484         }
485
486         $last = $current;
487     }    # while
488
489     push @$actions, [ loc("Incomplete query"), -1 ]
490       unless ( ( $want | PAREN ) || ( $want | KEYWORD ) );
491
492     push @$actions, [ loc("Incomplete Query"), -1 ]
493       unless ( $last && ( $last | PAREN ) || ( $last || VALUE ) );
494
495     # This will never happen, because the parser will complain
496     push @$actions, [ loc("Mismatched parentheses"), -1 ]
497       unless $depth == 1;
498 };
499
500 my $tree;
501 {
502     my @parsing_errors;
503     $ParseQuery->( $Query, \$tree, \@parsing_errors );
504
505     # if parsing went poorly, send them to the edit page
506     # to fix it
507     if ( @parsing_errors ) {
508         return $m->comp(
509             "Edit.html",
510             Query   => $Query,
511             actions => \@parsing_errors
512         );
513     }
514 }
515
516 $Query  = "";
517
518 my @options = $tree->GetDisplayedNodes;
519
520 my @current_values = grep { defined } @options[@clauses];
521
522 # {{{ Move things around
523 if ( $ARGS{"Up"} ) {
524     if (@current_values) {
525         foreach my $value (@current_values) {
526             my $index = $value->getIndex();
527             if ( $value->getIndex() > 0 ) {
528                 my $parent = $value->getParent();
529                 $parent->removeChild($index);
530                 $parent->insertChild( $index - 1, $value );
531                 $value = $parent->getChild( $index - 1 );
532             }
533             else {
534                 push( @actions, [ loc("error: can't move up"), -1 ] );
535             }
536         }
537     }
538     else {
539         push( @actions, [ loc("error: nothing to move"), -1 ] );
540     }
541 }
542 elsif ( $ARGS{"Down"} ) {
543     if (@current_values) {
544         foreach my $value (@current_values) {
545             my $index  = $value->getIndex();
546             my $parent = $value->getParent();
547             if ( $value->getIndex() < ( $parent->getChildCount - 1 ) ) {
548                 $parent->removeChild($index);
549                 $parent->insertChild( $index + 1, $value );
550                 $value = $parent->getChild( $index + 1 );
551             }
552             else {
553                 push( @actions, [ loc("error: can't move down"), -1 ] );
554             }
555         }
556     }
557     else {
558         push( @actions, [ loc("error: nothing to move"), -1 ] );
559     }
560 }
561 elsif ( $ARGS{"Left"} ) {
562     if (@current_values) {
563         foreach my $value (@current_values) {
564             my $parent      = $value->getParent();
565             my $grandparent = $parent->getParent();
566             if ( !$grandparent->isRoot ) {
567                 my $index = $parent->getIndex();
568                 $parent->removeChild($value);
569                 $grandparent->insertChild( $index, $value );
570                 if ( $parent->isLeaf() ) {
571                     $grandparent->removeChild($parent);
572                 }
573             }
574             else {
575                 push( @actions, [ loc("error: can't move left"), -1 ] );
576             }
577         }
578     }
579     else {
580         push( @actions, [ loc("error: nothing to move"), -1 ] );
581     }
582 }
583 elsif ( $ARGS{"Right"} ) {
584     if (@current_values) {
585         foreach my $value (@current_values) {
586             my $parent = $value->getParent();
587             my $index  = $value->getIndex();
588             my $newparent;
589             if ( $index > 0 ) {
590                 my $sibling = $parent->getChild( $index - 1 );
591                 if ( ref( $sibling->getNodeValue ) ) {
592                     $parent->removeChild($value);
593                     my $newtree = RT::Interface::Web::QueryBuilder::Tree->new( 'AND', $parent );
594                     $newtree->addChild($value);
595                 }
596                 else {
597                     $parent->removeChild($index);
598                     $sibling->addChild($value);
599                 }
600             }
601             else {
602                 $parent->removeChild($value);
603                 $newparent = RT::Interface::Web::QueryBuilder::Tree->new( 'AND', $parent );
604                 $newparent->addChild($value);
605             }
606         }
607     }
608     else {
609         push( @actions, [ loc("error: nothing to move"), -1 ] );
610     }
611 }
612 elsif ( $ARGS{"DeleteClause"} ) {
613     if (@current_values) {
614         $_->getParent()->removeChild($_) for @current_values;
615                 @current_values = ();
616     }
617     else {
618         push( @actions, [ loc("error: nothing to delete"), -1 ] );
619     }
620 }
621 elsif ( $ARGS{"Toggle"} ) {
622     my $ea;
623     if (@current_values) {
624         foreach my $value (@current_values) {
625             my $parent = $value->getParent();
626
627             if ( $parent->getNodeValue eq 'AND' ) {
628                 $parent->setNodeValue('OR');
629             }
630             else {
631                 $parent->setNodeValue('AND');
632             }
633         }
634     }
635     else {
636         push( @actions, [ loc("error: nothing to toggle"), -1 ] );
637     }
638 }
639
640 # {{{ Try to find if we're adding a clause
641 foreach my $arg ( keys %ARGS ) {
642     if (
643             $arg =~ m/^ValueOf(\w+|'CF.{.*?}')$/
644         && ( ref $ARGS{$arg} eq "ARRAY"
645             ? grep { $_ ne "" } @{ $ARGS{$arg} }
646             : $ARGS{$arg} ne "" )
647       )
648     {
649
650         # We're adding a $1 clause
651         my $field = $1;
652         my ( $keyword, $op, $value );
653
654         #figure out if it's a grouping
655         if ( $ARGS{ $field . "Field" } ) {
656             $keyword = $ARGS{ $field . "Field" };
657         }
658         else {
659             $keyword = $field;
660         }
661
662         my ( @ops, @values );
663         if ( ref $ARGS{ 'ValueOf' . $field } eq "ARRAY" ) {
664
665             # we have many keys/values to iterate over, because there is
666             # more than one CF with the same name.
667             @ops    = @{ $ARGS{ $field . 'Op' } };
668             @values = @{ $ARGS{ 'ValueOf' . $field } };
669         }
670         else {
671             @ops    = ( $ARGS{ $field . 'Op' } );
672             @values = ( $ARGS{ 'ValueOf' . $field } );
673         }
674         $RT::Logger->error("Bad Parameters passed into Query Builder")
675           unless @ops == @values;
676
677         for my $i ( 0 .. @ops - 1 ) {
678             my ( $op, $value ) = ( $ops[$i], $values[$i] );
679             next if $value eq "";
680
681             if ( $value eq 'NULL' && $op =~ /=/ ) {
682                 if ( $op eq '=' ) {
683                     $op = "IS";
684                 }
685                 elsif ( $op eq '!=' ) {
686                     $op = "IS NOT";
687                 }
688
689                 # This isn't "right", but...
690                 # It has to be this way until #5182 is fixed
691                 $value = "'NULL'";
692             }
693             else {
694                 $value = "'$value'";
695             }
696
697             my $clause = {
698                 Key   => $keyword,
699                 Op    => $op,
700                 Value => $value
701             };
702
703             my $newnode = RT::Interface::Web::QueryBuilder::Tree->new($clause);
704             if (@current_values) {
705                 foreach my $value (@current_values) {
706                     my $newindex = $value->getIndex() + 1;
707                     $value->insertSibling( $newindex, $newnode );
708                     $value = $newnode;
709                 }
710             }
711             else {
712                 $tree->getChild(0)->addChild($newnode);
713                 @current_values = $newnode;
714             }
715             $newnode->getParent()->setNodeValue( $ARGS{'AndOr'} );
716         }
717     }
718 }
719
720 # }}}
721
722 $tree->PruneChildlessAggregators;
723
724 # }}}
725
726 # {{{ Rebuild $Query based on the additions / movements
727 $Query      = "";
728 my $optionlist_arrayref;
729
730 ($Query, $optionlist_arrayref) = $tree->GetQueryAndOptionList(\@current_values);
731   
732 my $optionlist = join "\n", map { qq(<option value="$_->{INDEX}" $_->{SELECTED}>) 
733                                   . ("&nbsp;" x (5 * $_->{DEPTH}))
734                                   . $m->interp->apply_escapes($_->{TEXT}, 'h') . qq(</option>) } @$optionlist_arrayref;
735
736
737
738
739 # }}}
740
741 # }}}
742
743 my $queues = $tree->GetReferencedQueues;
744
745 # {{{ Deal with format changes
746 my ( $AvailableColumns, $CurrentFormat );
747 ( $Format, $AvailableColumns, $CurrentFormat ) = $m->comp(
748     'Elements/BuildFormatString',
749     cfqueues => $queues,
750     %ARGS, Format => $Format
751 );
752
753 # }}}
754
755 # {{{ If we're modifying an old query, check if it has changed
756 my $dirty = 0;
757 $dirty = 1
758   if defined $search
759   and ($search->SubValue('Format') ne $Format
760     or $search->SubValue('Query')       ne $Query
761     or $search->SubValue('Order')       ne $Order
762     or $search->SubValue('OrderBy')     ne $OrderBy
763     or $search->SubValue('RowsPerPage') ne $RowsPerPage );
764
765 # }}}
766
767 # {{{ Push the updates into the session so we don't loose 'em
768 $search_hash->{'SearchId'}    = $SearchId;
769 $search_hash->{'Format'}      = $Format;
770 $search_hash->{'Query'}       = $Query;
771 $search_hash->{'Description'} = $Description;
772 $search_hash->{'Object'}      = $search;
773 $search_hash->{'Order'}       = $Order;
774 $search_hash->{'OrderBy'}     = $OrderBy;
775 $search_hash->{'RowsPerPage'} = $RowsPerPage;
776
777 $session{'CurrentSearchHash'} = $search_hash;
778
779 # }}}
780
781 # {{{ Show the results, if we were asked.
782 if ( $ARGS{"DoSearch"}) {
783     $m->comp(
784         "Results.html",
785         Query   => $Query,
786         Format  => $Format,
787         Order   => $Order,
788         OrderBy => $OrderBy,
789         Rows    => $RowsPerPage
790     );
791     $m->comp('/Elements/Footer');
792     $m->abort();
793 }
794
795 # }}}
796
797 # {{{ Build a querystring for the tabs
798
799 my $QueryString;
800 if ($NewQuery) {
801     $QueryString = '?NewQuery=1';
802 }
803 else {
804     $QueryString = '?'
805       . $m->comp(
806         '/Elements/QueryString',
807         Query   => $Query,
808         Format  => $Format,
809         Order   => $Order,
810         OrderBy => $OrderBy,
811         Rows    => $RowsPerPage
812       )
813       if ($Query);
814 }
815
816 # }}}
817
818 </%INIT>
819
820 <%ARGS>
821 $NewQuery => 0
822 $SearchId => undef
823 $Query => undef
824 $Format => undef 
825 $Description => undef
826 $Order => undef
827 $OrderBy => undef
828 $RowsPerPage => undef
829 $HideResults => 0
830 @clauses => ()
831 </%ARGS>
832