[Bps-public-commit] rt-extension-assetsql branch, master, updated. 83af1bf91c7d5ec2c4fc69ae8b782069d1c3a6aa

Shawn Moore shawn at bestpractical.com
Wed Jun 15 14:42:44 EDT 2016


The branch, master has been updated
       via  83af1bf91c7d5ec2c4fc69ae8b782069d1c3a6aa (commit)
       via  656860135daaac554c717cc6e029d878befdb103 (commit)
      from  0cca0ff23ef78b45d46bc0b92e050a8bb2ac19d5 (commit)

Summary of changes:
 META.yml                                           |    9 +-
 Makefile.PL                                        |    2 +
 README                                             |   54 +
 html/Asset/Elements/SelectAttachmentField          |    7 +
 html/Asset/Elements/SelectDateType                 |    7 +
 html/Asset/Search/Build.html                       |  287 ++++
 html/Asset/Search/Bulk.html                        |  152 ++
 html/Asset/Search/Edit.html                        |   40 +
 html/Asset/Search/Elements/BuildFormatString       |  163 +++
 html/Asset/Search/Elements/ConditionRow            |   52 +
 html/Asset/Search/Elements/DisplayOptions          |    6 +
 html/Asset/Search/Elements/EditFormat              |   84 ++
 html/Asset/Search/Elements/EditQuery               |  196 +++
 html/Asset/Search/Elements/EditSearches            |  282 ++++
 html/Asset/Search/Elements/EditSort                |   93 ++
 html/Asset/Search/Elements/NewListActions          |   19 +
 html/Asset/Search/Elements/PickAssetCFs            |   16 +
 html/Asset/Search/Elements/PickBasics              |  121 ++
 html/Asset/Search/Elements/PickCriteria            |   26 +
 html/Asset/Search/Elements/SearchPrivacy           |   14 +
 html/Asset/Search/Elements/SelectAndOr             |    6 +
 html/Asset/Search/Elements/SelectLinks             |   20 +
 html/Asset/Search/Elements/SelectPersonType        |   27 +
 html/Asset/Search/Elements/SelectSearchObject      |   17 +
 .../Asset/Search/Elements/SelectSearchesForObjects |   21 +
 html/Asset/Search/Results.html                     |  165 +++
 html/Asset/Search/Results.tsv                      |   50 +
 html/Callbacks/AssetSQL/Elements/Tabs/Privileged   |  121 ++
 inc/Module/Install.pm                              |    2 +-
 inc/Module/Install/Base.pm                         |    2 +-
 inc/Module/Install/Can.pm                          |    2 +-
 inc/Module/Install/Fetch.pm                        |    2 +-
 inc/Module/Install/Include.pm                      |    2 +-
 inc/Module/Install/Makefile.pm                     |    2 +-
 inc/Module/Install/Metadata.pm                     |    2 +-
 inc/Module/Install/RTx.pm                          |    2 +-
 inc/Module/Install/ReadmeFromPod.pm                |   76 +-
 inc/Module/Install/Win32.pm                        |    2 +-
 inc/Module/Install/WriteAll.pm                     |    2 +-
 inc/YAML/Tiny.pm                                   |   29 +-
 inc/unicore/Name.pm                                |  417 ------
 lib/RT/Extension/AssetSQL.pm                       |  102 ++
 lib/RT/Extension/AssetSQL/Assets.pm                | 1544 ++++++++++++++++++++
 patches/assetsql.patch                             |   43 +
 patches/rt-4.4.0-4.4.1.patch                       |  123 ++
 static/css/assetsql.css                            |    7 +
 46 files changed, 3960 insertions(+), 458 deletions(-)
 create mode 100644 README
 create mode 100644 html/Asset/Elements/SelectAttachmentField
 create mode 100644 html/Asset/Elements/SelectDateType
 create mode 100644 html/Asset/Search/Build.html
 create mode 100644 html/Asset/Search/Bulk.html
 create mode 100644 html/Asset/Search/Edit.html
 create mode 100644 html/Asset/Search/Elements/BuildFormatString
 create mode 100644 html/Asset/Search/Elements/ConditionRow
 create mode 100644 html/Asset/Search/Elements/DisplayOptions
 create mode 100644 html/Asset/Search/Elements/EditFormat
 create mode 100644 html/Asset/Search/Elements/EditQuery
 create mode 100644 html/Asset/Search/Elements/EditSearches
 create mode 100644 html/Asset/Search/Elements/EditSort
 create mode 100644 html/Asset/Search/Elements/NewListActions
 create mode 100644 html/Asset/Search/Elements/PickAssetCFs
 create mode 100644 html/Asset/Search/Elements/PickBasics
 create mode 100644 html/Asset/Search/Elements/PickCriteria
 create mode 100644 html/Asset/Search/Elements/SearchPrivacy
 create mode 100644 html/Asset/Search/Elements/SelectAndOr
 create mode 100644 html/Asset/Search/Elements/SelectLinks
 create mode 100644 html/Asset/Search/Elements/SelectPersonType
 create mode 100644 html/Asset/Search/Elements/SelectSearchObject
 create mode 100644 html/Asset/Search/Elements/SelectSearchesForObjects
 create mode 100644 html/Asset/Search/Results.html
 create mode 100644 html/Asset/Search/Results.tsv
 create mode 100644 html/Callbacks/AssetSQL/Elements/Tabs/Privileged
 delete mode 100644 inc/unicore/Name.pm
 create mode 100644 lib/RT/Extension/AssetSQL/Assets.pm
 create mode 100644 patches/assetsql.patch
 create mode 100644 patches/rt-4.4.0-4.4.1.patch
 create mode 100644 static/css/assetsql.css

- Log -----------------------------------------------------------------
commit 656860135daaac554c717cc6e029d878befdb103
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Jun 14 18:31:35 2016 +0000

    Scaffolding update

diff --git a/META.yml b/META.yml
index d23c672..32b4404 100644
--- a/META.yml
+++ b/META.yml
@@ -8,7 +8,7 @@ configure_requires:
   ExtUtils::MakeMaker: 6.59
 distribution_type: module
 dynamic_config: 1
-generated_by: 'Module::Install version 1.14'
+generated_by: 'Module::Install version 1.16'
 license: gpl
 meta-spec:
   url: http://module-build.sourceforge.net/META-spec-v1.4.html
@@ -22,6 +22,6 @@ requires:
 resources:
   license: http://opensource.org/licenses/gpl-license.php
 version: '0.01'
-x_module_install_rtx_version: '0.36'
+x_module_install_rtx_version: '0.37'
 x_requires_rt: '4.4'
 x_rt_too_new: '4.6'
diff --git a/README b/README
new file mode 100644
index 0000000..f157b7c
--- /dev/null
+++ b/README
@@ -0,0 +1,40 @@
+NAME
+    RT-Extension-AssetSQL - SQL search builder for Assets
+
+INSTALLATION
+    RT-Extension-AssetSQL requires version RT 4.4.0 or later.
+
+    perl Makefile.PL
+    make
+    make install
+        This step may require root permissions.
+
+    Edit your /opt/rt4/etc/RT_SiteConfig.pm
+        Add this line:
+
+            Plugin( "RT::Extension::AssetSQL" );
+
+    Clear your mason cache
+            rm -rf /opt/rt4/var/mason_data/obj
+
+    Restart your webserver
+
+AUTHOR
+    Best Practical Solutions, LLC <modules at bestpractical.com>
+
+BUGS
+    All bugs should be reported via email to
+
+        L<bug-RT-Extension-AssetSQL at rt.cpan.org|mailto:bug-RT-Extension-AssetSQL at rt.cpan.org>
+
+    or via the web at
+
+        L<rt.cpan.org|http://rt.cpan.org/Public/Dist/Display.html?Name=RT-Extension-AssetSQL>.
+
+COPYRIGHT
+    This extension is Copyright (C) 2016 Best Practical Solutions, LLC.
+
+    This is free software, licensed under:
+
+      The GNU General Public License, Version 2, June 1991
+
diff --git a/inc/Module/Install.pm b/inc/Module/Install.pm
index ff767fa..f44ab4d 100644
--- a/inc/Module/Install.pm
+++ b/inc/Module/Install.pm
@@ -31,7 +31,7 @@ BEGIN {
 	# This is not enforced yet, but will be some time in the next few
 	# releases once we can make sure it won't clash with custom
 	# Module::Install extensions.
-	$VERSION = '1.14';
+	$VERSION = '1.16';
 
 	# Storage for the pseudo-singleton
 	$MAIN    = undef;
diff --git a/inc/Module/Install/Base.pm b/inc/Module/Install/Base.pm
index 4206347..5762a74 100644
--- a/inc/Module/Install/Base.pm
+++ b/inc/Module/Install/Base.pm
@@ -4,7 +4,7 @@ package Module::Install::Base;
 use strict 'vars';
 use vars qw{$VERSION};
 BEGIN {
-	$VERSION = '1.14';
+	$VERSION = '1.16';
 }
 
 # Suspend handler for "redefined" warnings
diff --git a/inc/Module/Install/Can.pm b/inc/Module/Install/Can.pm
index 9929b1b..d859276 100644
--- a/inc/Module/Install/Can.pm
+++ b/inc/Module/Install/Can.pm
@@ -8,7 +8,7 @@ use Module::Install::Base ();
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.14';
+	$VERSION = '1.16';
 	@ISA     = 'Module::Install::Base';
 	$ISCORE  = 1;
 }
diff --git a/inc/Module/Install/Fetch.pm b/inc/Module/Install/Fetch.pm
index 3d8de76..41d3517 100644
--- a/inc/Module/Install/Fetch.pm
+++ b/inc/Module/Install/Fetch.pm
@@ -6,7 +6,7 @@ use Module::Install::Base ();
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.14';
+	$VERSION = '1.16';
 	@ISA     = 'Module::Install::Base';
 	$ISCORE  = 1;
 }
diff --git a/inc/Module/Install/Include.pm b/inc/Module/Install/Include.pm
index f274f87..2eb1d1f 100644
--- a/inc/Module/Install/Include.pm
+++ b/inc/Module/Install/Include.pm
@@ -6,7 +6,7 @@ use Module::Install::Base ();
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.14';
+	$VERSION = '1.16';
 	@ISA     = 'Module::Install::Base';
 	$ISCORE  = 1;
 }
diff --git a/inc/Module/Install/Makefile.pm b/inc/Module/Install/Makefile.pm
index 66993af..e9918d2 100644
--- a/inc/Module/Install/Makefile.pm
+++ b/inc/Module/Install/Makefile.pm
@@ -8,7 +8,7 @@ use Fcntl qw/:flock :seek/;
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.14';
+	$VERSION = '1.16';
 	@ISA     = 'Module::Install::Base';
 	$ISCORE  = 1;
 }
diff --git a/inc/Module/Install/Metadata.pm b/inc/Module/Install/Metadata.pm
index e547fa0..9792685 100644
--- a/inc/Module/Install/Metadata.pm
+++ b/inc/Module/Install/Metadata.pm
@@ -6,7 +6,7 @@ use Module::Install::Base ();
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.14';
+	$VERSION = '1.16';
 	@ISA     = 'Module::Install::Base';
 	$ISCORE  = 1;
 }
diff --git a/inc/Module/Install/RTx.pm b/inc/Module/Install/RTx.pm
index 1b55bfa..97acf77 100644
--- a/inc/Module/Install/RTx.pm
+++ b/inc/Module/Install/RTx.pm
@@ -8,7 +8,7 @@ no warnings 'once';
 
 use Module::Install::Base;
 use base 'Module::Install::Base';
-our $VERSION = '0.36';
+our $VERSION = '0.37';
 
 use FindBin;
 use File::Glob     ();
diff --git a/inc/Module/Install/ReadmeFromPod.pm b/inc/Module/Install/ReadmeFromPod.pm
index b5e03c3..3634ee0 100644
--- a/inc/Module/Install/ReadmeFromPod.pm
+++ b/inc/Module/Install/ReadmeFromPod.pm
@@ -7,12 +7,41 @@ use warnings;
 use base qw(Module::Install::Base);
 use vars qw($VERSION);
 
-$VERSION = '0.22';
+$VERSION = '0.26';
+
+{
+
+    # these aren't defined until after _require_admin is run, so
+    # define them so prototypes are available during compilation.
+    sub io;
+    sub capture(&;@);
+
+#line 28
+
+    my $done = 0;
+
+    sub _require_admin {
+
+	# do this once to avoid redefinition warnings from IO::All
+	return if $done;
+
+	require IO::All;
+	IO::All->import( '-binary' );
+
+	require Capture::Tiny;
+	Capture::Tiny->import ( 'capture' );
+
+	return;
+    }
+
+}
 
 sub readme_from {
   my $self = shift;
   return unless $self->is_admin;
 
+  _require_admin;
+
   # Input file
   my $in_file  = shift || $self->_all_from
     or die "Can't determine file to make readme_from";
@@ -50,6 +79,8 @@ sub readme_from {
     $out_file = $self->_readme_htm($in_file, $out_file, $options);
   } elsif ($format eq 'man') {
     $out_file = $self->_readme_man($in_file, $out_file, $options);
+  } elsif ($format eq 'md') {
+    $out_file = $self->_readme_md($in_file, $out_file, $options);
   } elsif ($format eq 'pdf') {
     $out_file = $self->_readme_pdf($in_file, $out_file, $options);
   }
@@ -67,10 +98,10 @@ sub _readme_txt {
   $out_file ||= 'README';
   require Pod::Text;
   my $parser = Pod::Text->new( @$options );
-  open my $out_fh, '>', $out_file or die "Could not write file $out_file:\n$!\n";
+  my $io = io->file($out_file)->open(">");
+  my $out_fh = $io->io_handle;
   $parser->output_fh( *$out_fh );
   $parser->parse_file( $in_file );
-  close $out_fh;
   return $out_file;
 }
 
@@ -79,11 +110,14 @@ sub _readme_htm {
   my ($self, $in_file, $out_file, $options) = @_;
   $out_file ||= 'README.htm';
   require Pod::Html;
-  Pod::Html::pod2html(
-    "--infile=$in_file",
-    "--outfile=$out_file",
-    @$options,
-  );
+  my ($o) = capture {
+    Pod::Html::pod2html(
+      "--infile=$in_file",
+      "--outfile=-",
+      @$options,
+    );
+  };
+  io->file($out_file)->print($o);
   # Remove temporary files if needed
   for my $file ('pod2htmd.tmp', 'pod2htmi.tmp') {
     if (-e $file) {
@@ -99,7 +133,10 @@ sub _readme_man {
   $out_file ||= 'README.1';
   require Pod::Man;
   my $parser = Pod::Man->new( @$options );
-  $parser->parse_from_file($in_file, $out_file);
+  my $io = io->file($out_file)->open(">");
+  my $out_fh = $io->io_handle;
+  $parser->output_fh( *$out_fh );
+  $parser->parse_file( $in_file );
   return $out_file;
 }
 
@@ -111,11 +148,20 @@ sub _readme_pdf {
     or die "Could not generate $out_file because pod2pdf could not be found\n";
   my $parser = App::pod2pdf->new( @$options );
   $parser->parse_from_file($in_file);
-  open my $out_fh, '>', $out_file or die "Could not write file $out_file:\n$!\n";
-  select $out_fh;
-  $parser->output;
-  select STDOUT;
-  close $out_fh;
+  my ($o) = capture { $parser->output };
+  io->file($out_file)->print($o);
+  return $out_file;
+}
+
+sub _readme_md {
+  my ($self, $in_file, $out_file, $options) = @_;
+  $out_file ||= 'README.md';
+  require Pod::Markdown;
+  my $parser = Pod::Markdown->new( @$options );
+  my $io = io->file($out_file)->open(">");
+  my $out_fh = $io->io_handle;
+  $parser->output_fh( *$out_fh );
+  $parser->parse_file( $in_file );
   return $out_file;
 }
 
@@ -134,5 +180,5 @@ sub _all_from {
 
 __END__
 
-#line 254
+#line 316
 
diff --git a/inc/Module/Install/Win32.pm b/inc/Module/Install/Win32.pm
index 9706e5f..218a66b 100644
--- a/inc/Module/Install/Win32.pm
+++ b/inc/Module/Install/Win32.pm
@@ -6,7 +6,7 @@ use Module::Install::Base ();
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.14';
+	$VERSION = '1.16';
 	@ISA     = 'Module::Install::Base';
 	$ISCORE  = 1;
 }
diff --git a/inc/Module/Install/WriteAll.pm b/inc/Module/Install/WriteAll.pm
index dbedc00..530749b 100644
--- a/inc/Module/Install/WriteAll.pm
+++ b/inc/Module/Install/WriteAll.pm
@@ -6,7 +6,7 @@ use Module::Install::Base ();
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.14';
+	$VERSION = '1.16';
 	@ISA     = qw{Module::Install::Base};
 	$ISCORE  = 1;
 }
diff --git a/inc/YAML/Tiny.pm b/inc/YAML/Tiny.pm
index 1be0cb1..aa539f7 100644
--- a/inc/YAML/Tiny.pm
+++ b/inc/YAML/Tiny.pm
@@ -2,16 +2,12 @@
 use 5.008001; # sane UTF-8 support
 use strict;
 use warnings;
-package YAML::Tiny;
-BEGIN {
-  $YAML::Tiny::AUTHORITY = 'cpan:ADAMK';
-}
-# git description: v1.61-3-g0a82466
-$YAML::Tiny::VERSION = '1.62';
+package YAML::Tiny; # git description: v1.68-2-gcc5324e
 # XXX-INGY is 5.8.1 too old/broken for utf8?
 # XXX-XDG Lancaster consensus was that it was sufficient until
 # proven otherwise
 
+our $VERSION = '1.69';
 
 #####################################################################
 # The YAML::Tiny API.
@@ -300,10 +296,11 @@ Did you decode with lax ":utf8" instead of strict ":encoding(UTF-8)"?
             }
         }
     };
-    if ( ref $@ eq 'SCALAR' ) {
-        $self->_error(${$@});
-    } elsif ( $@ ) {
-        $self->_error($@);
+    my $err = $@;
+    if ( ref $err eq 'SCALAR' ) {
+        $self->_error(${$err});
+    } elsif ( $err ) {
+        $self->_error($err);
     }
 
     return $self;
@@ -515,6 +512,10 @@ sub _load_hash {
             die \"YAML::Tiny failed to classify line '$lines->[0]'";
         }
 
+        if ( exists $hash->{$key} ) {
+            warn "YAML::Tiny found a duplicate key '$key' in line '$lines->[0]'";
+        }
+
         # Do we have a value?
         if ( length $lines->[0] ) {
             # Yes
@@ -828,9 +829,10 @@ sub _can_flock {
 #####################################################################
 # Use Scalar::Util if possible, otherwise emulate it
 
+use Scalar::Util ();
 BEGIN {
     local $@;
-    if ( eval { require Scalar::Util; Scalar::Util->VERSION(1.18); } ) {
+    if ( eval { Scalar::Util->VERSION(1.18); } ) {
         *refaddr = *Scalar::Util::refaddr;
     }
     else {
@@ -852,8 +854,7 @@ END_PERL
     }
 }
 
-
-
+delete $YAML::Tiny::{refaddr};
 
 1;
 
@@ -870,4 +871,4 @@ END_PERL
 
 __END__
 
-#line 1488
+#line 1489
diff --git a/inc/unicore/Name.pm b/inc/unicore/Name.pm
deleted file mode 100644
index d72eb6e..0000000
--- a/inc/unicore/Name.pm
+++ /dev/null
@@ -1,417 +0,0 @@
-#line 1
-# !!!!!!!   DO NOT EDIT THIS FILE   !!!!!!!
-# This file is machine-generated by lib/unicore/mktables from the Unicode
-# database, Version 6.3.0.  Any changes made here will be lost!
-
-
-# !!!!!!!   INTERNAL PERL USE ONLY   !!!!!!!
-# This file is for internal use by core Perl only.  The format and even the
-# name or existence of this file are subject to change without notice.  Don't
-# use it directly.  Use Unicode::UCD to access the Unicode character data
-# base.
-
-
-package charnames;
-
-# This module contains machine-generated tables and code for the
-# algorithmically-determinable Unicode character names.  The following
-# routines can be used to translate between name and code point and vice versa
-
-{ # Closure
-
-    # Matches legal code point.  4-6 hex numbers, If there are 6, the first
-    # two must be 10; if there are 5, the first must not be a 0.  Written this
-    # way to decrease backtracking.  The first regex allows the code point to
-    # be at the end of a word, but to work properly, the word shouldn't end
-    # with a valid hex character.  The second one won't match a code point at
-    # the end of a word, and doesn't have the run-on issue
-    my $run_on_code_point_re = qr/(?^aax: (?: 10[0-9A-F]{4} | [1-9A-F][0-9A-F]{4} | [0-9A-F]{4} ) \b)/;
-    my $code_point_re = qr/(?^aa:\b(?^aax: (?: 10[0-9A-F]{4} | [1-9A-F][0-9A-F]{4} | [0-9A-F]{4} ) \b))/;
-
-    # In the following hash, the keys are the bases of names which include
-    # the code point in the name, like CJK UNIFIED IDEOGRAPH-4E01.  The value
-    # of each key is another hash which is used to get the low and high ends
-    # for each range of code points that apply to the name.
-    my %names_ending_in_code_point = (
-'CJK COMPATIBILITY IDEOGRAPH' => 
-{
-'high' => 
-[
-64109,
-64217,
-195101,
-],
-'low' => 
-[
-63744,
-64112,
-194560,
-],
-},
-'CJK UNIFIED IDEOGRAPH' => 
-{
-'high' => 
-[
-19893,
-40908,
-173782,
-177972,
-178205,
-],
-'low' => 
-[
-13312,
-19968,
-131072,
-173824,
-177984,
-],
-},
-
-    );
-
-    # The following hash is a copy of the previous one, except is for loose
-    # matching, so each name has blanks and dashes squeezed out
-    my %loose_names_ending_in_code_point = (
-'CJKCOMPATIBILITYIDEOGRAPH' => 
-{
-'high' => 
-[
-64109,
-64217,
-195101,
-],
-'low' => 
-[
-63744,
-64112,
-194560,
-],
-},
-'CJKUNIFIEDIDEOGRAPH' => 
-{
-'high' => 
-[
-19893,
-40908,
-173782,
-177972,
-178205,
-],
-'low' => 
-[
-13312,
-19968,
-131072,
-173824,
-177984,
-],
-},
-
-    );
-
-    # And the following array gives the inverse mapping from code points to
-    # names.  Lowest code points are first
-    my @code_points_ending_in_code_point = (
-
-{
-'high' => 19893,
-'low' => 13312,
-'name' => 'CJK UNIFIED IDEOGRAPH',
-},
-{
-'high' => 40908,
-'low' => 19968,
-'name' => 'CJK UNIFIED IDEOGRAPH',
-},
-{
-'high' => 64109,
-'low' => 63744,
-'name' => 'CJK COMPATIBILITY IDEOGRAPH',
-},
-{
-'high' => 64217,
-'low' => 64112,
-'name' => 'CJK COMPATIBILITY IDEOGRAPH',
-},
-{
-'high' => 173782,
-'low' => 131072,
-'name' => 'CJK UNIFIED IDEOGRAPH',
-},
-{
-'high' => 177972,
-'low' => 173824,
-'name' => 'CJK UNIFIED IDEOGRAPH',
-},
-{
-'high' => 178205,
-'low' => 177984,
-'name' => 'CJK UNIFIED IDEOGRAPH',
-},
-{
-'high' => 195101,
-'low' => 194560,
-'name' => 'CJK COMPATIBILITY IDEOGRAPH',
-},
-,
-
-    );
-
-    # Convert from code point to Jamo short name for use in composing Hangul
-    # syllable names
-    my %Jamo = (
-4352 => 'G',
-4353 => 'GG',
-4354 => 'N',
-4355 => 'D',
-4356 => 'DD',
-4357 => 'R',
-4358 => 'M',
-4359 => 'B',
-4360 => 'BB',
-4361 => 'S',
-4362 => 'SS',
-4363 => '',
-4364 => 'J',
-4365 => 'JJ',
-4366 => 'C',
-4367 => 'K',
-4368 => 'T',
-4369 => 'P',
-4370 => 'H',
-4449 => 'A',
-4450 => 'AE',
-4451 => 'YA',
-4452 => 'YAE',
-4453 => 'EO',
-4454 => 'E',
-4455 => 'YEO',
-4456 => 'YE',
-4457 => 'O',
-4458 => 'WA',
-4459 => 'WAE',
-4460 => 'OE',
-4461 => 'YO',
-4462 => 'U',
-4463 => 'WEO',
-4464 => 'WE',
-4465 => 'WI',
-4466 => 'YU',
-4467 => 'EU',
-4468 => 'YI',
-4469 => 'I',
-4520 => 'G',
-4521 => 'GG',
-4522 => 'GS',
-4523 => 'N',
-4524 => 'NJ',
-4525 => 'NH',
-4526 => 'D',
-4527 => 'L',
-4528 => 'LG',
-4529 => 'LM',
-4530 => 'LB',
-4531 => 'LS',
-4532 => 'LT',
-4533 => 'LP',
-4534 => 'LH',
-4535 => 'M',
-4536 => 'B',
-4537 => 'BS',
-4538 => 'S',
-4539 => 'SS',
-4540 => 'NG',
-4541 => 'J',
-4542 => 'C',
-4543 => 'K',
-4544 => 'T',
-4545 => 'P',
-4546 => 'H',
-
-    );
-
-    # Leading consonant (can be null)
-    my %Jamo_L = (
-'' => 11,
-'B' => 7,
-'BB' => 8,
-'C' => 14,
-'D' => 3,
-'DD' => 4,
-'G' => 0,
-'GG' => 1,
-'H' => 18,
-'J' => 12,
-'JJ' => 13,
-'K' => 15,
-'M' => 6,
-'N' => 2,
-'P' => 17,
-'R' => 5,
-'S' => 9,
-'SS' => 10,
-'T' => 16,
-
-    );
-
-    # Vowel
-    my %Jamo_V = (
-'A' => 0,
-'AE' => 1,
-'E' => 5,
-'EO' => 4,
-'EU' => 18,
-'I' => 20,
-'O' => 8,
-'OE' => 11,
-'U' => 13,
-'WA' => 9,
-'WAE' => 10,
-'WE' => 15,
-'WEO' => 14,
-'WI' => 16,
-'YA' => 2,
-'YAE' => 3,
-'YE' => 7,
-'YEO' => 6,
-'YI' => 19,
-'YO' => 12,
-'YU' => 17,
-
-    );
-
-    # Optional trailing consonant
-    my %Jamo_T = (
-'B' => 17,
-'BS' => 18,
-'C' => 23,
-'D' => 7,
-'G' => 1,
-'GG' => 2,
-'GS' => 3,
-'H' => 27,
-'J' => 22,
-'K' => 24,
-'L' => 8,
-'LB' => 11,
-'LG' => 9,
-'LH' => 15,
-'LM' => 10,
-'LP' => 14,
-'LS' => 12,
-'LT' => 13,
-'M' => 16,
-'N' => 4,
-'NG' => 21,
-'NH' => 6,
-'NJ' => 5,
-'P' => 26,
-'S' => 19,
-'SS' => 20,
-'T' => 25,
-
-    );
-
-    # Computed re that splits up a Hangul name into LVT or LV syllables
-    my $syllable_re = qr/(|B|BB|C|D|DD|G|GG|H|J|JJ|K|M|N|P|R|S|SS|T)(A|AE|E|EO|EU|I|O|OE|U|WA|WAE|WE|WEO|WI|YA|YAE|YE|YEO|YI|YO|YU)(B|BS|C|D|G|GG|GS|H|J|K|L|LB|LG|LH|LM|LP|LS|LT|M|N|NG|NH|NJ|P|S|SS|T)?/;
-
-    my $HANGUL_SYLLABLE = "HANGUL SYLLABLE ";
-    my $loose_HANGUL_SYLLABLE = "HANGULSYLLABLE";
-
-    # These constants names and values were taken from the Unicode standard,
-    # version 5.1, section 3.12.  They are used in conjunction with Hangul
-    # syllables
-    my $SBase = 0xAC00;
-    my $LBase = 0x1100;
-    my $VBase = 0x1161;
-    my $TBase = 0x11A7;
-    my $SCount = 11172;
-    my $LCount = 19;
-    my $VCount = 21;
-    my $TCount = 28;
-    my $NCount = $VCount * $TCount;
-
-    sub name_to_code_point_special {
-        my ($name, $loose) = @_;
-
-        # Returns undef if not one of the specially handled names; otherwise
-        # returns the code point equivalent to the input name
-        # $loose is non-zero if to use loose matching, 'name' in that case
-        # must be input as upper case with all blanks and dashes squeezed out.
-
-        if ((! $loose && $name =~ s/$HANGUL_SYLLABLE//)
-            || ($loose && $name =~ s/$loose_HANGUL_SYLLABLE//))
-        {
-            return if $name !~ qr/^$syllable_re$/;
-            my $L = $Jamo_L{$1};
-            my $V = $Jamo_V{$2};
-            my $T = (defined $3) ? $Jamo_T{$3} : 0;
-            return ($L * $VCount + $V) * $TCount + $T + $SBase;
-        }
-
-        # Name must end in 'code_point' for this to handle.
-        return if (($loose && $name !~ /^ (.*?) ($run_on_code_point_re) $/x)
-                   || (! $loose && $name !~ /^ (.*) ($code_point_re) $/x));
-
-        my $base = $1;
-        my $code_point = CORE::hex $2;
-        my $names_ref;
-
-        if ($loose) {
-            $names_ref = \%loose_names_ending_in_code_point;
-        }
-        else {
-            return if $base !~ s/-$//;
-            $names_ref = \%names_ending_in_code_point;
-        }
-
-        # Name must be one of the ones which has the code point in it.
-        return if ! $names_ref->{$base};
-
-        # Look through the list of ranges that apply to this name to see if
-        # the code point is in one of them.
-        for (my $i = 0; $i < scalar @{$names_ref->{$base}{'low'}}; $i++) {
-            return if $names_ref->{$base}{'low'}->[$i] > $code_point;
-            next if $names_ref->{$base}{'high'}->[$i] < $code_point;
-
-            # Here, the code point is in the range.
-            return $code_point;
-        }
-
-        # Here, looked like the name had a code point number in it, but
-        # did not match one of the valid ones.
-        return;
-    }
-
-    sub code_point_to_name_special {
-        my $code_point = shift;
-
-        # Returns the name of a code point if algorithmically determinable;
-        # undef if not
-
-        # If in the Hangul range, calculate the name based on Unicode's
-        # algorithm
-        if ($code_point >= $SBase && $code_point <= $SBase + $SCount -1) {
-            use integer;
-            my $SIndex = $code_point - $SBase;
-            my $L = $LBase + $SIndex / $NCount;
-            my $V = $VBase + ($SIndex % $NCount) / $TCount;
-            my $T = $TBase + $SIndex % $TCount;
-            $name = "$HANGUL_SYLLABLE$Jamo{$L}$Jamo{$V}";
-            $name .= $Jamo{$T} if $T != $TBase;
-            return $name;
-        }
-
-        # Look through list of these code points for one in range.
-        foreach my $hash (@code_points_ending_in_code_point) {
-            return if $code_point < $hash->{'low'};
-            if ($code_point <= $hash->{'high'}) {
-                return sprintf("%s-%04X", $hash->{'name'}, $code_point);
-            }
-        }
-        return;            # None found
-    }
-} # End closure
-
-1;

commit 83af1bf91c7d5ec2c4fc69ae8b782069d1c3a6aa
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Jun 15 18:42:32 2016 +0000

    First cut

diff --git a/META.yml b/META.yml
index 32b4404..9a2fd9b 100644
--- a/META.yml
+++ b/META.yml
@@ -16,7 +16,12 @@ meta-spec:
 name: RT-Extension-AssetSQL
 no_index:
   directory:
+    - etc
+    - html
     - inc
+    - static
+  package:
+    - RT::Assets
 requires:
   perl: 5.10.1
 resources:
diff --git a/Makefile.PL b/Makefile.PL
index 459f728..af713b1 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -2,6 +2,8 @@ use inc::Module::Install;
 
 RTx 'RT-Extension-AssetSQL';
 
+no_index package => "RT::Assets";
+
 requires_rt '4.4';
 rt_too_new '4.6';
 
diff --git a/README b/README
index f157b7c..9f36c4c 100644
--- a/README
+++ b/README
@@ -9,6 +9,20 @@ INSTALLATION
     make install
         This step may require root permissions.
 
+    Patch your RT
+        AssetSQL requires a small patch to work on versions of RT prior to
+        4.4.2. To patch such older versions of RT, run:
+
+            patch -d /opt/rt4 -p1 < patches/rt-4.4.0-4.4.1.patch
+
+        RT versions 4.4.2 and later already contain the above patch.
+
+        All versions of RT require the following patch for AssetSQL support:
+
+            patch -d /opt/rt4 -p1 < patches/assetsql.patch
+
+        You must apply both patches if you're on RT 4.4.0 or 4.4.1.
+
     Edit your /opt/rt4/etc/RT_SiteConfig.pm
         Add this line:
 
diff --git a/html/Asset/Elements/SelectAttachmentField b/html/Asset/Elements/SelectAttachmentField
new file mode 100644
index 0000000..73cc92e
--- /dev/null
+++ b/html/Asset/Elements/SelectAttachmentField
@@ -0,0 +1,7 @@
+<select name="<%$Name%>">
+<option value="Name"><&|/l&>Name</&></option>
+<option value="Description"><&|/l&>Description</&></option>
+</select>
+<%ARGS>
+$Name => 'AttachmentField'
+</%ARGS>
diff --git a/html/Asset/Elements/SelectDateType b/html/Asset/Elements/SelectDateType
new file mode 100644
index 0000000..919d0b7
--- /dev/null
+++ b/html/Asset/Elements/SelectDateType
@@ -0,0 +1,7 @@
+<select name="<%$Name%>">
+<option value="Created"><&|/l&>Created</&></option>
+<option value="LastUpdated"><&|/l&>Last Updated</&></option>
+</select>
+<%ARGS>
+$Name => 'DateType'
+</%ARGS>
diff --git a/html/Asset/Search/Build.html b/html/Asset/Search/Build.html
new file mode 100644
index 0000000..20b0403
--- /dev/null
+++ b/html/Asset/Search/Build.html
@@ -0,0 +1,287 @@
+%#
+%# Data flow here:
+%#   The page receives a Query from the previous page, and maybe arguments
+%#   corresponding to actions.  (If it doesn't get a Query argument, it pulls
+%#   one out of the session hash.  Also, it could be getting just a raw query from
+%#   Build/Edit.html (Advanced).)
+%#
+%#   After doing some stuff with default arguments and saved searches, the ParseQuery
+%#   function (which is similar to, but not the same as, _parser in lib/RT/Assets.pm)
+%#   converts the Query into a RT::Interface::Web::QueryBuilder::Tree.  This mason file
+%#   then adds stuff to or modifies the tree based on the actions that had been requested
+%#   by clicking buttons.  It then calls GetQueryAndOptionList on the tree to generate
+%#   the SQL query (which is saved as a hidden input) and the option list for the Clauses
+%#   box in the top right corner.
+%#
+%#   Worthwhile refactoring: the tree manipulation code for the actions could use some cleaning
+%#   up.  The node-adding code is different in the "add" actions from in ParseQuery, which leads
+%#   to things like ParseQuery correctly not quoting numbers in numerical fields, while the "add"
+%#   action does quote it (this breaks SQLite).
+%#
+<& /Elements/Header, Title => $title &>
+<& /Elements/Tabs, %TabArgs &>
+
+<form method="post" action="Build.html" name="BuildQuery" id="BuildQuery">
+<input type="hidden" class="hidden" name="SavedSearchId" value="<% $saved_search{'Id'} %>" />
+<input type="hidden" class="hidden" name="Query" value="<% $query{'Query'} %>" />
+<input type="hidden" class="hidden" name="Format" value="<% $query{'Format'} %>" />
+
+
+
+
+<div id="pick-criteria">
+    <& Elements/PickCriteria, query => $query{'Query'}, catalogs => $catalogs &>
+</div>
+<& /Elements/Submit,  Label => loc('Add these terms'), SubmitId => 'AddClause', Name => 'AddClause'&>
+<& /Elements/Submit, Label => loc('Add these terms and Search'), SubmitId => 'DoSearch', Name => 'DoSearch'&>
+
+
+<div id="editquery">
+<& Elements/EditQuery,
+    %ARGS,
+    actions => \@actions,
+    optionlist => $optionlist,
+    Description => $saved_search{'Description'},
+    &>
+</div>
+<div id="editsearches">
+    <& Elements/EditSearches, %saved_search, CurrentSearch => \%query &>
+</div>
+
+<span id="display-options">
+<& Elements/DisplayOptions,
+    %ARGS, %query,
+    AvailableColumns => $AvailableColumns,
+    CurrentFormat    => $CurrentFormat,
+&>
+</span>
+<& /Elements/Submit, Label => loc('Update format and Search'), Name => 'DoSearch', id=>"formatbuttons"&>
+</form>
+
+<%INIT>
+use RT::Interface::Web::QueryBuilder;
+use RT::Interface::Web::QueryBuilder::Tree;
+
+my $title = loc("Asset Query Builder");
+
+my %query;
+for( qw(Query Format OrderBy Order RowsPerPage) ) {
+    $query{$_} = $ARGS{$_};
+}
+
+my %saved_search;
+my @actions = $m->comp( 'Elements/EditSearches:Init', %ARGS, Query => \%query, SavedSearch => \%saved_search);
+
+if ( $NewQuery ) {
+
+    # Wipe all data-carrying variables clear if we want a new
+    # search, or we're deleting an old one..
+    %query = ();
+    %saved_search = ( Id => 'new', Type => 'Asset', );
+
+    # ..then wipe the session out..
+    delete $session{'CurrentAssetSearchHash'};
+
+    # ..and the search results.
+    $session{'assets'}->CleanSlate if defined $session{'assets'};
+}
+
+{ # Attempt to load what we can from the session and preferences, set defaults
+
+    my $current = $session{'CurrentAssetSearchHash'};
+    my $default = { Query => '',
+                    Format => '',
+                    OrderBy => 'id',
+                    Order => 'ASC',
+                    RowsPerPage => 50 };
+
+    for( qw(Query Format OrderBy Order RowsPerPage) ) {
+        $query{$_} = $current->{$_} unless defined $query{$_};
+        $query{$_} = $default->{$_} unless defined $query{$_};
+    }
+
+    for( qw(Order OrderBy) ) {
+        if (ref $query{$_} eq "ARRAY") {
+            $query{$_} = join( '|', @{ $query{$_} } );
+        }
+    }
+    if ( $query{'Format'} ) {
+        # Clean unwanted junk from the format
+        $query{'Format'} = $m->comp( '/Elements/ScrubHTML', Content => $query{'Format'} );
+    }
+}
+
+my $ParseQuery = sub {
+    my ($string, $results) = @_;
+
+    my $tree = RT::Interface::Web::QueryBuilder::Tree->new('AND');
+    @$results = $tree->ParseAssetSQL( Query => $string, CurrentUser => $session{'CurrentUser'} );
+
+    return $tree;
+};
+
+my @parse_results;
+my $tree = $ParseQuery->( $query{'Query'}, \@parse_results );
+
+# if parsing went poorly, send them to the edit page to fix it
+if ( @parse_results ) {
+    push @actions, @parse_results;
+    return $m->comp(
+        "Edit.html",
+        Query => $query{'Query'},
+        Format => $query{'Format'},
+        SavedSearchId => $saved_search{'Id'},
+        actions => \@actions,
+    );
+}
+
+my @options = $tree->GetDisplayedNodes;
+my @current_values = grep defined, @options[@clauses];
+my @new_values = ();
+
+my $cf_field_names =
+    join "|",
+     map quotemeta,
+    grep { $RT::Assets::FIELD_METADATA{$_}->[0] eq 'CUSTOMFIELD' }
+    sort keys %RT::Assets::FIELD_METADATA;
+
+# Try to find if we're adding a clause
+foreach my $arg ( keys %ARGS ) {
+    next unless $arg =~ m/^ValueOf(\w+|($cf_field_names).\{.*?\})$/
+                && ( ref $ARGS{$arg} eq "ARRAY"
+                     ? grep $_ ne '', @{ $ARGS{$arg} }
+                     : $ARGS{$arg} ne '' );
+
+    # We're adding a $1 clause
+    my $field = $1;
+
+    my ($op, $value);
+
+    #figure out if it's a grouping
+    my $keyword = $ARGS{ $field . "Field" } || $field;
+
+    my ( @ops, @values );
+    if ( ref $ARGS{ 'ValueOf' . $field } eq "ARRAY" ) {
+        # we have many keys/values to iterate over, because there is
+        # more than one CF with the same name.
+        @ops    = @{ $ARGS{ $field . 'Op' } };
+        @values = @{ $ARGS{ 'ValueOf' . $field } };
+    }
+    else {
+        @ops    = ( $ARGS{ $field . 'Op' } );
+        @values = ( $ARGS{ 'ValueOf' . $field } );
+    }
+    $RT::Logger->error("Bad Parameters passed into Query Builder")
+        unless @ops == @values;
+
+    for ( my $i = 0; $i < @ops; $i++ ) {
+        my ( $op, $value ) = ( $ops[$i], $values[$i] );
+        next if !defined $value || $value eq '';
+        my $rawvalue = $value;
+
+        if ( $value =~ /^NULL$/i && $op =~ /=/ ) {
+            if ( $op eq '=' ) {
+                $op = "IS";
+            }
+            elsif ( $op eq '!=' ) {
+                $op = "IS NOT";
+            }
+        }
+        elsif ($value =~ /\D/) {
+            $value =~ s/(['\\])/\\$1/g;
+            $value = "'$value'";
+        }
+
+        if ($keyword =~ s/(['\\])/\\$1/g or $keyword =~ /[^{}\w\.]/) {
+            $keyword = "'$keyword'";
+        }
+
+        my $clause = {
+            Key   => $keyword,
+            Op    => $op,
+            Value => $value,
+            RawValue => $rawvalue,
+        };
+
+        push @new_values, RT::Interface::Web::QueryBuilder::Tree->new($clause);
+    }
+}
+
+
+push @actions, $m->comp('Elements/EditQuery:Process',
+    %ARGS,
+    Tree     => $tree,
+    Selected => \@current_values,
+    New      => \@new_values,
+);
+
+# Rebuild $Query based on the additions / movements
+
+my $optionlist_arrayref;
+($query{'Query'}, $optionlist_arrayref) = $tree->GetQueryAndOptionList(\@current_values);
+
+my $optionlist = join "\n", map { qq(<option value="$_->{INDEX}" $_->{SELECTED}>) 
+                                  . (" " x (5 * $_->{DEPTH}))
+                                  . $m->interp->apply_escapes($_->{TEXT}, 'h') . qq(</option>) } @$optionlist_arrayref;
+
+
+my $catalogs = $tree->GetReferencedCatalogs;
+
+# Deal with format changes
+my ( $AvailableColumns, $CurrentFormat );
+( $query{'Format'}, $AvailableColumns, $CurrentFormat ) = $m->comp(
+    'Elements/BuildFormatString',
+    %ARGS,
+    catalogs => $catalogs,
+    Format => $query{'Format'},
+);
+
+
+# if we're asked to save the current search, save it
+push @actions, $m->comp( 'Elements/EditSearches:Save', %ARGS, Query => \%query, SavedSearch => \%saved_search);
+
+# Populate the "query" context with saved search data
+
+if ($ARGS{SavedSearchSave}) {
+    $query{'SavedSearchId'} = $saved_search{'Id'};
+}
+
+# Push the updates into the session so we don't lose 'em
+
+$session{'CurrentAssetSearchHash'} = {
+    %query,
+    SearchId    => $saved_search{'Id'},
+    Object      => $saved_search{'Object'},
+    Description => $saved_search{'Description'},
+};
+
+
+# Show the results, if we were asked.
+
+if ( $ARGS{'DoSearch'} ) {
+    my $redir_query_string = $m->comp(
+        '/Elements/QueryString',
+        %query,
+        SavedSearchId => $saved_search{'Id'},
+    );
+    RT::Interface::Web::Redirect(RT->Config->Get('WebURL') . '/Asset/Search/Results.html?' . $redir_query_string);
+    $m->abort;
+}
+
+
+# Build a querystring for the tabs
+
+my %TabArgs = ();
+if ($NewQuery) {
+    $TabArgs{QueryString} = 'NewQuery=1';
+}
+elsif ( $query{'Query'} ) {
+    $TabArgs{QueryArgs} = \%query;
+}
+
+</%INIT>
+
+<%ARGS>
+$NewQuery => 0
+ at clauses => ()
+</%ARGS>
diff --git a/html/Asset/Search/Bulk.html b/html/Asset/Search/Bulk.html
new file mode 100644
index 0000000..a03cd03
--- /dev/null
+++ b/html/Asset/Search/Bulk.html
@@ -0,0 +1,152 @@
+<& /Elements/Header, Title => loc("Assets") &>
+<& /Elements/Tabs &>
+
+% $m->callback(CallbackName => 'BeforeActionList', ARGSRef => \%ARGS, Assets => $assets, Actions => \@results);
+
+<& /Elements/ListActions, actions => \@results &>
+
+<form method="post" action="<% RT->Config->Get('WebPath') %>/Asset/Search/Bulk.html" enctype="multipart/form-data" name="BulkUpdate" id="BulkUpdate">
+% foreach my $var ( @{$search{'PassArguments'}} )  {
+<input type="hidden" class="hidden" name="<% $var %>" value="<% $ARGS{$var} || '' %>" />
+% }
+% foreach my $var (qw(Query Format OrderBy Order Rows Page Token)) {
+<input type="hidden" class="hidden" name="<%$var%>" value="<%$ARGS{$var} || ''%>" />
+%}
+<& /Elements/CollectionList,
+    %search,
+    Collection      => $assets,
+    AllowSorting    => 1,
+    DisplayFormat   => $DisplayFormat,
+    &>
+% if (not $assets->Count) {
+<em><&|/l&>No assets matching search criteria found.</&></em>
+% }
+
+<& /Elements/Submit,
+    Name => 'Update',
+    Label => loc('Update'),
+    CheckboxNameRegex => '/^UpdateAsset(All)?$/',
+    CheckAll => 1, ClearAll => 1,
+&>
+
+<&| /Widgets/TitleBox, title => loc("Basics"), class => "asset-basics asset-bulk-basics", title_class => "inverse" &>
+<table>
+  <tr class="asset-catalog">
+    <td class="label"><label for="UpdateCatalog"><&|/l&>Catalog</&></label></td>
+    <td><& /Asset/Elements/SelectCatalog, Name => 'UpdateCatalog', UpdateSession => 0, ShowNullOption => 1 &></td>
+  </tr>
+  <tr class="asset-status">
+    <td class="label"><label for="UpdateStatus"><&|/l&>Status</&></label></td>
+    <td><& /Asset/Elements/SelectStatus, Name => 'UpdateStatus', DefaultValue => 1 &></td>
+  </tr>
+</table>
+</&>
+
+<&| /Widgets/TitleBox, title => loc("People"), class => "asset-people asset-bulk-people", title_class => "inverse" &>
+<table>
+% for my $rname ( $asset->Roles( ACLOnly => 0 ) ) {
+% my $role = $asset->Role( $rname );
+% if ( $role->{'Single'} ) {
+% my $input = "SetRoleMember-$rname";
+<tr class="full-width">
+<td class="label"><label for="<% $input %>"><% loc($rname) %></label></td>
+<td><input type="text" value="<% $ARGS{ $input } || '' %>" name="<% $input %>" id="<% $input %>" data-autocomplete="Users" data-autocomplete-return="Name" /></td>
+</tr>
+% } else {
+<tr>
+% my $input = "AddRoleMember-$rname";
+<td class="label"><label for="<% $input %>"><% loc("Add [_1]", loc($rname)) %></label></td>
+<td><input type="text" value="<% $ARGS{ $input } || '' %>" name="<% $input %>" id="<% $input %>" data-autocomplete="Users" data-autocomplete-return="Name" /></td>
+</tr>
+
+<tr>
+% $input = "RemoveRoleMember-$rname";
+<td class="label"><label for="<% $input %>"><% loc("Remove [_1]", loc($rname)) %></label></td>
+<td>
+  <input type="text" value="<% $ARGS{ $input } || '' %>" name="<% $input %>" id="<% $input %>" data-autocomplete="Users" data-autocomplete-return="Name" />
+  <label>
+    <input type="checkbox" name="RemoveAllRoleMembers-<% $rname %>" value="1"/>
+    <em><&|/l&>(Check to delete all values)</&></em>
+  </label>
+</td>
+</tr>
+% }
+% }
+</table>
+</&>
+
+% for my $group ( RT::CustomField->CustomGroupings( 'RT::Asset' ), '' ) {
+%   my $cfs = $catalog_obj->AssetCustomFields;
+%   $cfs->LimitToGrouping( 'RT::Asset' => $group);
+%   if ( $cfs->Count ) {
+<&| /Widgets/TitleBox, class=>'asset-bulk-grouping asset-bulk-cfs', title => loc('Edit [_1]', ($group? loc($group) : loc('Custom Fields')) ) &>
+<& /Elements/BulkCustomFields, CustomFields => $cfs, &>
+</&>
+%   }
+% }
+
+<&|/Widgets/TitleBox, title => loc('Edit Links'), color => "#336633"&>
+<& /Elements/BulkLinks, Collection => $assets, %ARGS &>
+</&>
+
+<& /Elements/Submit, Label => loc('Update'), Name => 'Update' &>
+</form>
+
+<%INIT>
+my @results;
+$m->callback(ARGSRef => \%ARGS, Results => \@results, CallbackName => 'Initial');
+
+my $catalog_obj = LoadDefaultCatalog($ARGS{'Catalog'} || '');
+$ARGS{'Catalog'} = $catalog_obj->Id;
+
+my $assets = RT::Assets->new($session{CurrentUser});
+my %search;
+if ( $ARGS{Query} ) {
+    $assets->FromSQL($ARGS{Query});
+}
+else {
+    %search = ProcessAssetsSearchArguments(
+        Assets => $assets, Catalog => $catalog_obj, ARGSRef => \%ARGS,
+    );
+}
+$search{Format} ||= RT->Config->Get('AssetSearchFormat');
+
+my $DisplayFormat = "'__CheckBox.{UpdateAsset}__',". ($ARGS{Format} || $search{'Format'});
+$DisplayFormat =~ s/\s*,\s*('?__NEWLINE__'?)/,$1,''/gi;
+
+my $asset = RT::Asset->new( $session{'CurrentUser'} );
+
+delete $ARGS{$_} foreach grep { $ARGS{$_} =~ /^$/ } keys %ARGS;
+
+$DECODED_ARGS->{'UpdateAssetAll'} = 1 unless @UpdateAsset;
+
+if ( $ARGS{Update} ) {
+    my @attributes  = $asset->WritableAttributes;
+    @attributes = grep exists $ARGS{ 'Update'. $_ }, @attributes;
+    my %basics = map { $_ => $ARGS{ 'Update'. $_ } } @attributes;
+
+    foreach my $aid ( @UpdateAsset ) {
+        my $asset = LoadAsset($aid);
+
+        my @tmp_res;
+        push @tmp_res, UpdateRecordObject(
+            Object          => $asset,
+            AttributesRef   => \@attributes,
+            ARGSRef         => \%basics,
+        );
+        push @tmp_res, ProcessRoleMembers( $asset => %ARGS );
+        push @tmp_res, ProcessObjectCustomFieldUpdates( Object => $asset, ARGSRef => \%ARGS );
+        push @tmp_res, ProcessRecordLinks( RecordObj => $asset, RecordId => 'Asset', ARGSRef => \%ARGS );
+        push @tmp_res, ProcessRecordBulkCustomFields( RecordObj => $asset, ARGSRef => \%ARGS );
+        push @results, map { loc( "Asset #[_1]: [_2]", $asset->id, $_ ) } @tmp_res;
+    }
+
+    MaybeRedirectForResults(
+        Actions     => \@results,
+        Arguments   => { map { $_ => $ARGS{$_} } grep { defined $ARGS{$_} } @{$search{'PassArguments'}}, qw(Query Format OrderBy Order Rows Page Token) },
+    );
+}
+</%INIT>
+<%ARGS>
+ at UpdateAsset => ()
+</%ARGS>
diff --git a/html/Asset/Search/Edit.html b/html/Asset/Search/Edit.html
new file mode 100644
index 0000000..cc255bc
--- /dev/null
+++ b/html/Asset/Search/Edit.html
@@ -0,0 +1,40 @@
+<& /Elements/Header, Title => $title&>
+<& /Elements/Tabs &>
+
+<& Elements/NewListActions, actions => \@actions &>
+
+<form method="post" action="Build.html" id="BuildQueryAdvanced" name="BuildQueryAdvanced">
+<input type="hidden" class="hidden" name="SavedSearchId" value="<% $SavedSearchId %>" />
+<&|/Widgets/TitleBox, title => loc('Query'), &>
+<textarea name="Query" rows="8" cols="72"><% $Query %></textarea>
+</&>
+<&|/Widgets/TitleBox, title => loc('Format'), &>
+<textarea name="Format" rows="8" cols="72"><% $Format %></textarea>
+</&>
+<& /Elements/Submit, Label => loc("Apply"), Reset => 1, Caption => loc("Apply your changes")&>
+</form>
+
+<%INIT>
+my $title = loc("Edit Query");
+$Format = $m->comp('/Elements/ScrubHTML', Content => $Format);
+my $QueryString = $m->comp('/Elements/QueryString',
+                           Query   => $Query,
+                           Format  => $Format,
+                           RowsPerPage    => $Rows,
+                           OrderBy => $OrderBy,
+                           Order   => $Order,
+                          );
+
+</%INIT>
+
+
+<%ARGS>
+$SavedSearchId => 'new'
+$Query         => ''
+$Format        => ''
+$Rows          => '50'
+$OrderBy       => 'id'
+$Order         => 'ASC'
+
+ at actions       => ()
+</%ARGS>
diff --git a/html/Asset/Search/Elements/BuildFormatString b/html/Asset/Search/Elements/BuildFormatString
new file mode 100644
index 0000000..54a3191
--- /dev/null
+++ b/html/Asset/Search/Elements/BuildFormatString
@@ -0,0 +1,163 @@
+<%ARGS>
+$Format => RT->Config->Get('AssetSearchFormat')
+
+%catalogs => ()
+
+$Face => undef
+$Size => undef
+$Link => undef
+$Title => undef
+
+$AddCol => undef
+$RemoveCol => undef
+$ColUp => undef
+$ColDown => undef
+
+$SelectDisplayColumns => undef
+$CurrentDisplayColumns => undef
+</%ARGS>
+<%init>
+# This can't be in a <once> block, because otherwise we return the
+# same \@fields every request, and keep tacking more CustomFields onto
+# it -- and it grows per request.
+
+# All the things we can display in the format string by default
+my @fields = qw(
+    id Name Description Status
+    CreatedBy LastUpdatedBy
+
+    Created     CreatedRelative
+    LastUpdated LastUpdatedRelative
+
+    RefersTo    ReferredToBy
+    DependsOn   DependedOnBy
+    MemberOf    Members
+    Parents     Children
+
+    Owner HeldBy Contacts
+
+    NEWLINE
+    NBSP
+); # loc_qw
+
+my $CustomFields = RT::CustomFields->new( $session{'CurrentUser'});
+foreach my $id (keys %catalogs) {
+    # Gotta load up the $catalog object, since catalogs get stored by name now.
+    my $catalog = RT::Catalog->new($session{'CurrentUser'});
+    $catalog->Load($id);
+    next unless $catalog->Id;
+    $CustomFields->LimitToCatalog($catalog->Id);
+    $CustomFields->SetContextObject( $catalog ) if keys %catalogs == 1;
+}
+$CustomFields->LimitToCatalog(0);
+
+while ( my $CustomField = $CustomFields->Next ) {
+    push @fields, "CustomField.{" . $CustomField->Name . "}";
+}
+
+$m->callback( Fields => \@fields, ARGSRef => \%ARGS );
+
+my ( @seen);
+
+$Format ||= RT->Config->Get('AssetSearchFormat');
+my @format = $m->comp('/Elements/CollectionAsTable/ParseFormat', Format => $Format);
+foreach my $field (@format) {
+    # "title" is for columns like NEWLINE, which doesn't have "attribute"
+    $field->{Column} = $field->{attribute} || $field->{title} || '<blank>';
+    push @seen, $field;
+}
+
+if ( $RemoveCol ) {
+    # we do this regex match to avoid a non-numeric warning
+    my ($index) = ($CurrentDisplayColumns // '') =~ /^(\d+)/;
+    if ( defined($index) ) {
+        delete $seen[$index];
+        my @temp = @seen;
+        @seen = ();
+        foreach my $element (@temp) {
+            next unless $element;
+            push @seen, $element;
+        }
+    }
+}
+elsif ( $AddCol ) {
+    if ( defined $SelectDisplayColumns ) {
+        my $selected = $SelectDisplayColumns;
+        my @columns;
+        if (ref($selected) eq 'ARRAY') {
+            @columns = @$selected;
+        } else {
+            push @columns, $selected;
+        }
+        foreach my $col (@columns) {
+            my %column = ();
+            $column{Column} = $col;
+
+            if ( $Face eq "Bold" ) {
+                $column{Prefix} .= "<b>";
+                $column{Suffix} .= "</b>";
+            }
+            if ( $Face eq "Italic" ) {
+                $column{Prefix} .= "<i>";
+                $column{Suffix} .= "</i>";
+            }
+            if ($Size) {
+                $column{Prefix} .= "<" . $m->interp->apply_escapes( $Size,  'h' ) . ">";
+                $column{Suffix} .= "</" . $m->interp->apply_escapes( $Size, 'h' ) . ">";
+            }
+            if ( $Link eq "Display" ) {
+                $column{Prefix} .= q{<a HREF="__WebPath__/Asset/Display.html?id=__id__">};
+                $column{Suffix} .= "</a>";
+            }
+
+            if ($Title) {
+                $column{Suffix} .= "/TITLE:" . $m->interp->apply_escapes( $Title, 'h' );
+            }
+            push @seen, \%column;
+        }
+    }
+}
+elsif ( $ColUp ) {
+    my ($index) = ($CurrentDisplayColumns // '') =~ /^(\d+)/;
+    if ( defined $index && ( $index - 1 ) >= 0 ) {
+        my $column = $seen[$index];
+        $seen[$index]       = $seen[ $index - 1 ];
+        $seen[ $index - 1 ] = $column;
+        $CurrentDisplayColumns     = $index - 1;
+    }
+}
+elsif ( $ColDown ) {
+    my ($index) = ($CurrentDisplayColumns // '') =~ /^(\d+)/;
+    if ( defined $index && ( $index + 1 ) < scalar @seen ) {
+        my $column = $seen[$index];
+        $seen[$index]       = $seen[ $index + 1 ];
+        $seen[ $index + 1 ] = $column;
+        $CurrentDisplayColumns     = $index + 1;
+    }
+}
+
+
+my @format_string;
+foreach my $field (@seen) {
+    next unless $field;
+    my $row = "";
+    if ( $field->{'original_string'} ) {
+        $row = $field->{'original_string'};
+    }
+    else {
+        $row .= $field->{'Prefix'} if defined $field->{'Prefix'};
+        $row .= "__$field->{'Column'}__"
+          unless ( $field->{'Column'} eq "<blank>" );
+        $row .= $field->{'Suffix'} if defined $field->{'Suffix'};
+        $row =~ s!([\\'])!\\$1!g;
+        $row = "'$row'";
+    }
+    push( @format_string, $row );
+}
+
+$Format = join(",\n", @format_string);
+
+
+return($Format, \@fields, \@seen);
+
+</%init>
diff --git a/html/Asset/Search/Elements/ConditionRow b/html/Asset/Search/Elements/ConditionRow
new file mode 100644
index 0000000..8dd6328
--- /dev/null
+++ b/html/Asset/Search/Elements/ConditionRow
@@ -0,0 +1,52 @@
+<tr>
+<td class="label"><% $handle_block->( $Condition->{'Field'}, $Condition->{'Name'} .'Field' ) |n %></td>
+<td class="operator"><% $handle_block->( $Condition->{'Op'}, $Condition->{'Name'} .'Op') |n %></td>
+<td class="value"><% $handle_block->( $Condition->{'Value'}, 'ValueOf'. $Condition->{'Name'} ) |n %></td>
+</tr>
+<%INIT>
+return unless $Condition && $Condition->{'Name'};
+
+$m->callback( Condition => \$Condition );
+return unless $Condition;
+
+my $handle_block;
+$handle_block = sub {
+    my $box = shift;
+    return $box unless ref $box;
+
+    my $name = shift;
+    if ( UNIVERSAL::isa($box, 'ARRAY') ) {
+        my $res = '';
+        $res .= $handle_block->( $_, $name ) foreach @$box;
+        return $res;
+    }
+
+    return undef unless UNIVERSAL::isa($box, 'HASH');
+    if ( $box->{'Type'} eq 'component' ) {
+        $box->{'Arguments'} ||= {},
+        return $m->scomp( $box->{'Path'}, %{ $box->{'Arguments'} }, Name => $name );
+    }
+    if ( $box->{'Type'} eq 'text' ) {
+        $box->{id} ||= $box->{name} ||= $name;
+        $box->{value} ||= delete($box->{Default}) || '';
+        return "<input ".join(" ", map{$m->interp->apply_escapes(lc($_),'h')
+                                      .q{="}.$m->interp->apply_escapes($box->{$_},'h').q{"}}
+                                   sort keys %$box)." />";
+    }
+    if ( $box->{'Type'} eq 'select' ) {
+        my $res = '';
+        $res .= qq{<select id="$name" name="$name">};
+        my @options = @{ $box->{'Options'} };
+        while( my $k = shift @options ) {
+            my $v = shift @options;
+            $res .= qq{<option value="$k">$v</option>};
+        }
+        $res .= qq{</select>};
+        return $res;
+    }
+};
+
+</%INIT>
+<%ARGS>
+$Condition => {}
+</%ARGS>
diff --git a/html/Asset/Search/Elements/DisplayOptions b/html/Asset/Search/Elements/DisplayOptions
new file mode 100644
index 0000000..6847aeb
--- /dev/null
+++ b/html/Asset/Search/Elements/DisplayOptions
@@ -0,0 +1,6 @@
+<&| /Widgets/TitleBox, title => loc("Sorting"), id => 'sorting' &>
+<& EditSort, %ARGS &>
+</&>
+<&| /Widgets/TitleBox, title => loc("Display Columns"), id => 'columns' &>
+<& EditFormat, %ARGS &>
+</&>
diff --git a/html/Asset/Search/Elements/EditFormat b/html/Asset/Search/Elements/EditFormat
new file mode 100644
index 0000000..bd649ac
--- /dev/null
+++ b/html/Asset/Search/Elements/EditFormat
@@ -0,0 +1,84 @@
+<table class="edit-columns">
+
+<tr>
+<th><&|/l&>Add Columns</&>:</th>
+<th><&|/l&>Format</&>:</th>
+<th></th>
+<th><&|/l&>Show Columns</&>:</th>
+</tr>
+
+<tr>
+
+<td valign="top"><select size="15" name="SelectDisplayColumns" multiple="multiple">
+% my %seen;
+% foreach my $field ( grep !$seen{lc $_}++, @$AvailableColumns) {
+<option value="<% $field %>" <% $selected{$field} ? 'selected="selected"' : '' |n%>>\
+<% $field =~ /^(?:CustomField|CF)\./ ? $field : loc($field) %></option>
+% }
+</select></td>
+<td>
+<div class="row">
+<span class="label"><&|/l&>Link</&>:</span>
+<span class="value">
+<select name="Link">
+<option value="None">-</option>
+<option value="Display"><&|/l&>Display</&></option>
+</select>
+</span>
+</div>
+<div class="row">
+<span class="label"><&|/l&>Title</&>:</span>
+<span class="value"><input name="Title" size="10" /></span>
+</div>
+<div class="row">
+<span class="label"><&|/l&>Size</&>:</span>
+<span class="value"><select name="Size">
+<option value="">-</option>
+<option value="Small"><&|/l&>Small</&></option>
+<option value="Large"><&|/l&>Large</&></option>
+</select>
+</span>
+</div>
+<div class="row">
+<span class="label"><&|/l&>Style</&>:</span>
+<span class="value"><select name="Face">
+<option value="">-</option>
+<option value="Bold"><&|/l&>Bold</&></option>
+<option value="Italic"><&|/l&>Italic</&></option>
+</select>
+</span>
+</div>
+</td>
+
+<td><input type="submit" class="button" name="AddCol" value=" → " /></td>
+
+<td valign="top">
+<select size="15" name="CurrentDisplayColumns">
+% my $i=0;
+% my $current = $ARGS{CurrentDisplayColumns} || ''; $current =~ s/^\d+>//;
+% foreach my $field ( @$CurrentFormat ) {
+<option value="<% $i++ %>><% $field->{Column} %>" <% $field->{Column} eq $current ? 'selected="selected"' : '' |n%>>\
+<% $field->{Column} =~ /^(?:CustomField|CF)\./ ? $field->{Column} : loc( $field->{Column} ) %></option>
+% }
+</select>
+<br />
+<center>
+<input type="submit" class="button" name="ColUp" value=" ↑ " />
+<input type="submit" class="button" name="ColDown" value=" ↓ " />
+<input type="submit" class="button" name="RemoveCol" value="<%loc('Delete')%>" />
+</center>
+</td>
+
+</tr>
+</table>
+
+<%init>
+my $selected = $ARGS{SelectDisplayColumns};
+$selected = [ $selected ] unless ref $selected;
+my %selected;
+$selected{$_}++ for grep {defined} @{ $selected };
+</%init>
+<%ARGS>
+$CurrentFormat => undef
+$AvailableColumns => undef
+</%ARGS>
diff --git a/html/Asset/Search/Elements/EditQuery b/html/Asset/Search/Elements/EditQuery
new file mode 100644
index 0000000..d019cc3
--- /dev/null
+++ b/html/Asset/Search/Elements/EditQuery
@@ -0,0 +1,196 @@
+<& NewListActions, actions => $actions &>
+<&|/Widgets/TitleBox, title => join(': ', grep defined, loc("Current search"), $Description) &>
+
+<select size="10" name="clauses" style="width: 100%" multiple="multiple">
+% $m->out($optionlist);
+</select>
+
+<p align="center">
+<input type="submit" class="button" name="Up" value=" ↑ " />
+<input type="submit" class="button" name="Down" value=" ↓ " />
+<input type="submit" class="button" name="Left" value=" ← " />
+<input type="submit" class="button" name="Right" value=" → " />
+<input type="submit" class="button" name="Toggle" value="<&|/l&>And/Or</&>" />
+<input type="submit" class="button" name="DeleteClause" value="<&|/l&>Delete</&>" />
+%#<input type="submit" class="button" name="EditQuery" value="Advanced" />
+</p>
+
+</&>
+<%ARGS>
+$Description => undef
+$optionlist => ''
+$actions => []
+</%ARGS>
+
+<%METHOD Process>
+<%ARGS>
+$Tree
+$Selected
+ at New       => ()
+</%ARGS>
+<%INIT>
+
+my @NewSelection = ();
+
+my @results;
+if ( $ARGS{'Up'} || $ARGS{'Down'} ) {
+    if (@$Selected) {
+        foreach my $value (@$Selected) {
+            my $parent = $value->getParent;
+            my $index = $value->getIndex;
+            my $newindex = $index;
+            $newindex++ if $ARGS{'Down'};
+            $newindex-- if $ARGS{'Up'};
+            if ( $newindex < 0 || $newindex >= $parent->getChildCount ) {
+                push( @results, [ loc("error: can't move up"), -1 ] ) if $ARGS{'Up'};
+                push( @results, [ loc("error: can't move down"), -1 ] ) if $ARGS{'Down'};
+                next;
+            }
+
+            $parent->removeChild( $index );
+            $parent->insertChild( $newindex, $value );
+        }
+    }
+    else {
+        push( @results, [ loc("error: nothing to move"), -1 ] );
+    }
+}
+elsif ( $ARGS{"Left"} ) {
+    if (@$Selected) {
+        foreach my $value (@$Selected) {
+            my $parent = $value->getParent;
+            if( $value->isRoot || $parent->isRoot ) {
+                push( @results, [ loc("error: can't move left"), -1 ] );
+                next;
+            }
+
+            my $grandparent = $parent->getParent;
+            if( $grandparent->isRoot ) {
+                push( @results, [ loc("error: can't move left"), -1 ] );
+                next;
+            }
+            
+            my $index = $parent->getIndex;
+            $parent->removeChild($value);
+            $grandparent->insertChild( $index, $value );
+            if ( $parent->isLeaf ) {
+                $grandparent->removeChild($parent);
+            }
+        }
+    }
+    else {
+        push( @results, [ loc("error: nothing to move"), -1 ] );
+    }
+}
+elsif ( $ARGS{"Right"} ) {
+    if (@$Selected) {
+        foreach my $value (@$Selected) {
+            my $parent = $value->getParent;
+            my $index  = $value->getIndex;
+
+            my $newparent;
+            if ( $index > 0 ) {
+                my $sibling = $parent->getChild( $index - 1 );
+                $newparent = $sibling unless $sibling->isLeaf;
+            }
+            $newparent ||= RT::Interface::Web::QueryBuilder::Tree->new( $ARGS{'AndOr'} || 'AND', $parent );
+
+            $parent->removeChild($value);
+            $newparent->addChild($value);
+        }
+    }
+    else {
+        push( @results, [ loc("error: nothing to move"), -1 ] );
+    }
+}
+elsif ( $ARGS{"DeleteClause"} ) {
+    if (@$Selected) {
+        my (@top);
+        my %Selected = map { $_ => 1 } @$Selected;
+        foreach my $node ( @$Selected ) {
+            my $tmp = $node->getParent;
+            while ( !$Selected{ $tmp } && !$tmp->isRoot ) {
+                $tmp = $tmp->getParent;
+            }
+            next if $Selected{ $tmp };
+            push @top, $node;
+        }
+
+        my %seen;
+        my @non_siblings_top = grep !$seen{ $_->getParent }++, @top;
+
+        foreach ( @New ) {
+            my $add = $_->clone;
+            foreach my $selected( @non_siblings_top ) {
+                my $newindex = $selected->getIndex + 1;
+                $selected->insertSibling( $newindex, $add );
+            }
+            $add->getParent->setNodeValue( $ARGS{'AndOr'} );
+            push @NewSelection, $add;
+        }
+        @New = ();
+    
+        while( my $node = shift @top ) {
+            my $parent = $node->getParent;
+            $parent->removeChild($node);
+            $node->DESTROY;
+        }
+        @$Selected = ();
+    }
+    else {
+        push( @results, [ loc("error: nothing to delete"), -1 ] );
+    }
+}
+elsif ( $ARGS{"Toggle"} ) {
+    if (@$Selected) {
+        my %seen;
+        my @unique_nodes = grep !$seen{ $_ + 0 }++,
+            map ref $_->getNodeValue? $_->getParent: $_,
+            @$Selected;
+
+        foreach my $node ( @unique_nodes ) {
+            if ( $node->getNodeValue eq 'AND' ) {
+                $node->setNodeValue('OR');
+            }
+            else {
+                $node->setNodeValue('AND');
+            }
+        }
+    }
+    else {
+        push( @results, [ loc("error: nothing to toggle"), -1 ] );
+    }
+}
+
+if ( @New && @$Selected ) {
+    my %seen;
+    my @non_siblings_selected = grep !$seen{ $_->getParent }++, @$Selected;
+
+    foreach ( @New ) {
+        my $add = $_->clone;
+        foreach my $selected( @non_siblings_selected ) {
+            my $newindex = $selected->getIndex + 1;
+            $selected->insertSibling( $newindex, $add );
+        }
+        $add->getParent->setNodeValue( $ARGS{'AndOr'} );
+        push @NewSelection, $add;
+    }
+    @$Selected = ();
+}
+elsif ( @New ) {
+    foreach ( @New ) {
+        my $add = $_->clone;
+        $Tree->addChild( $add );
+        push @NewSelection, $add;
+    }
+    $Tree->setNodeValue( $ARGS{'AndOr'} );
+}
+$_->DESTROY foreach @New;
+
+push @$Selected, @NewSelection;
+
+$Tree->PruneChildlessAggregators;
+
+return @results;
+</%INIT>
+</%METHOD>
diff --git a/html/Asset/Search/Elements/EditSearches b/html/Asset/Search/Elements/EditSearches
new file mode 100644
index 0000000..f1db3f3
--- /dev/null
+++ b/html/Asset/Search/Elements/EditSearches
@@ -0,0 +1,282 @@
+<div class="edit-saved-searches">
+<&| /Widgets/TitleBox, title => loc($Title)&>
+
+%# Hide all the save functionality if the user shouldn't see it.
+% if ( $can_modify ) {
+<span class="label"><&|/l&>Privacy</&>:</span>
+<& SelectSearchObject, Name => 'SavedSearchOwner', Objects => \@Objects, Object => ( $Object && $Object->id ) ? $Object->Object : '' &>
+<br />
+<span class="label"><&|/l&>Description</&>:</span>
+<input size="25" name="SavedSearchDescription" value="<% $Description || '' %>" />
+
+% if ($Id ne 'new') {
+<nobr>
+% if ( $Dirty ) {
+<input type="submit" class="button" name="SavedSearchRevert" value="<%loc('Revert')%>" />
+% }
+<input type="submit" class="button" name="SavedSearchDelete" value="<%loc('Delete')%>" />
+% if ( $AllowCopy ) {
+<input type="submit" class="button" name="SavedSearchCopy"   value="<%loc('Save as New')%>" />
+% }
+</nobr>
+% }
+% if ( $Object && $Object->Id ) {
+<input type="submit" class="button" id="SavedSearchSave" name="SavedSearchSave"   value="<%loc('Update')%>" />
+% } else {
+<input type="submit" class="button" id="SavedSearchSave" name="SavedSearchSave"   value="<%loc('Save')%>" />
+%}
+% }
+<br />
+<hr />
+<span class="label"><&|/l&>Load saved search</&>:</span>
+<& SelectSearchesForObjects, Name => 'SavedSearchLoad', Objects => \@Objects, SearchType => $Type &>
+<input type="submit" value="<% loc('Load') %>" id="SavedSearchLoadSubmit" name="SavedSearchLoadSubmit" class="button" />
+
+</&>
+</div>
+<%INIT>
+return unless $session{'CurrentUser'}->HasRight(
+    Right  => 'LoadSavedSearch',
+    Object => $RT::System,
+);
+
+my $can_modify = $session{'CurrentUser'}->HasRight(
+    Right  => 'CreateSavedSearch',
+    Object => $RT::System,
+);
+
+use RT::SavedSearch;
+my @Objects = RT::SavedSearch->new($session{CurrentUser})->_PrivacyObjects;
+push @Objects, RT::System->new( $session{'CurrentUser'} )
+    if $session{'CurrentUser'}->HasRight( Object=> $RT::System,
+                                          Right => 'SuperUser' );
+
+my $is_dirty = sub {
+    my %arg = (
+        Query       => {},
+        SavedSearch => {},
+        SearchFields => [qw(Query Format OrderBy Order RowsPerPage)],
+        @_
+    );
+
+    my $obj  = $arg{'SavedSearch'}->{'Object'};
+    return 0 unless $obj && $obj->id;
+
+    foreach( @{ $arg{'SearchFields'} } ) {
+        return 1 if $obj->SubValue( $_ ) ne $arg{'Query'}->{$_};
+    }
+
+    return 0;
+};
+
+# If we're modifying an old query, check if it's been changed
+my $Dirty = $is_dirty->(
+    Query       => $CurrentSearch,
+    SavedSearch => { Id => $Id, Object => $Object, Description => $Description },
+    SearchFields => \@SearchFields,
+);
+
+</%INIT>
+
+<%ARGS>
+$Id            => 'new'
+$Object        => undef
+$Type          => 'Asset'
+$Description   => ''
+$CurrentSearch => {}
+ at SearchFields   => ()
+$AllowCopy     => 1
+$Title         => loc('Saved searches')
+</%ARGS>
+
+<%METHOD Init>
+<%ARGS>
+$Query       => {}
+$SavedSearch => {}
+ at SearchFields => qw(Query Format OrderBy Order RowsPerPage)
+</%ARGS>
+<%INIT>
+
+$SavedSearch->{'Id'}          = $ARGS{'SavedSearchId'} || 'new';
+$SavedSearch->{'Type'}        = 'Asset';
+$SavedSearch->{'Description'} = $ARGS{'SavedSearchDescription'} || '';
+$SavedSearch->{'Privacy'}     = $ARGS{'SavedSearchOwner'}       || undef;
+
+my @results;
+
+if ( $ARGS{'SavedSearchRevert'} ) {
+    $ARGS{'SavedSearchLoad'} = $SavedSearch->{'Id'};
+}
+
+if ( $ARGS{'SavedSearchLoad'} ) {
+    my ($container, $id ) = _parse_saved_search ($ARGS{'SavedSearchLoad'});
+    if ( $container ) {
+        my $search = RT::Attribute->new( $session{'CurrentUser'} );
+        $search->Load( $id );
+        $SavedSearch->{'Id'}          = $ARGS{'SavedSearchLoad'};
+        $SavedSearch->{'Object'}      = $search;
+        $SavedSearch->{'Description'} = $search->Description;
+        $Query->{$_} = $search->SubValue($_) foreach @SearchFields;
+
+        if ( $ARGS{'SavedSearchRevert'} ) {
+            push @results, loc('Loaded original "[_1]" saved search', $SavedSearch->{'Description'} );
+        } else {
+            push @results, loc('Loaded saved search "[_1]"', $SavedSearch->{'Description'} );
+        }
+    }
+    else {
+        push @results, loc( 'Can not load saved search "[_1]"',
+                $ARGS{'SavedSearchLoad'} );
+        return @results;
+    }
+}
+elsif ( $ARGS{'SavedSearchDelete'} ) {
+    # We set $SearchId to 'new' above already, so peek into the %ARGS
+    my ($container, $id) = _parse_saved_search( $SavedSearch->{'Id'} );
+    if ( $container && $container->id ) {
+        # We have the object the entry is an attribute on; delete the entry...
+        my ($val, $msg) = $container->Attributes->DeleteEntry( Name => 'SavedSearch', id => $id );
+        unless ( $val ) {
+            push @results, $msg;
+            return @results;
+        }
+    }
+    $SavedSearch->{'Id'}          = 'new';
+    $SavedSearch->{'Object'}      = undef;
+    $SavedSearch->{'Description'} = undef;
+    push @results, loc("Deleted saved search");
+}
+elsif ( $ARGS{'SavedSearchCopy'} ) {
+    my ($container, $id ) = _parse_saved_search( $ARGS{'SavedSearchId'} );
+    $SavedSearch->{'Object'} = RT::Attribute->new( $session{'CurrentUser'} );
+    $SavedSearch->{'Object'}->Load( $id );
+    if ( $ARGS{'SavedSearchDescription'} && $ARGS{'SavedSearchDescription'} ne $SavedSearch->{'Object'}->Description ) {
+        $SavedSearch->{'Description'} = $ARGS{'SavedSearchDescription'};
+    } else {
+        $SavedSearch->{'Description'} = loc( "[_1] copy", $SavedSearch->{'Object'}->Description );
+    }
+    $SavedSearch->{'Id'}          = 'new';
+    $SavedSearch->{'Object'}      = undef;
+}
+
+if ( $SavedSearch->{'Id'} && $SavedSearch->{'Id'} ne 'new'
+     && !$SavedSearch->{'Object'} )
+{
+    my ($container, $id ) = _parse_saved_search( $ARGS{'SavedSearchId'} );
+    $SavedSearch->{'Object'} = RT::Attribute->new( $session{'CurrentUser'} );
+    $SavedSearch->{'Object'}->Load( $id );
+    $SavedSearch->{'Description'} ||= $SavedSearch->{'Object'}->Description;
+}
+
+return @results;
+
+</%INIT>
+</%METHOD>
+
+<%METHOD Save>
+<%ARGS>
+$Query        => {}
+$SavedSearch  => {}
+ at SearchFields => qw(Query Format OrderBy Order RowsPerPage)
+</%ARGS>
+<%INIT>
+
+return unless $ARGS{'SavedSearchSave'} || $ARGS{'SavedSearchCopy'};
+
+my @results;
+my $obj  = $SavedSearch->{'Object'};
+my $id   = $SavedSearch->{'Id'};
+my $desc = $SavedSearch->{'Description'};
+my $privacy = $SavedSearch->{'Privacy'};
+
+my %params = map { $_ => $Query->{$_} } @SearchFields;
+my ($new_obj_type, $new_obj_id) = split(/\-/, ($privacy || ''));
+
+if ( $obj && $obj->id ) {
+    # permission check
+    if ($obj->Object->isa('RT::System')) {
+        unless ($session{'CurrentUser'}->HasRight( Object=> $RT::System, Right => 'SuperUser')) {
+            push @results, loc("No permission to save system-wide searches");
+            return @results;
+        }
+    }
+
+    $obj->SetSubValues( %params );
+    $obj->SetDescription( $desc );
+
+    my $obj_type = ref($obj->Object);
+    # We need to get current obj_id now, because when we change obj_type to
+    # RT::System, $obj->Object->Id returns 1, not the old one :(
+    my $obj_id = $obj->Object->Id;
+
+    if ( $new_obj_type && $new_obj_id ) {
+        my ($val, $msg);
+
+        # we need to check right before we change any of ObjectType and ObjectId, 
+        # or it will fail the 2nd change if we use SetObjectType and
+        # SetObjectId sequentially
+
+        if ( $obj->CurrentUserHasRight('update') ) {
+            if ( $new_obj_type ne $obj_type ) {
+                ( $val, $msg ) = $obj->__Set(
+                    Field => 'ObjectType',
+                    Value => $new_obj_type,
+                );
+                push @results, loc( 'Unable to set privacy object: [_1]', $msg )
+                  unless ($val);
+            }
+            if ( $new_obj_id != $obj_id ) {
+                ( $val, $msg ) = $obj->__Set(
+                    Field => 'ObjectId',
+                    Value => $new_obj_id,
+                );
+                push @results, loc( 'Unable to set privacy id: [_1]', $msg )
+                  unless ($val);
+            }
+        }
+        else {
+            # two loc are just for convenience so we don't need to
+            # write an extra i18n translation item
+            push @results,
+              loc( 'Unable to set privacy object or id: [_1]',
+                loc('Permission Denied') )
+        }
+    } else {
+        push @results, loc('Unable to determine object type or id');
+    }
+    push @results, loc('Updated saved search "[_1]"', $desc);
+}
+elsif ( $id eq 'new' and defined $desc and length $desc ) {
+    my $saved_search = RT::SavedSearch->new( $session{'CurrentUser'} );
+    my ($status, $msg) = $saved_search->Save(
+        Privacy      => $privacy,
+        Name         => $desc,
+        Type         => $SavedSearch->{'Type'},
+        SearchParams => \%params,
+    );
+
+    if ( $status ) {
+        $SavedSearch->{'Object'} = RT::Attribute->new( $session{'CurrentUser'} );
+        $SavedSearch->{'Object'}->Load( $saved_search->Id );
+        # Build new SearchId
+        $SavedSearch->{'Id'} =
+                ref( $session{'CurrentUser'}->UserObj ) . '-'
+                    . $session{'CurrentUser'}->UserObj->Id
+                    . '-SavedSearch-'
+                    . $SavedSearch->{'Object'}->Id;
+    }
+    else {
+        push @results, loc("Can't find a saved search to work with").': '.loc($msg);
+    }
+}
+elsif ( $id eq 'new' ) {
+    push @results, loc("Can't save a search without a Description");
+}
+else {
+    push @results, loc("Can't save this search");
+}
+
+return @results;
+
+</%INIT>
+</%METHOD>
diff --git a/html/Asset/Search/Elements/EditSort b/html/Asset/Search/Elements/EditSort
new file mode 100644
index 0000000..ec60707
--- /dev/null
+++ b/html/Asset/Search/Elements/EditSort
@@ -0,0 +1,93 @@
+<table valign="top">
+
+% for my $o (0..3) {
+% $Order[$o] ||= ''; $OrderBy[$o] ||= '';
+<tr>
+<td class="label">
+% if ($o == 0) {
+<&|/l&>Order by</&>:
+% }
+</td>
+<td class="value">
+<select name="OrderBy">
+% if ($o > 0) {
+<option value=""><&|/l&>~[none~]</&></option>
+% }
+% # %fields maps display name to SQL column/function
+% foreach my $field (sort keys %fields) {
+%    next unless $field;
+%    my $fieldval = $fields{$field};
+<option value="<%$fieldval%>"
+% if (defined $OrderBy[$o] and $fieldval eq $OrderBy[$o]) {
+selected="selected"
+% }
+><% $field =~ /^(?:CustomField|CF)\./ ? $field : loc($field) %></option>
+% }
+</select>
+<select name="Order">
+<option value="ASC"
+% unless ( ($Order[$o]||'') eq "DESC" ) {
+selected="selected"
+% }
+><&|/l&>Asc</&></option>
+<option value="DESC"
+% if ( ($Order[$o]||'') eq "DESC" ) {
+selected="selected"
+% }
+><&|/l&>Desc</&></option>
+</select>
+</td>
+</tr>
+% }
+<tr>
+<td class="label">
+<&|/l&>Rows per page</&>:
+</td><td class="value">
+<& /Elements/SelectResultsPerPage, 
+    Name => "RowsPerPage", 
+    Default => $RowsPerPage &>
+</td>
+</tr>
+</table>
+
+<%INIT>
+my $assets = RT::Assets->new($session{'CurrentUser'});
+my %FieldDescriptions = %{$assets->FIELDS};
+my %fields;
+
+for my $field (keys %FieldDescriptions) {
+    next if $field eq 'EffectiveId';
+    next unless $FieldDescriptions{$field}->[0] =~ /^(?:ENUM|INT|DATE|STRING|ID)$/;
+    $fields{$field} = $field;
+}
+
+$fields{'Owner'} = 'Owner';
+$fields{'HeldBy'} = 'HeldBy';
+$fields{'Contact'} = 'Contact';
+
+# Add all available CustomFields to the list of sortable columns.
+my @cfs = grep /^CustomField/, @{$ARGS{AvailableColumns}};
+$fields{$_} = $_ for @cfs;
+
+# Add PAW sort
+$fields{'Custom.Ownership'} = 'Custom.Ownership';
+
+$m->callback(CallbackName => 'MassageSortFields', Fields => \%fields );
+
+my @Order = split /\|/, $Order;
+my @OrderBy = split /\|/, $OrderBy;
+if ($Order =~ /\|/) {
+    @Order = split /\|/, $Order;
+} else {
+    @Order = ( $Order );
+}
+
+</%INIT>
+
+<%ARGS>
+$Order => ''
+$OrderBy => ''
+$RowsPerPage => undef
+$Format => undef
+$GroupBy => 'id'
+</%ARGS>
diff --git a/html/Asset/Search/Elements/NewListActions b/html/Asset/Search/Elements/NewListActions
new file mode 100644
index 0000000..4424405
--- /dev/null
+++ b/html/Asset/Search/Elements/NewListActions
@@ -0,0 +1,19 @@
+<b><%loc('Results')%></b><br />
+% foreach my $action (@actions) {
+% my @item = @$action;
+% if ($item[1] < 0) {
+<font color="red"> 
+% }
+ <%$item[0]%><br />
+% if ($item[1] < 0) {
+</font>
+% }
+% }
+<br />
+<%init>
+ at actions = map ref $_? $_: [$_, 0], grep defined && length, @actions;
+return unless @actions;
+</%init>
+<%ARGS>
+ at actions => undef
+</%ARGS>
diff --git a/html/Asset/Search/Elements/PickAssetCFs b/html/Asset/Search/Elements/PickAssetCFs
new file mode 100644
index 0000000..2f85464
--- /dev/null
+++ b/html/Asset/Search/Elements/PickAssetCFs
@@ -0,0 +1,16 @@
+<%ARGS>
+%catalogs => ()
+</%ARGS>
+<%init>
+my $CustomFields = RT::CustomFields->new( $session{'CurrentUser'});
+foreach my $id (keys %catalogs) {
+    my $catalog = RT::Catalog->new($session{'CurrentUser'});
+    $catalog->Load($id);
+    next unless $catalog->Id;
+    $CustomFields->LimitToCatalog($catalog->Id);
+    $CustomFields->SetContextObject( $catalog ) if keys %catalogs == 1;
+}
+$CustomFields->LimitToCatalog(0);
+$CustomFields->OrderBy( FIELD => 'Name', ORDER => 'ASC' );
+</%init>
+<& /Search/Elements/PickCFs, %ARGS, CustomFields => $CustomFields &>
diff --git a/html/Asset/Search/Elements/PickBasics b/html/Asset/Search/Elements/PickBasics
new file mode 100644
index 0000000..6d6b242
--- /dev/null
+++ b/html/Asset/Search/Elements/PickBasics
@@ -0,0 +1,121 @@
+% foreach( @lines ) {
+<& ConditionRow, Condition => $_ &>
+% }
+<%INIT>
+
+my @lines = (
+    {
+        Name => 'id',
+        Field => loc('id'),
+        Op => {
+            Type => 'component',
+            Path => '/Elements/SelectEqualityOperator',
+        },
+        Value => { Type => 'text', Size => 5 }
+    },
+    {
+        Name => 'Attachment',
+        Field => {
+            Type => 'component',
+            Path => '/Asset/Elements/SelectAttachmentField',
+        },
+        Op => {
+            Type => 'component',
+            Path => '/Elements/SelectBoolean',
+            Arguments => {
+                True => loc("matches"),
+                False => loc("doesn't match"),
+                TrueVal => 'LIKE',
+                FalseVal => 'NOT LIKE',
+            },
+        },
+        Value => { Type => 'text', Size => 20 },
+    },
+    {
+        Name => 'Catalog',
+        Field => loc('Catalog'),
+        Op => {
+            Type => 'component',
+            Path => '/Elements/SelectBoolean',
+            Arguments => { TrueVal=> '=', FalseVal => '!=' },
+        },
+        Value => {
+            Type => 'component',
+            Path => '/Asset/Elements/SelectCatalog',
+            Arguments => { NamedValues => 1, ShowNullOption => 1, UpdateSession => 0, CheckRight => 'ShowAsset' },
+        },
+    },
+    {
+        Name => 'Status',
+        Field => loc('Status'),
+        Op => {
+            Type => 'component',
+            Path => '/Elements/SelectBoolean',
+            Arguments => { TrueVal=> '=', FalseVal => '!=' },
+        },
+        Value => {
+            Type => 'component',
+            Path => '/Asset/Elements/SelectStatus',
+            Arguments => { Catalogs => \%catalogs },
+        },
+    },
+    {
+        Name => 'Watcher',
+        Field => {
+            Type => 'component',
+            Path => 'SelectPersonType',
+            Arguments => { Default => 'Owner' },
+        },
+        Op => {
+            Type => 'component',
+            Path => '/Elements/SelectMatch',
+        },
+        Value => { Type => 'text', Size => 20 }
+    },
+    {
+        Name => 'WatcherGroup',
+        Field => {
+            Type => 'component',
+            Path => 'SelectPersonType',
+            Arguments => { Default => 'Owner', Suffix => 'Group' },
+        },
+        Op => {
+            Type => 'select',
+            Options => [ '=' => loc('is') ],
+        },
+        Value => { Type => 'text', Size => 20, "data-autocomplete" => "Groups" }
+    },
+    {
+        Name => 'Date',
+        Field => {
+            Type => 'component',
+            Path => '/Asset/Elements/SelectDateType',
+        },
+        Op => {
+            Type => 'component',
+            Path => '/Elements/SelectDateRelation',
+        },
+        Value => {
+            Type => 'component',
+            Path => '/Elements/SelectDate',
+            Arguments => { ShowTime => 0, Default => '' },
+        },
+    },
+    {
+        Name => 'Links',
+        Field => { Type => 'component', Path => 'SelectLinks' },
+        Op => {
+            Type => 'component',
+            Path => '/Elements/SelectBoolean',
+            Arguments => { TrueVal=> '=', FalseVal => '!=' },
+        },
+        Value => { Type => 'text', Size => 5 }
+    },
+);
+
+$m->callback( Conditions => \@lines );
+
+</%INIT>
+<%ARGS>
+%catalogs => ()
+</%ARGS>
diff --git a/html/Asset/Search/Elements/PickCriteria b/html/Asset/Search/Elements/PickCriteria
new file mode 100644
index 0000000..94920d9
--- /dev/null
+++ b/html/Asset/Search/Elements/PickCriteria
@@ -0,0 +1,26 @@
+<&| /Widgets/TitleBox, title => loc('Add Criteria')&>
+
+<table width="100%" cellspacing="0" cellpadding="0" border="0">
+
+
+% $m->callback( %ARGS, CallbackName => "BeforeBasics" );
+<& PickBasics, catalogs => \%catalogs &>
+<& PickAssetCFs, catalogs => \%catalogs &>
+% $m->callback( %ARGS, CallbackName => "AfterCFs" );
+
+<tr class="separator"><td colspan="3"><hr /></td></tr>
+<tr>
+<td class="label"><&|/l&>Aggregator</&></td>
+<td class="operator" colspan="2"><& SelectAndOr, Name => "AndOr" &></td>
+
+</tr>
+
+</table>
+
+</&>
+
+<%ARGS>
+$addquery => 0
+$query => undef
+%catalogs => ()
+</%ARGS>
diff --git a/html/Asset/Search/Elements/SearchPrivacy b/html/Asset/Search/Elements/SearchPrivacy
new file mode 100644
index 0000000..2008cdd
--- /dev/null
+++ b/html/Asset/Search/Elements/SearchPrivacy
@@ -0,0 +1,14 @@
+<%args>
+$Object => undef
+</%args>
+<%init>
+my $label;
+if (ref($Object) eq 'RT::User') {
+    $label = $Object->id == $session{'CurrentUser'}->Id
+                ? loc("My saved searches")
+                : loc("[_1]'s saved searches", $Object->Format);
+} else {
+    $label = loc("[_1]'s saved searches", $Object->Name);
+}
+</%init>
+<% $label %>\
diff --git a/html/Asset/Search/Elements/SelectAndOr b/html/Asset/Search/Elements/SelectAndOr
new file mode 100644
index 0000000..5002360
--- /dev/null
+++ b/html/Asset/Search/Elements/SelectAndOr
@@ -0,0 +1,6 @@
+<label><input type="radio" class="radio" name="<%$Name%>" checked="checked" value="AND" /><&|/l&>AND</&></label>
+<label><input type="radio" class="radio" name="<%$Name%>" value="OR" /><&|/l&>OR</&></label>
+
+<%ARGS>
+$Name => "Operator"
+</%ARGS>
diff --git a/html/Asset/Search/Elements/SelectLinks b/html/Asset/Search/Elements/SelectLinks
new file mode 100644
index 0000000..4b76f21
--- /dev/null
+++ b/html/Asset/Search/Elements/SelectLinks
@@ -0,0 +1,20 @@
+<select name="<%$Name%>">
+% foreach (@fields) {
+<option value="<%$_->[0]%>"><% $_->[1] %></option>
+% }
+</select>
+<%ARGS>
+$Name => 'LinksField'
+</%ARGS>
+
+<%INIT>
+my @fields = (
+    [ HasMember    => loc("Child") ],
+    [ MemberOf     => loc("Parent") ],
+    [ DependsOn    => loc("Depends on") ],
+    [ DependedOnBy => loc("Depended on by") ],
+    [ RefersTo     => loc("Refers to") ],
+    [ ReferredToBy => loc("Referred to by") ],
+    [ Linked       => loc("Links to") ],
+);
+</%INIT>
diff --git a/html/Asset/Search/Elements/SelectPersonType b/html/Asset/Search/Elements/SelectPersonType
new file mode 100644
index 0000000..985cd0e
--- /dev/null
+++ b/html/Asset/Search/Elements/SelectPersonType
@@ -0,0 +1,27 @@
+<select id="<%$Name%>" name="<%$Name%>">
+% if ($AllowNull) {
+<option value="">-</option>
+% }
+% for my $option (@types) {
+%  if ($Suffix) {
+<option value="<% $option %><% $Suffix %>"<%$option eq $Default && qq[ selected="selected"] |n %> ><% loc($option) %> <% loc('Group') %></option>
+%   next;
+%  }
+%  foreach my $subtype (@subtypes) {
+<option value="<%"$option.$subtype"%>"<%$option eq $Default && $subtype eq 'EmailAddress' && qq[ selected="selected"] |n %> ><% loc($option) %> <% loc($subtype) %></option>
+%  }
+% }
+</select>
+
+<%INIT>
+my @types = qw(Owner HeldBy Contact);
+my @subtypes = @{ $RT::Assets::SEARCHABLE_SUBFIELDS{'User'} };
+
+</%INIT>
+<%ARGS>
+$AllowNull => 1
+$Suffix => ''
+$Default =>undef
+$Scope => 'asset'
+$Name => 'WatcherType'
+</%ARGS>
diff --git a/html/Asset/Search/Elements/SelectSearchObject b/html/Asset/Search/Elements/SelectSearchObject
new file mode 100644
index 0000000..fd1d75e
--- /dev/null
+++ b/html/Asset/Search/Elements/SelectSearchObject
@@ -0,0 +1,17 @@
+<%args>
+ at Objects => undef
+$Name => undef
+$Object => undef
+</%args>
+<%init>
+my $default_privacy = '';
+if ( $Object && $Object->Id ) {
+    $default_privacy = ref($Object).'-'.$Object->Id;
+}
+</%init>
+<select id="<%$Name%>" name="<%$Name%>">
+% foreach my $object (@Objects) {
+% my $privacy = ref($object).'-'.$object->id;
+<option value="<%$privacy%>" <% ( $privacy eq $default_privacy ) ? "selected='selected'" : '' |n %>><& SearchPrivacy, Object => $object &></option>
+% }
+</select>
diff --git a/html/Asset/Search/Elements/SelectSearchesForObjects b/html/Asset/Search/Elements/SelectSearchesForObjects
new file mode 100644
index 0000000..d2f3068
--- /dev/null
+++ b/html/Asset/Search/Elements/SelectSearchesForObjects
@@ -0,0 +1,21 @@
+<%args>
+ at Objects => undef
+$Name => undef
+$SearchType => 'Asset',
+</%args>
+<select id="<%$Name%>" name="<%$Name%>">
+<option value="">-</option>
+% foreach my $object (@Objects) {
+% my @searches = $object->Attributes->Named('SavedSearch');
+% if ( @searches ) {
+<optgroup label="<& SearchPrivacy, Object => $object &>">
+% foreach my $search (@searches) { 
+%     # Skip it if it is not of search type we want.
+%     next if ($search->SubValue('SearchType')
+%              && $search->SubValue('SearchType') ne $SearchType);
+<option value="<%ref($object)%>-<%$object->id%>-SavedSearch-<%$search->Id%>"><%$search->Description||loc('Unnamed search')%></option>
+% }
+</optgroup>
+% }
+% }
+</select>
diff --git a/html/Asset/Search/Results.html b/html/Asset/Search/Results.html
new file mode 100644
index 0000000..8c4f5ba
--- /dev/null
+++ b/html/Asset/Search/Results.html
@@ -0,0 +1,165 @@
+<& /Elements/Header, Title => $title,
+    Refresh => $refresh,
+    LinkRel => \%link_rel &>
+<& /Elements/Tabs &>
+
+% my $DisplayFormat;
+% $m->callback( ARGSRef => \%ARGS, Format => \$Format, DisplayFormat => \$DisplayFormat, CallbackName => 'BeforeResults' );
+
+% unless ($ok) {
+%    $msg =~ s{ at .*? line .*}{}s;
+<&| /Widgets/TitleBox, title => loc("Error"), class => "error-titlebox" &>
+<&|/l_unsafe, "<i>".$m->interp->apply_escapes($msg, "h")."</i>" &>There was an error parsing your search query: [_1].  Your RT admin can find more information in the error logs.</&>
+</&>
+% } else {
+% my $Collection = RT::Assets->new($session{CurrentUser});
+% $Collection->FromSQL($Query);
+<& /Elements/CollectionList, 
+    Collection => $Collection,
+    Query => $Query,
+    TotalFound => $assetcount,
+    AllowSorting => 1,
+    OrderBy => $OrderBy,
+    Order => $Order,
+    Rows => $Rows,
+    Page => $Page,
+    Format => $Format,
+    DisplayFormat => $DisplayFormat, # in case we set it in callbacks
+    Class => 'RT::Assets',
+    BaseURL => $BaseURL,
+    SavedSearchId => $ARGS{'SavedSearchId'},
+    SavedChartSearchId => $ARGS{'SavedChartSearchId'},
+    PassArguments => [qw(Query Format Rows Page Order OrderBy SavedSearchId SavedChartSearchId)],
+&>
+% }
+% $m->callback( ARGSRef => \%ARGS, CallbackName => 'AfterResults' );
+
+% my %hiddens = (Query => $Query, Format => $Format, Rows => $Rows, OrderBy => $OrderBy, Order => $Order, HideResults => $HideResults, Page => $Page );
+<div align="right" class="refresh">
+<form method="get" action="<%RT->Config->Get('WebPath')%>/Asset/Search/Results.html">
+% foreach my $key (keys(%hiddens)) {
+<input type="hidden" class="hidden" name="<%$key%>" value="<% defined($hiddens{$key})?$hiddens{$key}:'' %>" />
+% }
+<& /Elements/Refresh, Name => 'AssetsRefreshInterval', Default => $session{'assets_refresh_interval'}||RT->Config->Get('SearchResultsRefreshInterval', $session{'CurrentUser'}) &>
+<input type="submit" class="button" value="<&|/l&>Change</&>" />
+</form>
+</div>
+<%INIT>
+$m->callback( ARGSRef => \%ARGS, CallbackName => 'Initial' );
+
+# These variables are what define a search_hash; this is also
+# where we give sane defaults.
+$Format ||= RT->Config->Get('AssetSearchFormat');
+
+# Some forms pass in "RowsPerPage" rather than "Rows"
+# We call it RowsPerPage everywhere else.
+
+if ( !defined($Rows) ) {
+    if (defined $ARGS{'RowsPerPage'} ) {
+        $Rows = $ARGS{'RowsPerPage'};
+    } else {
+        $Rows = 50;
+    }
+}
+$Page = 1 unless $Page && $Page > 0;
+
+$session{'i'}++;
+$session{'assets'} = RT::Assets->new($session{'CurrentUser'}) ;
+my ($ok, $msg) = $Query ? $session{'assets'}->FromSQL($Query) : (1, "Vacuously OK");
+# Provide an empty search if parsing failed
+$session{'assets'}->FromSQL("id < 0") unless ($ok);
+
+if ($OrderBy =~ /\|/) {
+    # Multiple Sorts
+    my @OrderBy = split /\|/,$OrderBy;
+    my @Order = split /\|/,$Order;
+    $session{'assets'}->OrderByCols(
+        map { { FIELD => $OrderBy[$_], ORDER => $Order[$_] } } ( 0
+        .. $#OrderBy ) );; 
+} else {
+    $session{'assets'}->OrderBy(FIELD => $OrderBy, ORDER => $Order); 
+}
+$session{'assets'}->RowsPerPage( $Rows ) if $Rows;
+$session{'assets'}->GotoPage( $Page - 1 );
+
+$session{'CurrentAssetSearchHash'} = {
+    Format      => $Format,
+    Query       => $Query,
+    Page        => $Page,
+    Order       => $Order,
+    OrderBy     => $OrderBy,
+    RowsPerPage => $Rows
+};
+
+
+my ($title, $assetcount) = (loc("Find assets"), 0);
+if ( $session{'assets'}->Query()) {
+    $assetcount = $session{assets}->CountAll();
+    $title = loc('Found [quant,_1,asset,assets]', $assetcount);
+}
+
+my $QueryString = "?".$m->comp('/Elements/QueryString',
+                               Query => $Query,
+                               Format => $Format,
+                               Rows => $Rows,
+                               OrderBy => $OrderBy,
+                               Order => $Order,
+                               Page => $Page);
+my $ShortQueryString = "?".$m->comp('/Elements/QueryString', Query => $Query);
+
+if ($ARGS{'AssetsRefreshInterval'}) {
+    $session{'assets_refresh_interval'} = $ARGS{'AssetsRefreshInterval'};
+}
+
+my $refresh = $session{'assets_refresh_interval'}
+    || RT->Config->Get('SearchResultsRefreshInterval', $session{'CurrentUser'} );
+
+# Check $m->request_args, not $DECODED_ARGS, to avoid creating a new CSRF token on each refresh
+if (RT->Config->Get('RestrictReferrer') and $refresh and not $m->request_args->{CSRF_Token}) {
+    my $token = RT::Interface::Web::StoreRequestToken( $session{'CurrentAssetSearchHash'} );
+    $m->notes->{RefreshURL} = RT->Config->Get('WebURL')
+        . "Asset/Search/Results.html?CSRF_Token="
+            . $token;
+}
+
+my %link_rel;
+my $genpage = sub {
+    return $m->comp(
+        '/Elements/QueryString',
+        Query   => $Query,
+        Format  => $Format,
+        Rows    => $Rows,
+        OrderBy => $OrderBy,
+        Order   => $Order,
+        Page    => shift(@_),
+    );
+};
+
+if ( RT->Config->Get('SearchResultsAutoRedirect') && $assetcount == 1 &&
+    $session{assets}->First ) {
+# $assetcount is not always precise unless $UseSQLForACLChecks is set to true,
+# check $session{assets}->First here is to make sure the asset is there.
+    RT::Interface::Web::Redirect( RT->Config->Get('WebURL')
+            ."Asset/Display.html?id=". $session{assets}->First->id );
+}
+
+my $BaseURL = RT->Config->Get('WebPath')."/Asset/Search/Results.html?";
+$link_rel{first} = $BaseURL . $genpage->(1)         if $Page > 1;
+$link_rel{prev}  = $BaseURL . $genpage->($Page - 1) if $Page > 1;
+$link_rel{next}  = $BaseURL . $genpage->($Page + 1) if ($Page * $Rows) < $assetcount;
+$link_rel{last}  = $BaseURL . $genpage->(POSIX::ceil($assetcount/$Rows)) if $Rows and ($Page * $Rows) < $assetcount;
+</%INIT>
+<%CLEANUP>
+$session{'assets'}->PrepForSerialization();
+</%CLEANUP>
+<%ARGS>
+$Query => undef
+$Format => undef 
+$HideResults => 0
+$Rows => undef
+$Page => 1
+$OrderBy => undef
+$Order => undef
+$SavedSearchId => undef
+$SavedChartSearchId => undef
+</%ARGS>
diff --git a/html/Asset/Search/Results.tsv b/html/Asset/Search/Results.tsv
new file mode 100644
index 0000000..3f0eac2
--- /dev/null
+++ b/html/Asset/Search/Results.tsv
@@ -0,0 +1,50 @@
+<%ARGS>
+$Format => undef
+$Query => ''
+$OrderBy => 'id'
+$Order => 'ASC'
+$PreserveNewLines => 0
+</%ARGS>
+<%INIT>
+my $Assets = RT::Assets->new( $session{'CurrentUser'} );
+
+my $catalog_obj = LoadDefaultCatalog($ARGS{'Catalog'} || '');
+$ARGS{'Catalog'} = $catalog_obj->Id;
+
+if ( $ARGS{Query} ) {
+    $Assets->FromSQL( $Query );
+}
+else {
+    ProcessAssetsSearchArguments(
+        Assets => $Assets, Catalog => $catalog_obj, ARGSRef => \%ARGS,
+    );
+}
+if ( $OrderBy =~ /\|/ ) {
+    # Multiple Sorts
+    my @OrderBy = split /\|/, $OrderBy;
+    my @Order   = split /\|/, $Order;
+    $Assets->OrderByCols(
+        map { { FIELD => $OrderBy[$_], ORDER => $Order[$_] } }
+        ( 0 .. $#OrderBy )
+    );
+}
+else {
+    $Assets->OrderBy( FIELD => $OrderBy, ORDER => $Order );
+}
+
+if ( !$Format ) {
+    $Format = q|id, Name, Description, Status, Catalog, |;
+    $Format .= "$_, " for RT::Asset->Roles;
+    my $catalog_obj = LoadDefaultCatalog($ARGS{'Catalog'} || '');
+    my $CFs = RT::CustomFields->new( $session{CurrentUser} );
+    $CFs->LimitToCatalog( $catalog_obj->Id );
+    $CFs->LimitToObjectId( 0 ); # LimitToGlobal but no LookupType restriction
+    $Format .= "'__CF.{$_}__/TITLE:$_', " for map {$_ = $_->Name; s/['\\]/\\$1/g; $_} @{$CFs->ItemsArrayRef};
+    $m->callback(CallbackName => "ModifyFormat", Format => \$Format );
+}
+
+my $comp = "/Asset/Elements/TSVExport";
+$comp = "/Elements/TSVExport" if $m->comp_exists("/Elements/TSVExport");
+$m->comp( $comp, Collection => $Assets, Format => $Format );
+
+</%INIT>
diff --git a/html/Callbacks/AssetSQL/Elements/Tabs/Privileged b/html/Callbacks/AssetSQL/Elements/Tabs/Privileged
new file mode 100644
index 0000000..cd273bb
--- /dev/null
+++ b/html/Callbacks/AssetSQL/Elements/Tabs/Privileged
@@ -0,0 +1,121 @@
+<%ARGS>
+$Path
+$ARGSRef
+</%ARGS>
+<%INIT>
+# replace Search -> Assets with a submenu (if the user has ShowAssetsMenu)
+if (my $search_assets = Menu()->child("search")->child("assets")) {
+    $search_assets->path('/Asset/Search/Build.html');
+    $search_assets->child("asset_simple", title => loc("Simple Search"), path => "/Asset/Search/");
+    $search_assets->child("assetsql", title => loc("New Search"), path => "/Asset/Search/Build.html?NewQuery=1");
+}
+
+# add Assets -> New Search (if the user has ShowAssetsMenu)
+if (Menu()->child("assets") && (my $assets_search = Menu()->child("assets")->child("search"))) {
+    $assets_search->title(loc("Simple Search"));
+    $assets_search->add_before("assetsql", title => loc("New Search"), path => "/Asset/Search/Build.html?NewQuery=1");
+}
+
+my $query_string = sub {
+    my %args = @_;
+    my $u    = URI->new();
+    $u->query_form(map { $_ => $args{$_} } sort keys %args);
+    return $u->query || '';
+};
+
+my $page = PageMenu();
+
+if ($Path =~ m{^/Asset/Search/(index.html)?$}) {
+    # no action needed; just here for the elsif
+}
+elsif ($Path =~ m{^/Asset/Search/}) {
+    my %search = map @{$_},
+        grep defined $_->[1] && length $_->[1],
+        map {ref $DECODED_ARGS->{$_} ? [$_, $DECODED_ARGS->{$_}[0]] : [$_, $DECODED_ARGS->{$_}] }
+        grep /^(?:q|SearchAssets|!?(Name|Description|Catalog|Status|Role\..+|CF\..+)|Order(?:By)?|Page)$/,
+        keys %$DECODED_ARGS;
+
+    my $current_search = $session{"CurrentAssetSearchHash"} || {};
+    my $search_id = $DECODED_ARGS->{'SavedSearchLoad'} || $DECODED_ARGS->{'SavedSearchId'} || $current_search->{'SearchId'} || '';
+    my $args      = '';
+    my $has_query;
+    $has_query = 1 if ( $DECODED_ARGS->{'Query'} or $current_search->{'Query'} );
+
+    my %query_args;
+    my %fallback_query_args = (
+        SavedSearchId => ( $search_id eq 'new' ) ? undef : $search_id,
+        (
+            map {
+                my $p = $_;
+                $p => $DECODED_ARGS->{$p} || $current_search->{$p}
+            } qw(Query Format OrderBy Order Page)
+        ),
+        RowsPerPage => (
+            defined $DECODED_ARGS->{'RowsPerPage'}
+            ? $DECODED_ARGS->{'RowsPerPage'}
+            : $current_search->{'RowsPerPage'}
+        ),
+    );
+
+    my $QueryString = $ARGSRef->{QueryString};
+    my $QueryArgs = $ARGSRef->{QueryArgs};
+    if ($QueryString) {
+        $args = '?' . $QueryString;
+    }
+    else {
+        my %final_query_args = ();
+        # key => callback to avoid unnecessary work
+
+        for my $param (keys %fallback_query_args) {
+            $final_query_args{$param} = defined($QueryArgs->{$param})
+                                      ? $QueryArgs->{$param}
+                                      : $fallback_query_args{$param};
+        }
+
+        for my $field (qw(Order OrderBy)) {
+            if ( ref( $final_query_args{$field} ) eq 'ARRAY' ) {
+                $final_query_args{$field} = join( "|", @{ $final_query_args{$field} } );
+            } elsif (not defined $final_query_args{$field}) {
+                delete $final_query_args{$field};
+            }
+            else {
+                $final_query_args{$field} ||= '';
+            }
+        }
+
+        $args = '?' . $query_string->(%final_query_args);
+    }
+
+    $page->child('edit_search',
+        title      => loc('Edit Search'),
+        path       => '/Asset/Search/Build.html' . $args,
+        sort_order => 1,
+    );
+    $page->child( advanced => title => loc('Advanced'), path => '/Asset/Search/Edit.html' . $args, sort_order => 2 );
+    if ($has_query) {
+        # these overwrite the core Asset menu items
+        $page->child( results => title => loc('Show Results'), path => '/Asset/Search/Results.html' . $args, sort_order => 3 );
+        $page->child('bulk',
+            title => loc('Bulk Update'),
+            path => '/Asset/Search/Bulk.html' . $args,
+            sort_order => 4,
+        );
+        $page->child('csv',
+            title => loc('Download Spreadsheet'),
+            path  => '/Asset/Search/Results.tsv' . $args,
+            sort_order => 5,
+        );
+    }
+}
+elsif ( $Path =~ m{^/Ticket/} && $session{CurrentUser}->HasRight( Right => 'ShowAssetsMenu', Object => RT->System) ) {
+    if ( ( $DECODED_ARGS->{'id'} || '' ) =~ /^(\d+)$/ ) {
+        my $id  = $1;
+        my $obj = RT::Ticket->new( $session{'CurrentUser'} );
+        $obj->Load($id);
+
+        if ( $obj and $obj->id && $obj->CurrentUserHasRight('ModifyTicket')) {
+            $page->child('actions')->child( edit_assets => title => loc('Edit Assets'), path => "/Asset/Search/Bulk.html?Query=Linked=" . $id );
+        }
+    }
+}
+</%INIT>
diff --git a/lib/RT/Extension/AssetSQL.pm b/lib/RT/Extension/AssetSQL.pm
index 011c031..00b4faf 100644
--- a/lib/RT/Extension/AssetSQL.pm
+++ b/lib/RT/Extension/AssetSQL.pm
@@ -5,6 +5,93 @@ use 5.010_001;
 
 our $VERSION = '0.01';
 
+require RT::Extension::AssetSQL::Assets;
+
+RT->AddStyleSheets("assetsql.css");
+
+sub RT::Interface::Web::QueryBuilder::Tree::GetReferencedCatalogs {
+    my $self = shift;
+
+    my $catalogs = {};
+
+    $self->traverse(
+        sub {
+            my $node = shift;
+
+            return if $node->isRoot;
+            return unless $node->isLeaf;
+
+            my $clause = $node->getNodeValue();
+            return unless $clause->{ Key } eq 'Catalog';
+            return unless $clause->{ Op } eq '=';
+
+            $catalogs->{ $clause->{ RawValue } } = 1;
+        }
+    );
+
+    return $catalogs;
+}
+
+sub RT::Interface::Web::QueryBuilder::Tree::ParseAssetSQL {
+    my $self = shift;
+    my %args = (
+        Query       => '',
+        CurrentUser => '',    #XXX: Hack
+        @_
+    );
+    my $string = $args{ 'Query' };
+
+    my @results;
+
+    my %field = %{ RT::Assets->new( $args{ 'CurrentUser' } )->FIELDS };
+    my %lcfield = map { ( lc( $_ ) => $_ ) } keys %field;
+
+    my $node = $self;
+
+    my %callback;
+    $callback{ 'OpenParen' } = sub {
+        $node = RT::Interface::Web::QueryBuilder::Tree->new( 'AND', $node );
+    };
+    $callback{ 'CloseParen' } = sub { $node = $node->getParent };
+    $callback{ 'EntryAggregator' } = sub { $node->setNodeValue( $_[ 0 ] ) };
+    $callback{ 'Condition' } = sub {
+        my ( $key, $op, $value ) = @_;
+        my $rawvalue = $value;
+
+        my ( $main_key ) = split /[.]/, $key;
+
+        my $class;
+        if ( exists $lcfield{ lc $main_key } ) {
+            $key =~ s/^[^.]+/ $lcfield{ lc $main_key } /e;
+            ( $main_key ) = split /[.]/, $key;    # make the case right
+            $class = $field{ $main_key }->[ 0 ];
+        }
+        unless ( $class ) {
+            push @results, [ $args{ 'CurrentUser' }->loc( "Unknown field: [_1]", $key ), -1 ];
+        }
+
+        if ( lc $op eq 'is' || lc $op eq 'is not' ) {
+            $value = 'NULL';                      # just fix possible mistakes here
+        }
+        elsif ( $value !~ /^[+-]?[0-9]+$/ ) {
+            $value =~ s/(['\\])/\\$1/g;
+            $value = "'$value'";
+        }
+
+        if ( $key =~ s/(['\\])/\\$1/g or $key =~ /[^{}\w\.]/ ) {
+            $key = "'$key'";
+        }
+
+        my $clause = { Key => $key, Op => $op, Value => $value, RawValue => $rawvalue };
+        $node->addChild( RT::Interface::Web::QueryBuilder::Tree->new( $clause ) );
+    };
+    $callback{ 'Error' } = sub { push @results, @_ };
+
+    require RT::SQL;
+    RT::SQL::Parse( $string, \%callback );
+    return @results;
+}
+
 =head1 NAME
 
 RT-Extension-AssetSQL - SQL search builder for Assets
@@ -25,6 +112,21 @@ RT-Extension-AssetSQL requires version RT 4.4.0 or later.
 
 This step may require root permissions.
 
+=item Patch your RT
+
+AssetSQL requires a small patch to work on versions of RT prior to 4.4.2.
+To patch such older versions of RT, run:
+
+    patch -d /opt/rt4 -p1 < patches/rt-4.4.0-4.4.1.patch
+
+RT versions 4.4.2 and later already contain the above patch.
+
+All versions of RT require the following patch for AssetSQL support:
+
+    patch -d /opt/rt4 -p1 < patches/assetsql.patch
+
+You must apply both patches if you're on RT 4.4.0 or 4.4.1.
+
 =item Edit your /opt/rt4/etc/RT_SiteConfig.pm
 
 Add this line:
diff --git a/lib/RT/Extension/AssetSQL/Assets.pm b/lib/RT/Extension/AssetSQL/Assets.pm
new file mode 100644
index 0000000..09a0a08
--- /dev/null
+++ b/lib/RT/Extension/AssetSQL/Assets.pm
@@ -0,0 +1,1544 @@
+use strict;
+use warnings;
+
+package RT::Assets;
+
+no warnings 'redefine';
+
+sub _Init {
+    my $self = shift;
+
+    $self->{'table'}             = "Assets";
+    $self->{'RecalcAssetLimits'} = 1;
+    $self->{'restriction_index'} = 1;
+    $self->{'primary_key'}       = "id";
+
+    delete $self->{'items_array'};
+    delete $self->{'item_map'};
+    delete $self->{'columns_to_display'};
+
+    $self->OrderBy( FIELD => 'Name', ORDER => 'ASC' );
+
+    $self->SUPER::_Init(@_);
+
+    $self->_InitSQL();
+}
+
+sub _InitSQL {
+    my $self = shift;
+    # Private Member Variables (which should get cleaned)
+    $self->{'_sql_cf_alias'}  = undef;
+    $self->{'_sql_object_cfv_alias'}  = undef;
+    $self->{'_sql_watcher_join_users_alias'} = undef;
+    $self->{'_sql_query'}         = '';
+    $self->{'_sql_looking_at'}    = {};
+}
+
+sub Count {
+    my $self = shift;
+    $self->_ProcessRestrictions() if ( $self->{'RecalcAssetLimits'} == 1 );
+    return ( $self->SUPER::Count() );
+}
+
+sub CountAll {
+    my $self = shift;
+    $self->_ProcessRestrictions() if ( $self->{'RecalcAssetLimits'} == 1 );
+    return ( $self->SUPER::CountAll() );
+}
+
+sub ItemsArrayRef {
+    my $self = shift;
+
+    return $self->{'items_array'} if $self->{'items_array'};
+
+    my $placeholder = $self->_ItemsCounter;
+    $self->GotoFirstItem();
+    while ( my $item = $self->Next ) {
+        push( @{ $self->{'items_array'} }, $item );
+    }
+    $self->GotoItem($placeholder);
+    $self->{'items_array'} ||= [];
+    $self->{'items_array'}
+        = $self->ItemsOrderBy( $self->{'items_array'} );
+
+    return $self->{'items_array'};
+}
+
+sub ItemsArrayRefWindow {
+    my $self = shift;
+    my $window = shift;
+
+    my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
+
+    $self->RowsPerPage( $window );
+    $self->FirstRow(1);
+    $self->GotoFirstItem;
+
+    my @res;
+    while ( my $item = $self->Next ) {
+        push @res, $item;
+    }
+
+    $self->RowsPerPage( $old[1] );
+    $self->FirstRow( $old[2] );
+    $self->GotoItem( $old[0] );
+
+    return \@res;
+}
+
+sub Next {
+    my $self = shift;
+
+    $self->_ProcessRestrictions() if ( $self->{'RecalcAssetLimits'} == 1 );
+
+    my $Asset = $self->SUPER::Next;
+    return $Asset unless $Asset;
+
+    if ( $Asset->__Value('Status') eq 'deleted'
+        && !$self->{'allow_deleted_search'} )
+    {
+        return $self->Next;
+    }
+    elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
+        # if we found an asset with this option enabled then
+        # all assets we found are ACLed, cache this fact
+        my $key = join ";:;", $self->CurrentUser->id, 'ShowAsset', 'RT::Asset-'. $Asset->id;
+        $RT::Principal::_ACL_CACHE->{ $key } = 1;
+        return $Asset;
+    }
+    elsif ( $Asset->CurrentUserHasRight('ShowAsset') ) {
+        # has rights
+        return $Asset;
+    }
+    else {
+        # If the user doesn't have the right to show this asset
+        return $self->Next;
+    }
+}
+
+sub _DoSearch {
+    my $self = shift;
+    $self->Limit( FIELD => 'Status', OPERATOR => '!=', VALUE => 'deleted', SUBCLAUSE => "not_deleted" )
+      unless $self->{ 'allow_deleted_search' };
+    $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
+    return $self->SUPER::_DoSearch( @_ );
+}
+
+sub _DoCount {
+    my $self = shift;
+    $self->Limit( FIELD => 'Status', OPERATOR => '!=', VALUE => 'deleted', SUBCLAUSE => "not_deleted" )
+      unless $self->{ 'allow_deleted_search' };
+    $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
+    return $self->SUPER::_DoCount( @_ );
+}
+
+sub _RolesCanSee {
+    my $self = shift;
+
+    my $cache_key = 'RolesHasRight;:;ShowAsset';
+
+    if ( my $cached = $RT::Principal::_ACL_CACHE->{ $cache_key } ) {
+        return %$cached;
+    }
+
+    my $ACL = RT::ACL->new( RT->SystemUser );
+    $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowAsset' );
+    $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
+    my $principal_alias = $ACL->Join(
+        ALIAS1 => 'main',
+        FIELD1 => 'PrincipalId',
+        TABLE2 => 'Principals',
+        FIELD2 => 'id',
+    );
+    $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
+
+    my %res = ();
+    foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
+        my $role = $ACE->__Value('PrincipalType');
+        my $type = $ACE->__Value('ObjectType');
+        if ( $type eq 'RT::System' ) {
+            $res{ $role } = 1;
+        }
+        elsif ( $type eq 'RT::Catalog' ) {
+            next if $res{ $role } && !ref $res{ $role };
+            push @{ $res{ $role } ||= [] }, $ACE->__Value('ObjectId');
+        }
+        else {
+            $RT::Logger->error('ShowAsset right is granted on unsupported object');
+        }
+    }
+    $RT::Principal::_ACL_CACHE->{ $cache_key } = \%res;
+    return %res;
+}
+
+sub _DirectlyCanSeeIn {
+    my $self = shift;
+    my $id = $self->CurrentUser->id;
+
+    my $cache_key = 'User-'. $id .';:;ShowAsset;:;DirectlyCanSeeIn';
+    if ( my $cached = $RT::Principal::_ACL_CACHE->{ $cache_key } ) {
+        return @$cached;
+    }
+
+    my $ACL = RT::ACL->new( RT->SystemUser );
+    $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowAsset' );
+    my $principal_alias = $ACL->Join(
+        ALIAS1 => 'main',
+        FIELD1 => 'PrincipalId',
+        TABLE2 => 'Principals',
+        FIELD2 => 'id',
+    );
+    $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
+    my $cgm_alias = $ACL->Join(
+        ALIAS1 => 'main',
+        FIELD1 => 'PrincipalId',
+        TABLE2 => 'CachedGroupMembers',
+        FIELD2 => 'GroupId',
+    );
+    $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
+    $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
+
+    my @res = ();
+    foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
+        my $type = $ACE->__Value('ObjectType');
+        if ( $type eq 'RT::System' ) {
+            # If user is direct member of a group that has the right
+            # on the system then he can see any asset
+            $RT::Principal::_ACL_CACHE->{ $cache_key } = [-1];
+            return (-1);
+        }
+        elsif ( $type eq 'RT::Catalog' ) {
+            push @res, $ACE->__Value('ObjectId');
+        }
+        else {
+            $RT::Logger->error('ShowAsset right is granted on unsupported object');
+        }
+    }
+    $RT::Principal::_ACL_CACHE->{ $cache_key } = \@res;
+    return @res;
+}
+
+sub CurrentUserCanSee {
+    my $self = shift;
+    return if $self->{'_sql_current_user_can_see_applied'};
+
+    return $self->{'_sql_current_user_can_see_applied'} = 1
+        if $self->CurrentUser->UserObj->HasRight(
+            Right => 'SuperUser', Object => $RT::System
+        );
+
+    local $self->{using_restrictions};
+
+    my $id = $self->CurrentUser->id;
+
+    # directly can see in all catalogs then we have nothing to do
+    my @direct_catalogs = $self->_DirectlyCanSeeIn;
+    return $self->{'_sql_current_user_can_see_applied'} = 1
+        if @direct_catalogs && $direct_catalogs[0] == -1;
+
+    my %roles = $self->_RolesCanSee;
+    {
+        my %skip = map { $_ => 1 } @direct_catalogs;
+        foreach my $role ( keys %roles ) {
+            next unless ref $roles{ $role };
+
+            my @catalogs = grep !$skip{$_}, @{ $roles{ $role } };
+            if ( @catalogs ) {
+                $roles{ $role } = \@catalogs;
+            } else {
+                delete $roles{ $role };
+            }
+        }
+    }
+
+# there is no global watchers, only catalogs and tickes, if at
+# some point we will add global roles then it's gonna blow
+# the idea here is that if the right is set globaly for a role
+# and user plays this role for a catalog directly not a ticket
+# then we have to check in advance
+    if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
+
+        my $groups = RT::Groups->new( RT->SystemUser );
+        $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Catalog-Role', CASESENSITIVE => 0 );
+        $groups->Limit(
+            FIELD         => 'Name',
+            FUNCTION      => 'LOWER(?)',
+            OPERATOR      => 'IN',
+            VALUE         => [ map {lc $_} @tmp ],
+            CASESENSITIVE => 1,
+        );
+        my $principal_alias = $groups->Join(
+            ALIAS1 => 'main',
+            FIELD1 => 'id',
+            TABLE2 => 'Principals',
+            FIELD2 => 'id',
+        );
+        $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
+        my $cgm_alias = $groups->Join(
+            ALIAS1 => 'main',
+            FIELD1 => 'id',
+            TABLE2 => 'CachedGroupMembers',
+            FIELD2 => 'GroupId',
+        );
+        $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
+        $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
+        while ( my $group = $groups->Next ) {
+            push @direct_catalogs, $group->Instance;
+        }
+    }
+
+    unless ( @direct_catalogs || keys %roles ) {
+        $self->Limit(
+            SUBCLAUSE => 'ACL',
+            ALIAS => 'main',
+            FIELD => 'id',
+            VALUE => 0,
+            ENTRYAGGREGATOR => 'AND',
+        );
+        return $self->{'_sql_current_user_can_see_applied'} = 1;
+    }
+
+    {
+        my $join_roles = keys %roles;
+        $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
+        my ($role_group_alias, $cgm_alias);
+        if ( $join_roles ) {
+            $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
+            $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
+            $self->Limit(
+                LEFTJOIN   => $cgm_alias,
+                FIELD      => 'MemberId',
+                OPERATOR   => '=',
+                VALUE      => $id,
+            );
+        }
+        my $limit_catalogs = sub {
+            my $ea = shift;
+            my @catalogs = @_;
+
+            return unless @catalogs;
+            $self->Limit(
+                SUBCLAUSE       => 'ACL',
+                ALIAS           => 'main',
+                FIELD           => 'Catalog',
+                OPERATOR        => 'IN',
+                VALUE           => [ @catalogs ],
+                ENTRYAGGREGATOR => $ea,
+            );
+            return 1;
+        };
+
+        $self->SUPER::_OpenParen('ACL');
+        my $ea = 'AND';
+        $ea = 'OR' if $limit_catalogs->( $ea, @direct_catalogs );
+        while ( my ($role, $catalogs) = each %roles ) {
+            $self->SUPER::_OpenParen('ACL');
+            if ( $role eq 'Owner' ) {
+                $self->Limit(
+                    SUBCLAUSE => 'ACL',
+                    FIELD           => 'Owner',
+                    VALUE           => $id,
+                    ENTRYAGGREGATOR => $ea,
+                );
+            }
+            else {
+                $self->Limit(
+                    SUBCLAUSE       => 'ACL',
+                    ALIAS           => $cgm_alias,
+                    FIELD           => 'MemberId',
+                    OPERATOR        => 'IS NOT',
+                    VALUE           => 'NULL',
+                    QUOTEVALUE      => 0,
+                    ENTRYAGGREGATOR => $ea,
+                );
+                $self->Limit(
+                    SUBCLAUSE       => 'ACL',
+                    ALIAS           => $role_group_alias,
+                    FIELD           => 'Name',
+                    VALUE           => $role,
+                    ENTRYAGGREGATOR => 'AND',
+                    CASESENSITIVE   => 0,
+                );
+            }
+            $limit_catalogs->( 'AND', @$catalogs ) if ref $catalogs;
+            $ea = 'OR' if $ea eq 'AND';
+            $self->SUPER::_CloseParen('ACL');
+        }
+        $self->SUPER::_CloseParen('ACL');
+    }
+    return $self->{'_sql_current_user_can_see_applied'} = 1;
+}
+
+sub _OpenParen {
+    $_[0]->SUPER::_OpenParen( $_[1] || 'assetsql' );
+}
+sub _CloseParen {
+    $_[0]->SUPER::_CloseParen( $_[1] || 'assetsql' );
+}
+
+sub Limit {
+    my $self = shift;
+    my %args = (
+        CASESENSITIVE => 0,
+        @_
+    );
+    $self->{'must_redo_search'} = 1;
+    delete $self->{'raw_rows'};
+    delete $self->{'count_all'};
+
+    if ($self->{'using_restrictions'}) {
+        RT->Deprecated( Message => "Mixing old-style LimitFoo methods with Limit is deprecated" );
+        $self->LimitField(@_);
+    }
+
+    $args{SUBCLAUSE} ||= "assetsql"
+        if $self->{parsing_assetsql} and not $args{LEFTJOIN};
+
+    $self->{_sql_looking_at}{ lc $args{FIELD} } = 1
+        if $args{FIELD} and (not $args{ALIAS} or $args{ALIAS} eq "main");
+
+    $self->SUPER::Limit(%args);
+}
+
+
+=head2 LimitField
+
+Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
+Generally best called from LimitFoo methods
+
+=cut
+
+sub LimitField {
+    my $self = shift;
+    my %args = (
+        FIELD       => undef,
+        OPERATOR    => '=',
+        VALUE       => undef,
+        DESCRIPTION => undef,
+        @_
+    );
+    $args{'DESCRIPTION'} = $self->loc(
+        "[_1] [_2] [_3]",  $args{'FIELD'},
+        $args{'OPERATOR'}, $args{'VALUE'}
+        )
+        if ( !defined $args{'DESCRIPTION'} );
+
+
+    if ($self->_isLimited > 1) {
+        RT->Deprecated( Message => "Mixing old-style LimitFoo methods with Limit is deprecated" );
+    }
+    $self->{using_restrictions} = 1;
+
+    my $index = $self->_NextIndex;
+
+# make the TicketRestrictions hash the equivalent of whatever we just passed in;
+
+    %{ $self->{'TicketRestrictions'}{$index} } = %args;
+
+    $self->{'RecalcTicketLimits'} = 1;
+
+    return ($index);
+}
+
+# Configuration Tables:
+
+# FIELD_METADATA is a mapping of searchable Field name, to Type, and other
+# metadata.
+
+our %FIELD_METADATA = (
+    id              => [ 'ID', ], #loc_left_pair
+    Name             => [ 'STRING', ], #loc_left_pair
+    Description      => [ 'STRING', ], #loc_left_pair
+    Status          => [ 'STRING', ], #loc_left_pair
+    Catalog         => [ 'ENUM' => 'Catalog', ], #loc_left_pair
+    LastUpdated      => [ 'DATE'            => 'LastUpdated', ], #loc_left_pair
+    Created          => [ 'DATE'            => 'Created', ], #loc_left_pair
+
+    Linked          => [ 'LINK' ], #loc_left_pair
+    LinkedTo        => [ 'LINK' => 'To' ], #loc_left_pair
+    LinkedFrom      => [ 'LINK' => 'From' ], #loc_left_pair
+    MemberOf        => [ 'LINK' => To => 'MemberOf', ], #loc_left_pair
+    DependsOn       => [ 'LINK' => To => 'DependsOn', ], #loc_left_pair
+    RefersTo        => [ 'LINK' => To => 'RefersTo', ], #loc_left_pair
+    HasMember       => [ 'LINK' => From => 'MemberOf', ], #loc_left_pair
+    DependentOn     => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
+    DependedOnBy    => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
+    ReferredToBy    => [ 'LINK' => From => 'RefersTo', ], #loc_left_pair
+
+    Owner           => [ 'WATCHERFIELD' => 'Owner', ], #loc_left_pair
+    OwnerGroup      => [ 'MEMBERSHIPFIELD' => 'Owner', ], #loc_left_pair
+    HeldBy          => [ 'WATCHERFIELD' => 'HeldBy', ], #loc_left_pair
+    HeldByGroup     => [ 'MEMBERSHIPFIELD' => 'HeldBy', ], #loc_left_pair
+    Contact         => [ 'WATCHERFIELD' => 'Contact', ], #loc_left_pair
+    ContactGroup    => [ 'MEMBERSHIPFIELD' => 'Contact', ], #loc_left_pair
+
+    CustomFieldValue => [ 'CUSTOMFIELD' => 'Asset' ], #loc_left_pair
+    CustomField      => [ 'CUSTOMFIELD' => 'Asset' ], #loc_left_pair
+    CF               => [ 'CUSTOMFIELD' => 'Asset' ], #loc_left_pair
+);
+
+# Lower Case version of FIELDS, for case insensitivity
+our %LOWER_CASE_FIELDS = map { ( lc($_) => $_ ) } (keys %FIELD_METADATA);
+
+our %SEARCHABLE_SUBFIELDS = (
+    User => [qw(
+        EmailAddress Name RealName Nickname Organization Address1 Address2
+        City State Zip Country WorkPhone HomePhone MobilePhone PagerPhone id
+    )],
+);
+
+# Mapping of Field Type to Function
+our %dispatch = (
+    ENUM            => \&_EnumLimit,
+    INT             => \&_IntLimit,
+    ID              => \&_IdLimit,
+    LINK            => \&_LinkLimit,
+    DATE            => \&_DateLimit,
+    STRING          => \&_StringLimit,
+    WATCHERFIELD    => \&_WatcherLimit,
+    MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
+    CUSTOMFIELD     => \&_CustomFieldLimit,
+#    HASATTRIBUTE    => \&_HasAttributeLimit,
+#    LIFECYCLE       => \&_LifecycleLimit,
+);
+
+# Default EntryAggregator per type
+# if you specify OP, you must specify all valid OPs
+my %DefaultEA = (
+    INT  => 'AND',
+    ENUM => {
+        '='  => 'OR',
+        '!=' => 'AND'
+    },
+    DATE => {
+        'IS' => 'OR',
+        'IS NOT' => 'OR',
+        '='  => 'OR',
+        '>=' => 'AND',
+        '<=' => 'AND',
+        '>'  => 'AND',
+        '<'  => 'AND'
+    },
+    STRING => {
+        '='        => 'OR',
+        '!='       => 'AND',
+        'LIKE'     => 'AND',
+        'NOT LIKE' => 'AND'
+    },
+    LINK         => 'OR',
+    LINKFIELD    => 'AND',
+    TARGET       => 'AND',
+    BASE         => 'AND',
+    WATCHERFIELD => {
+        '='        => 'OR',
+        '!='       => 'AND',
+        'LIKE'     => 'OR',
+        'NOT LIKE' => 'AND'
+    },
+
+    HASATTRIBUTE => {
+        '='        => 'AND',
+        '!='       => 'AND',
+    },
+
+    CUSTOMFIELD => 'OR',
+);
+
+sub FIELDS     { return \%FIELD_METADATA }
+
+our @SORTFIELDS = qw(id Name Status Catalog Owner Created LastUpdated );
+
+=head2 SortFields
+
+Returns the list of fields that lists of assets can easily be sorted by
+
+=cut
+
+sub SortFields {
+    my $self = shift;
+    return (@SORTFIELDS);
+}
+
+
+# BEGIN SQL STUFF *********************************
+
+
+sub CleanSlate {
+    my $self = shift;
+    $self->SUPER::CleanSlate( @_ );
+    delete $self->{$_} foreach qw(
+        _sql_cf_alias
+        _sql_group_members_aliases
+        _sql_object_cfv_alias
+        _sql_role_group_aliases
+        _sql_u_watchers_alias_for_sort
+        _sql_u_watchers_aliases
+        _sql_current_user_can_see_applied
+    );
+}
+
+=head1 Limit Helper Routines
+
+These routines are the targets of a dispatch table depending on the
+type of field.  They all share the same signature:
+
+  my ($self,$field,$op,$value, at rest) = @_;
+
+The values in @rest should be suitable for passing directly to
+DBIx::SearchBuilder::Limit.
+
+Essentially they are an expanded/broken out (and much simplified)
+version of what ProcessRestrictions used to do.  They're also much
+more clearly delineated by the TYPE of field being processed.
+
+=head2 _IdLimit
+
+Handle ID field.
+
+=cut
+
+sub _IdLimit {
+    my ( $sb, $field, $op, $value, @rest ) = @_;
+    return $sb->_IntLimit( $field, $op, $value, @rest );
+}
+
+=head2 _EnumLimit
+
+Handle Fields which are limited to certain values, and potentially
+need to be looked up from another class.
+
+This subroutine actually handles two different kinds of fields.  For
+some the user is responsible for limiting the values.  (i.e. Status,
+Type).
+
+For others, the value specified by the user will be looked by via
+specified class.
+
+Meta Data:
+  name of class to lookup in (Optional)
+
+=cut
+
+sub _EnumLimit {
+    my ( $sb, $field, $op, $value, @rest ) = @_;
+
+    # SQL::Statement changes != to <>.  (Can we remove this now?)
+    $op = "!=" if $op eq "<>";
+
+    die "Invalid Operation: $op for $field"
+        unless $op eq "="
+        or $op     eq "!=";
+
+    my $meta = $FIELD_METADATA{$field};
+    if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
+        my $class = "RT::" . $meta->[1];
+        my $o     = $class->new( $sb->CurrentUser );
+        $o->Load($value);
+        $value = $o->Id || 0;
+    }
+    $sb->Limit(
+        FIELD    => $field,
+        VALUE    => $value,
+        OPERATOR => $op,
+        @rest,
+    );
+}
+
+=head2 _IntLimit
+
+Handle fields where the values are limited to integers.  (For example,
+Priority, TimeWorked.)
+
+Meta Data:
+  None
+
+=cut
+
+sub _IntLimit {
+    my ( $sb, $field, $op, $value, @rest ) = @_;
+
+    my $is_a_like = $op =~ /MATCHES|ENDSWITH|STARTSWITH|LIKE/i;
+
+    # We want to support <id LIKE '1%'> for asset autocomplete,
+    # but we need to explicitly typecast on Postgres
+    if ( $is_a_like && RT->Config->Get('DatabaseType') eq 'Pg' ) {
+        return $sb->Limit(
+            FUNCTION => "CAST(main.$field AS TEXT)",
+            OPERATOR => $op,
+            VALUE    => $value,
+            @rest,
+        );
+    }
+
+    $sb->Limit(
+        FIELD    => $field,
+        VALUE    => $value,
+        OPERATOR => $op,
+        @rest,
+    );
+}
+
+=head2 _LinkLimit
+
+Handle fields which deal with links between assets.  (MemberOf, DependsOn)
+
+Meta Data:
+  1: Direction (From, To)
+  2: Link Type (MemberOf, DependsOn, RefersTo)
+
+=cut
+
+sub _LinkLimit {
+    my ( $sb, $field, $op, $value, @rest ) = @_;
+
+    my $meta = $FIELD_METADATA{$field};
+    die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
+
+    my $is_negative = 0;
+    if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
+        $is_negative = 1;
+    }
+    my $is_null = 0;
+    $is_null = 1 if !$value || $value =~ /^null$/io;
+
+    my $direction = $meta->[1] || '';
+    my ($matchfield, $linkfield) = ('', '');
+    if ( $direction eq 'To' ) {
+        ($matchfield, $linkfield) = ("Target", "Base");
+    }
+    elsif ( $direction eq 'From' ) {
+        ($matchfield, $linkfield) = ("Base", "Target");
+    }
+    elsif ( $direction ) {
+        die "Invalid link direction '$direction' for $field\n";
+    } else {
+        $sb->_OpenParen;
+        $sb->_LinkLimit( 'LinkedTo', $op, $value, @rest );
+        $sb->_LinkLimit(
+            'LinkedFrom', $op, $value, @rest,
+            ENTRYAGGREGATOR => (($is_negative && $is_null) || (!$is_null && !$is_negative))? 'OR': 'AND',
+        );
+        $sb->_CloseParen;
+        return;
+    }
+
+    my $is_local = 1;
+    if ( $is_null ) {
+        $op = ($op =~ /^(=|IS)$/i)? 'IS': 'IS NOT';
+    }
+    elsif ( $value =~ /\D/ ) {
+        $value = RT::URI->new( $sb->CurrentUser )->CanonicalizeURI( $value );
+        $is_local = 0;
+    }
+    $matchfield = "Local$matchfield" if $is_local;
+
+#For doing a left join to find "unlinked assets" we want to generate a query that looks like this
+#    SELECT main.* FROM Assets main
+#        LEFT JOIN Links Links_1 ON (     (Links_1.Type = 'MemberOf')
+#                                      AND(main.id = Links_1.LocalTarget))
+#        WHERE Links_1.LocalBase IS NULL;
+
+    my $join_expression;
+    if ( RT->Config->Get('DatabaseType') eq 'SQLite' ) {
+        $join_expression = q{'} . RT::URI::asset->new( RT->SystemUser )->LocalURIPrefix . q{' ||  main.id};
+    }
+    else {
+        $join_expression = q{CONCAT( '} . RT::URI::asset->new( RT->SystemUser )->LocalURIPrefix . q{',  main.id )};
+    }
+    if ( $is_null ) {
+        my $linkalias = $sb->Join(
+            TYPE   => 'LEFT',
+            ALIAS1 => 'main',
+            FIELD1 => 'id',
+            TABLE2 => 'Links',
+            FIELD2 => $linkfield,
+            EXPRESSION => $join_expression,
+        );
+        $sb->Limit(
+            LEFTJOIN => $linkalias,
+            FIELD    => 'Type',
+            OPERATOR => '=',
+            VALUE    => $meta->[2],
+        ) if $meta->[2];
+        $sb->Limit(
+            @rest,
+            ALIAS      => $linkalias,
+            FIELD      => $matchfield,
+            OPERATOR   => $op,
+            VALUE      => 'NULL',
+            QUOTEVALUE => 0,
+        );
+    }
+    else {
+        my $linkalias = $sb->Join(
+            TYPE   => 'LEFT',
+            ALIAS1 => 'main',
+            FIELD1 => 'id',
+            TABLE2 => 'Links',
+            FIELD2 => $linkfield,
+            EXPRESSION => $join_expression,
+        );
+        $sb->Limit(
+            LEFTJOIN => $linkalias,
+            FIELD    => 'Type',
+            OPERATOR => '=',
+            VALUE    => $meta->[2],
+        ) if $meta->[2];
+        $sb->Limit(
+            LEFTJOIN => $linkalias,
+            FIELD    => $matchfield,
+            OPERATOR => '=',
+            VALUE    => $value,
+        );
+        $sb->Limit(
+            @rest,
+            ALIAS      => $linkalias,
+            FIELD      => $matchfield,
+            OPERATOR   => $is_negative? 'IS': 'IS NOT',
+            VALUE      => 'NULL',
+            QUOTEVALUE => 0,
+        );
+    }
+}
+
+=head2 _DateLimit
+
+Handle date fields.  (Created, LastTold..)
+
+Meta Data:
+  1: type of link.  (Probably not necessary.)
+
+=cut
+
+sub _DateLimit {
+    my ( $sb, $field, $op, $value, %rest ) = @_;
+
+    die "Invalid Date Op: $op"
+        unless $op =~ /^(=|>|<|>=|<=|IS(\s+NOT)?)$/i;
+
+    my $meta = $FIELD_METADATA{$field};
+    die "Incorrect Meta Data for $field"
+        unless ( defined $meta->[1] );
+
+    if ( $op =~ /^(IS(\s+NOT)?)$/i) {
+        return $sb->Limit(
+            FUNCTION => $sb->NotSetDateToNullFunction,
+            FIELD    => $meta->[1],
+            OPERATOR => $op,
+            VALUE    => "NULL",
+            %rest,
+        );
+    }
+
+    if ( my $subkey = $rest{SUBKEY} ) {
+        if ( $subkey eq 'DayOfWeek' && $op !~ /IS/i && $value =~ /[^0-9]/ ) {
+            for ( my $i = 0; $i < @RT::Date::DAYS_OF_WEEK; $i++ ) {
+                # Use a case-insensitive regex for better matching across
+                # locales since we don't have fc() and lc() is worse.  Really
+                # we should be doing Unicode normalization too, but we don't do
+                # that elsewhere in RT.
+                # 
+                # XXX I18N: Replace the regex with fc() once we're guaranteed 5.16.
+                next unless lc $RT::Date::DAYS_OF_WEEK[ $i ] eq lc $value
+                         or $sb->CurrentUser->loc($RT::Date::DAYS_OF_WEEK[ $i ]) =~ /^\Q$value\E$/i;
+
+                $value = $i; last;
+            }
+            return $sb->Limit( FIELD => 'id', VALUE => 0, %rest )
+                if $value =~ /[^0-9]/;
+        }
+        elsif ( $subkey eq 'Month' && $op !~ /IS/i && $value =~ /[^0-9]/ ) {
+            for ( my $i = 0; $i < @RT::Date::MONTHS; $i++ ) {
+                # Use a case-insensitive regex for better matching across
+                # locales since we don't have fc() and lc() is worse.  Really
+                # we should be doing Unicode normalization too, but we don't do
+                # that elsewhere in RT.
+                # 
+                # XXX I18N: Replace the regex with fc() once we're guaranteed 5.16.
+                next unless lc $RT::Date::MONTHS[ $i ] eq lc $value
+                         or $sb->CurrentUser->loc($RT::Date::MONTHS[ $i ]) =~ /^\Q$value\E$/i;
+
+                $value = $i + 1; last;
+            }
+            return $sb->Limit( FIELD => 'id', VALUE => 0, %rest )
+                if $value =~ /[^0-9]/;
+        }
+
+        my $tz;
+        if ( RT->Config->Get('ChartsTimezonesInDB') ) {
+            my $to = $sb->CurrentUser->UserObj->Timezone
+                || RT->Config->Get('Timezone');
+            $tz = { From => 'UTC', To => $to }
+                if $to && lc $to ne 'utc';
+        }
+
+        # $subkey is validated by DateTimeFunction
+        my $function = $RT::Handle->DateTimeFunction(
+            Type     => $subkey,
+            Field    => $sb->NotSetDateToNullFunction,
+            Timezone => $tz,
+        );
+
+        return $sb->Limit(
+            FUNCTION => $function,
+            FIELD    => $meta->[1],
+            OPERATOR => $op,
+            VALUE    => $value,
+            %rest,
+        );
+    }
+
+    my $date = RT::Date->new( $sb->CurrentUser );
+    $date->Set( Format => 'unknown', Value => $value );
+
+    if ( $op eq "=" ) {
+
+        # if we're specifying =, that means we want everything on a
+        # particular single day.  in the database, we need to check for >
+        # and < the edges of that day.
+
+        $date->SetToMidnight( Timezone => 'server' );
+        my $daystart = $date->ISO;
+        $date->AddDay;
+        my $dayend = $date->ISO;
+
+        $sb->_OpenParen;
+
+        $sb->Limit(
+            FIELD    => $meta->[1],
+            OPERATOR => ">=",
+            VALUE    => $daystart,
+            %rest,
+        );
+
+        $sb->Limit(
+            FIELD    => $meta->[1],
+            OPERATOR => "<",
+            VALUE    => $dayend,
+            %rest,
+            ENTRYAGGREGATOR => 'AND',
+        );
+
+        $sb->_CloseParen;
+
+    }
+    else {
+        $sb->Limit(
+            FUNCTION => $sb->NotSetDateToNullFunction,
+            FIELD    => $meta->[1],
+            OPERATOR => $op,
+            VALUE    => $date->ISO,
+            %rest,
+        );
+    }
+}
+
+=head2 _StringLimit
+
+Handle simple fields which are just strings.  (Subject,Type)
+
+Meta Data:
+  None
+
+=cut
+
+sub _StringLimit {
+    my ( $sb, $field, $op, $value, @rest ) = @_;
+
+    # FIXME:
+    # Valid Operators:
+    #  =, !=, LIKE, NOT LIKE
+    if ( RT->Config->Get('DatabaseType') eq 'Oracle'
+        && (!defined $value || !length $value)
+        && lc($op) ne 'is' && lc($op) ne 'is not'
+    ) {
+        if ($op eq '!=' || $op =~ /^NOT\s/i) {
+            $op = 'IS NOT';
+        } else {
+            $op = 'IS';
+        }
+        $value = 'NULL';
+    }
+
+    if ($field eq "Status") {
+        $value = lc $value;
+    }
+
+    $sb->Limit(
+        FIELD         => $field,
+        OPERATOR      => $op,
+        VALUE         => $value,
+        CASESENSITIVE => 0,
+        @rest,
+    );
+}
+
+=head2 _WatcherLimit
+
+Handle watcher limits.  (Requestor, CC, etc..)
+
+Meta Data:
+  1: Field to query on
+
+
+
+=cut
+
+sub _WatcherLimit {
+    my $self  = shift;
+    my $field = shift;
+    my $op    = shift;
+    my $value = shift;
+    my %rest  = (@_);
+
+    my $meta = $FIELD_METADATA{ $field };
+    my $type = $meta->[1] || '';
+    my $class = $meta->[2] || 'Asset';
+
+    # Bail if the subfield is not allowed
+    if (    $rest{SUBKEY}
+        and not grep { $_ eq $rest{SUBKEY} } @{$SEARCHABLE_SUBFIELDS{'User'}})
+    {
+        die "Invalid watcher subfield: '$rest{SUBKEY}'";
+    }
+
+    $self->RoleLimit(
+        TYPE      => $type,
+        CLASS     => "RT::$class",
+        FIELD     => $rest{SUBKEY},
+        OPERATOR  => $op,
+        VALUE     => $value,
+        SUBCLAUSE => "assetsql",
+        %rest,
+    );
+}
+
+=head2 _WatcherMembershipLimit
+
+Handle watcher membership limits, i.e. whether the watcher belongs to a
+specific group or not.
+
+Meta Data:
+  1: Role to query on
+
+=cut
+
+sub _WatcherMembershipLimit {
+    my ( $self, $field, $op, $value, %rest ) = @_;
+
+    # we don't support anything but '='
+    die "Invalid $field Op: $op"
+        unless $op =~ /^=$/;
+
+    unless ( $value =~ /^\d+$/ ) {
+        my $group = RT::Group->new( $self->CurrentUser );
+        $group->LoadUserDefinedGroup( $value );
+        $value = $group->id || 0;
+    }
+
+    my $meta = $FIELD_METADATA{$field};
+    my $type = $meta->[1] || '';
+
+    my ($members_alias, $members_column);
+    if ( $type eq 'Owner' ) {
+        ($members_alias, $members_column) = ('main', 'Owner');
+    } else {
+        (undef, undef, $members_alias) = $self->_WatcherJoin( New => 1, Name => $type );
+        $members_column = 'id';
+    }
+
+    my $cgm_alias = $self->Join(
+        ALIAS1          => $members_alias,
+        FIELD1          => $members_column,
+        TABLE2          => 'CachedGroupMembers',
+        FIELD2          => 'MemberId',
+    );
+    $self->Limit(
+        LEFTJOIN => $cgm_alias,
+        ALIAS => $cgm_alias,
+        FIELD => 'Disabled',
+        VALUE => 0,
+    );
+
+    $self->Limit(
+        ALIAS    => $cgm_alias,
+        FIELD    => 'GroupId',
+        VALUE    => $value,
+        OPERATOR => $op,
+        %rest,
+    );
+}
+
+=head2 _CustomFieldDecipher
+
+Try and turn a CF descriptor into (cfid, cfname) object pair.
+
+Takes an optional second parameter of the CF LookupType, defaults to Asset CFs.
+
+=cut
+
+sub _CustomFieldDecipher {
+    my ($self, $string, $lookuptype) = @_;
+    $lookuptype ||= $self->_SingularClass->CustomFieldLookupType;
+
+    my ($object, $field, $column) = ($string =~ /^(?:(.+?)\.)?\{(.+)\}(?:\.(Content|LargeContent))?$/);
+    $field ||= ($string =~ /^\{(.*?)\}$/)[0] || $string;
+
+    my ($cf, $applied_to);
+
+    if ( $object ) {
+        my $record_class = RT::CustomField->RecordClassFromLookupType($lookuptype);
+        $applied_to = $record_class->new( $self->CurrentUser );
+        $applied_to->Load( $object );
+
+        if ( $applied_to->id ) {
+            RT->Logger->debug("Limiting to CFs identified by '$field' applied to $record_class #@{[$applied_to->id]} (loaded via '$object')");
+        }
+        else {
+            RT->Logger->warning("$record_class '$object' doesn't exist, parsed from '$string'");
+            $object = 0;
+            undef $applied_to;
+        }
+    }
+
+    if ( $field =~ /\D/ ) {
+        $object ||= '';
+        my $cfs = RT::CustomFields->new( $self->CurrentUser );
+        $cfs->Limit( FIELD => 'Name', VALUE => $field, CASESENSITIVE => 0 );
+        $cfs->LimitToLookupType($lookuptype);
+
+        if ($applied_to) {
+            $cfs->SetContextObject($applied_to);
+            $cfs->LimitToObjectId($applied_to->id);
+        }
+
+        # if there is more then one field the current user can
+        # see with the same name then we shouldn't return cf object
+        # as we don't know which one to use
+        $cf = $cfs->First;
+        if ( $cf ) {
+            $cf = undef if $cfs->Next;
+        }
+        else {
+            # find the cf without ACL
+            # this is because current _CustomFieldJoinByName has a bug that
+            # can't search correctly with negative cf ops :/
+            my $cfs = RT::CustomFields->new( RT->SystemUser );
+            $cfs->Limit( FIELD => 'Name', VALUE => $field, CASESENSITIVE => 0 );
+            $cfs->LimitToLookupType( $lookuptype );
+
+            if ( $applied_to ) {
+                $cfs->SetContextObject( $applied_to );
+                $cfs->LimitToObjectId( $applied_to->id );
+            }
+
+            $cf = $cfs->First unless $cfs->Count > 1;
+        }
+
+    }
+    else {
+        $cf = RT::CustomField->new( $self->CurrentUser );
+        $cf->Load( $field );
+        $cf->SetContextObject($applied_to)
+            if $cf->id and $applied_to;
+    }
+
+    return ($object, $field, $cf, $column);
+}
+
+=head2 _CustomFieldLimit
+
+Limit based on CustomFields
+
+Meta Data:
+  none
+
+=cut
+
+sub _CustomFieldLimit {
+    my ( $self, $_field, $op, $value, %rest ) = @_;
+
+    my $meta  = $FIELD_METADATA{ $_field };
+    my $class = $meta->[1] || 'Asset';
+    my $type  = "RT::$class"->CustomFieldLookupType;
+
+    my $field = $rest{'SUBKEY'} || die "No field specified";
+
+    # For our sanity, we can only limit on one object at a time
+
+    my ($object, $cfid, $cf, $column);
+    ($object, $field, $cf, $column) = $self->_CustomFieldDecipher( $field, $type );
+
+
+    $self->_LimitCustomField(
+        %rest,
+        LOOKUPTYPE  => $type,
+        CUSTOMFIELD => $cf || $field,
+        KEY      => $cf ? $cf->id : "$type-$object.$field",
+        OPERATOR => $op,
+        VALUE    => $value,
+        COLUMN   => $column,
+        SUBCLAUSE => "assetsql",
+    );
+}
+
+sub _CustomFieldJoinByName {
+    my $self = shift;
+    my ($ObjectAlias, $cf, $type) = @_;
+
+    my ($ocfvalias, $CFs, $ocfalias) = $self->SUPER::_CustomFieldJoinByName(@_);
+    $self->Limit(
+        LEFTJOIN        => $ocfalias,
+        ENTRYAGGREGATOR => 'OR',
+        FIELD           => 'ObjectId',
+        VALUE           => 'main.Catalog',
+        QUOTEVALUE      => 0,
+    );
+    return ($ocfvalias, $CFs, $ocfalias);
+}
+
+=head2 PrepForSerialization
+
+You don't want to serialize a big assets object, as
+the {items} hash will be instantly invalid _and_ eat
+lots of space
+
+=cut
+
+sub PrepForSerialization {
+    my $self = shift;
+    delete $self->{'items'};
+    delete $self->{'items_array'};
+    $self->RedoSearch();
+}
+
+=head2 FromSQL
+
+Convert a RT-SQL string into a set of SearchBuilder restrictions.
+
+Returns (1, 'Status message') on success and (0, 'Error Message') on
+failure.
+
+=cut
+
+sub _parser {
+    my ($self,$string) = @_;
+    my $ea = '';
+
+    my %sub_tree;
+    my $depth = 0;
+
+    my %callback;
+    $callback{'OpenParen'} = sub {
+      $self->_OpenParen;
+      $depth++;
+      push @$_, '(' foreach values %sub_tree;
+    };
+    $callback{'CloseParen'} = sub {
+      $self->_CloseParen;
+      $depth--;
+      foreach my $list ( values %sub_tree ) {
+          if ( $list->[-1] eq '(' ) {
+              pop @$list;
+              pop @$list if $list->[-1] =~ /^(?:AND|OR)$/i;
+          }
+          else {
+              pop @$list while $list->[-2] ne '(';
+              $list->[-1] = pop @$list;
+          }
+      }
+    };
+    $callback{'EntryAggregator'} = sub {
+      $ea = $_[0] || '';
+      push @$_, $ea foreach grep @$_ && $_->[-1] ne '(', values %sub_tree;
+    };
+    $callback{'Condition'} = sub {
+        my ($key, $op, $value) = @_;
+
+        my $negative_op = ($op eq '!=' || $op =~ /\bNOT\b/i);
+        my $null_op = ( 'is not' eq lc($op) || 'is' eq lc($op) );
+        # key has dot then it's compound variant and we have subkey
+        my $subkey = '';
+        ($key, $subkey) = ($1, $2) if $key =~ /^([^\.]+)\.(.+)$/;
+
+        # normalize key and get class (type)
+        my $class;
+        if (exists $LOWER_CASE_FIELDS{lc $key}) {
+            $key = $LOWER_CASE_FIELDS{lc $key};
+            $class = $FIELD_METADATA{$key}->[0];
+        }
+        die "Unknown field '$key' in '$string'" unless $class;
+
+        # replace __CurrentUser__ with id
+        $value = $self->CurrentUser->id if $value eq '__CurrentUser__';
+
+
+        unless( $dispatch{ $class } ) {
+            die "No dispatch method for class '$class'"
+        }
+        my $sub = $dispatch{ $class };
+
+        my @res; my $bundle_with;
+        if ( $class eq 'WATCHERFIELD' && $key ne 'Owner' && !$negative_op && (!$null_op || $subkey) ) {
+            if ( !$sub_tree{$key} ) {
+              $sub_tree{$key} = [ ('(')x$depth, \@res ];
+            } else {
+              $bundle_with = $self->_check_bundling_possibility( $string, @{ $sub_tree{$key} } );
+              if ( $sub_tree{$key}[-1] eq '(' ) {
+                    push @{ $sub_tree{$key} }, \@res;
+              }
+            }
+        }
+
+        # Remove our aggregator from subtrees where our condition didn't get added
+        pop @$_ foreach grep @$_ && $_->[-1] =~ /^(?:AND|OR)$/i, values %sub_tree;
+
+        # A reference to @res may be pushed onto $sub_tree{$key} from
+        # above, and we fill it here.
+        @res = $sub->( $self, $key, $op, $value,
+                SUBCLAUSE       => '',  # don't need anymore
+                ENTRYAGGREGATOR => $ea,
+                SUBKEY          => $subkey,
+                BUNDLE          => $bundle_with,
+              );
+        $ea = '';
+    };
+    RT::SQL::Parse($string, \%callback);
+}
+
+sub FromSQL {
+    my ($self,$query) = @_;
+
+    {
+        # preserve first_row and show_rows across the CleanSlate
+        local ($self->{'first_row'}, $self->{'show_rows'}, $self->{_sql_looking_at});
+        $self->CleanSlate;
+        $self->_InitSQL();
+    }
+
+    return (1, $self->loc("No Query")) unless $query;
+
+    $self->{_sql_query} = $query;
+    eval {
+        local $self->{parsing_assetsql} = 1;
+        $self->_parser( $query );
+    };
+    if ( $@ ) {
+        my $error = "$@";
+        $RT::Logger->error("Couldn't parse query: $error");
+        return (0, $error);
+    }
+
+    # We don't want deleted tickets unless 'allow_deleted_search' is set
+    unless( $self->{'allow_deleted_search'} ) {
+        $self->Limit(
+            FIELD    => 'Status',
+            OPERATOR => '!=',
+            VALUE => 'deleted',
+        );
+    }
+
+    # set SB's dirty flag
+    $self->{'must_redo_search'} = 1;
+    $self->{'RecalcAssetLimits'} = 0;
+    return (1, $self->loc("Valid Query"));
+}
+
+=head2 Query
+
+Returns the last string passed to L</FromSQL>.
+
+=cut
+
+sub Query {
+    my $self = shift;
+    return $self->{_sql_query};
+}
+
+=head2 ClearRestrictions
+
+Removes all restrictions irretrievably
+
+=cut
+
+sub ClearRestrictions {
+    my $self = shift;
+    delete $self->{'AssetRestrictions'};
+    $self->{_sql_looking_at} = {};
+    $self->{'RecalcAssetLimits'}      = 1;
+}
+
+# Convert a set of oldstyle SB Restrictions to Clauses for RQL
+
+sub _RestrictionsToClauses {
+    my $self = shift;
+
+    my %clause;
+    foreach my $row ( keys %{ $self->{'AssetRestrictions'} } ) {
+        my $restriction = $self->{'AssetRestrictions'}{$row};
+
+        # We need to reimplement the subclause aggregation that SearchBuilder does.
+        # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
+        # Then SB AND's the different Subclauses together.
+
+        # So, we want to group things into Subclauses, convert them to
+        # SQL, and then join them with the appropriate DefaultEA.
+        # Then join each subclause group with AND.
+
+        my $field = $restriction->{'FIELD'};
+        my $realfield = $field;    # CustomFields fake up a fieldname, so
+                                   # we need to figure that out
+
+        # One special case
+        # Rewrite LinkedTo meta field to the real field
+        if ( $field =~ /LinkedTo/ ) {
+            $realfield = $field = $restriction->{'TYPE'};
+        }
+
+        # Two special case
+        # Handle subkey fields with a different real field
+        if ( $field =~ /^(\w+)\./ ) {
+            $realfield = $1;
+        }
+
+        die "I don't know about $field yet"
+            unless ( exists $FIELD_METADATA{$realfield}
+                or $restriction->{CUSTOMFIELD} );
+
+        my $type = $FIELD_METADATA{$realfield}->[0];
+        my $op   = $restriction->{'OPERATOR'};
+
+        my $value = (
+            grep    {defined}
+                map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
+        )[0];
+
+        # this performs the moral equivalent of defined or/dor/C<//>,
+        # without the short circuiting.You need to use a 'defined or'
+        # type thing instead of just checking for truth values, because
+        # VALUE could be 0.(i.e. "false")
+
+        # You could also use this, but I find it less aesthetic:
+        # (although it does short circuit)
+        #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
+        # defined $restriction->{'TICKET'} ?
+        # $restriction->{TICKET} :
+        # defined $restriction->{'BASE'} ?
+        # $restriction->{BASE} :
+        # defined $restriction->{'TARGET'} ?
+        # $restriction->{TARGET} )
+
+        my $ea = $restriction->{ENTRYAGGREGATOR}
+            || $DefaultEA{$type}
+            || "AND";
+        if ( ref $ea ) {
+            die "Invalid operator $op for $field ($type)"
+                unless exists $ea->{$op};
+            $ea = $ea->{$op};
+        }
+
+        # Each CustomField should be put into a different Clause so they
+        # are ANDed together.
+        if ( $restriction->{CUSTOMFIELD} ) {
+            $realfield = $field;
+        }
+
+        exists $clause{$realfield} or $clause{$realfield} = [];
+
+        # Escape Quotes
+        $field =~ s!(['\\])!\\$1!g;
+        $value =~ s!(['\\])!\\$1!g;
+        my $data = [ $ea, $type, $field, $op, $value ];
+
+        # here is where we store extra data, say if it's a keyword or
+        # something.  (I.e. "TYPE SPECIFIC STUFF")
+
+        if (lc $ea eq 'none') {
+            $clause{$realfield} = [ $data ];
+        } else {
+            push @{ $clause{$realfield} }, $data;
+        }
+    }
+    return \%clause;
+}
+
+=head2 ClausesToSQL
+
+=cut
+
+sub ClausesToSQL {
+  my $self = shift;
+  my $clauses = shift;
+  my @sql;
+
+  for my $f (keys %{$clauses}) {
+    my $sql;
+    my $first = 1;
+
+    # Build SQL from the data hash
+    for my $data ( @{ $clauses->{$f} } ) {
+      $sql .= $data->[0] unless $first; $first=0; # ENTRYAGGREGATOR
+      $sql .= " '". $data->[2] . "' ";            # FIELD
+      $sql .= $data->[3] . " ";                   # OPERATOR
+      $sql .= "'". $data->[4] . "' ";             # VALUE
+    }
+
+    push @sql, " ( " . $sql . " ) ";
+  }
+
+  return join("AND", at sql);
+}
+
+sub _ProcessRestrictions {
+    my $self = shift;
+
+    delete $self->{'items_array'};
+    delete $self->{'item_map'};
+    delete $self->{'raw_rows'};
+    delete $self->{'count_all'};
+
+    my $sql = $self->Query;
+    if ( !$sql || $self->{'RecalcAssetLimits'} ) {
+
+        local $self->{using_restrictions};
+        #  "Restrictions to Clauses Branch\n";
+        my $clauseRef = eval { $self->_RestrictionsToClauses; };
+        if ($@) {
+            $RT::Logger->error( "RestrictionsToClauses: " . $@ );
+            $self->FromSQL("");
+        }
+        else {
+            $sql = $self->ClausesToSQL($clauseRef);
+            $self->FromSQL($sql) if $sql;
+        }
+    }
+
+    $self->{'RecalcAssetLimits'} = 0;
+
+}
+
+sub _check_bundling_possibility {
+    my $self = shift;
+    my $string = shift;
+    my @list = reverse @_;
+    while (my $e = shift @list) {
+        next if $e eq '(';
+        if ( lc($e) eq 'and' ) {
+            return undef;
+        }
+        elsif ( lc($e) eq 'or' ) {
+            return shift @list;
+        }
+        else {
+            # should not happen
+            $RT::Logger->error(
+                "Joins optimization failed when parsing '$string'. It's bug in RT, contact Best Practical"
+            );
+            die "Internal error. Contact your system administrator.";
+        }
+    }
+    return undef;
+}
+
+1;
+
diff --git a/patches/assetsql.patch b/patches/assetsql.patch
new file mode 100644
index 0000000..c841849
--- /dev/null
+++ b/patches/assetsql.patch
@@ -0,0 +1,43 @@
+diff --git a/share/html/Elements/CollectionList b/share/html/Elements/CollectionList
+index 5325b2e..42a2591 100644
+--- a/share/html/Elements/CollectionList
++++ b/share/html/Elements/CollectionList
+@@ -50,6 +50,10 @@ if (!$Collection && $Class eq 'RT::Tickets') {
+     $Collection = RT::Tickets->new( $session{'CurrentUser'} );
+     $Collection->FromSQL($Query);
+ }
++elsif (!$Collection && $Class eq 'RT::Assets') {
++    $Collection = RT::Assets->new( $session{'CurrentUser'} );
++    $Collection->FromSQL($Query);
++}
+ 
+ # flip HasResults from undef to 0 to indicate there was a search, so
+ # dashboard mail can be suppressed if there are no results
+diff --git a/share/html/Elements/ShowSearch b/share/html/Elements/ShowSearch
+index 15b7b75..4234387 100644
+--- a/share/html/Elements/ShowSearch
++++ b/share/html/Elements/ShowSearch
+@@ -52,7 +52,7 @@
+     titleright => $customize ? loc('Edit') : '',
+     titleright_href => $customize,
+     hideable => $hideable &>
+-<& $query_display_component, hideable => $hideable, %$ProcessedSearchArg, ShowNavigation => 0, Class => 'RT::Tickets', HasResults => $HasResults &>
++<& $query_display_component, hideable => $hideable, %$ProcessedSearchArg, ShowNavigation => 0, Class => $SearchArg && $SearchArg->{SearchType} eq 'Asset' ? 'RT::Assets' : 'RT::Tickets', HasResults => $HasResults &>
+ </&>
+ <%init>
+ my $search;
+@@ -76,7 +76,13 @@ if ($SavedSearch) {
+     }
+     $SearchArg->{'SavedSearchId'} ||= $SavedSearch;
+     $SearchArg->{'SearchType'} ||= 'Ticket';
+-    if ( $SearchArg->{SearchType} ne 'Ticket' ) {
++    if ( $SearchArg->{SearchType} eq 'Asset' ) {
++        $query_link_url = RT->Config->Get('WebPath') . "/Asset/Search/Results.html";
++        $customize = RT->Config->Get('WebPath') . '/Asset/Search/Build.html?'
++            . $m->comp( '/Elements/QueryString',
++            SavedSearchLoad => $SavedSearch );
++    }
++    elsif ( $SearchArg->{SearchType} ne 'Ticket' ) {
+ 
+         # XXX: dispatch to different handler here
+         $query_display_component
diff --git a/patches/rt-4.4.0-4.4.1.patch b/patches/rt-4.4.0-4.4.1.patch
new file mode 100644
index 0000000..4d0a92b
--- /dev/null
+++ b/patches/rt-4.4.0-4.4.1.patch
@@ -0,0 +1,123 @@
+diff --git a/lib/RT/Assets.pm b/lib/RT/Assets.pm
+index b52308a..e05a7ae 100644
+--- a/lib/RT/Assets.pm
++++ b/lib/RT/Assets.pm
+@@ -241,7 +241,7 @@ sub OrderByCols {
+     my $class = $self->_RoleGroupClass;
+ 
+     for my $row (@_) {
+-        if ($row->{FIELD} =~ /^CF\.(?:\{(.*)\}|(.*))$/) {
++        if ($row->{FIELD} =~ /^(?:CF|CustomField)\.(?:\{(.*)\}|(.*))$/) {
+             my $name = $1 || $2;
+             my $cf = RT::CustomField->new( $self->CurrentUser );
+             $cf->LoadByNameAndCatalog(
+diff --git a/share/html/Asset/Elements/AssetSearchBasics b/share/html/Asset/Elements/AssetSearchBasics
+index de9c35b..9557c8c 100644
+--- a/share/html/Asset/Elements/AssetSearchBasics
++++ b/share/html/Asset/Elements/AssetSearchBasics
+@@ -54,7 +54,7 @@
+ </td></tr>
+ <tr class="asset-status"><td class="label"><label for="Status"><&|/l&>Status</&></label></td>
+     <td class="value" colspan="3">
+-<& /Asset/Elements/SelectStatus, Name => 'Status', CatalogObj => $CatalogObj, DefaultValue => 1,
++<& /Asset/Elements/SelectStatus, Name => 'Status', Catalogs => { $CatalogObj->id => 1 }, DefaultValue => 1,
+        Default => ($ARGS{'Status'} || '') &>
+ </td></tr>
+ <tr class="asset-name"><td class="label"><label for="Name"><&|/l&>Name</&></label></td>
+diff --git a/share/html/Asset/Elements/SelectCatalog b/share/html/Asset/Elements/SelectCatalog
+index aa5d5fc..8c65e46 100644
+--- a/share/html/Asset/Elements/SelectCatalog
++++ b/share/html/Asset/Elements/SelectCatalog
+@@ -48,7 +48,7 @@
+ <& /Elements/SelectObject,
+     Name           => "Catalog",
+     ShowAll        => $ShowAll,
+-    ShowNullOption => 0,
++    ShowNullOption => $ShowNullOption,
+     CheckRight     => "CreateAsset",
+     %ARGS,
+     ObjectType     => "Catalog",
+@@ -59,6 +59,7 @@
+ $ShowAll => 0
+ $Default => undef
+ $UpdateSession => 1
++$ShowNullOption => 0
+ </%args>
+ <%init>
+ my $catalog_obj = LoadDefaultCatalog($Default || '');
+diff --git a/share/html/Asset/Elements/SelectStatus b/share/html/Asset/Elements/SelectStatus
+index ef3e826..99717da 100644
+--- a/share/html/Asset/Elements/SelectStatus
++++ b/share/html/Asset/Elements/SelectStatus
+@@ -45,22 +45,27 @@
+ %# those contributions and any derivatives thereof.
+ %#
+ %# END BPS TAGGED BLOCK }}}
+-<& /Elements/SelectStatus, %ARGS &>
++<& /Elements/SelectStatus, %ARGS, Type => 'asset', Object => $AssetObj && $AssetObj->id ? $AssetObj : $CatalogObj, Lifecycles => \@Lifecycles &>
+ <%init>
+-if ($AssetObj and $AssetObj->Id) {
++my @Lifecycles;
++for my $id (keys %Catalogs) {
++    my $catalog = RT::Catalog->new($session{'CurrentUser'});
++    $catalog->Load($id);
++    push @Lifecycles, $catalog->LifecycleObj if $catalog->id;
++}
++
++if ($AssetObj && $AssetObj->id) {
+     $ARGS{DefaultValue} = 0;
+     $ARGS{Default} = $DECODED_ARGS->{Status} || $ARGS{Default};
+     $ARGS{Object} = $AssetObj;
+-} else {
+-    my $lifecycle = ($CatalogObj || "RT::Catalog")->LifecycleObj;
+-    if ( not $ARGS{DefaultValue} ){
+-        $ARGS{DefaultValue} = 0;
+-        $ARGS{Default} ||= $DECODED_ARGS->{Status} || $lifecycle->DefaultOnCreate;
+-    }
+-    $ARGS{Statuses} = [ $AssetObj ? $lifecycle->Transitions("") : $lifecycle->Valid ];
++} elsif ( $CatalogObj ) {
++    my $lifecycle = $CatalogObj->LifecycleObj;
++    $ARGS{DefaultValue} = 0;
++    $ARGS{Default} ||= $DECODED_ARGS->{Status} || $lifecycle->DefaultOnCreate;
+ }
+ </%init>
+ <%args>
+ $AssetObj   => undef
+ $CatalogObj => undef
++%Catalogs => ()
+ </%args>
+diff --git a/share/html/Elements/SelectStatus b/share/html/Elements/SelectStatus
+index 7eb678d..3183a90 100644
+--- a/share/html/Elements/SelectStatus
++++ b/share/html/Elements/SelectStatus
+@@ -94,7 +94,7 @@ if ( @Statuses ) {
+     }
+ 
+     if (not keys %statuses_by_lifecycle) {
+-        for my $lifecycle (map { RT::Lifecycle->Load($_) } RT::Lifecycle->List($Type)) {
++        for my $lifecycle (map { RT::Lifecycle->Load(Type => $Type, Name => $_) } RT::Lifecycle->List($Type)) {
+             $statuses_by_lifecycle{$lifecycle->Name} = [ $lifecycle->Valid ];
+         }
+     }
+diff --git a/share/html/Elements/Tabs b/share/html/Elements/Tabs
+index f4ac8a9..e672e3b 100644
+--- a/share/html/Elements/Tabs
++++ b/share/html/Elements/Tabs
+@@ -1107,7 +1107,7 @@ my $build_main_nav = sub {
+         PageMenu()->child( edit => title => loc('Edit'), path => '/Prefs/MyRT.html' );
+     }
+ 
+-    $m->callback( CallbackName => 'Privileged', Path => $request_path );
++    $m->callback( CallbackName => 'Privileged', Path => $request_path, ARGSRef => \%ARGS );
+ };
+ 
+ my $build_selfservice_nav = sub {
+@@ -1174,7 +1174,7 @@ my $build_selfservice_nav = sub {
+         }
+     }
+ 
+-    $m->callback( CallbackName => 'SelfService', Path => $request_path );
++    $m->callback( CallbackName => 'SelfService', Path => $request_path, ARGSRef => \%ARGS );
+ };
+ 
+ 
diff --git a/static/css/assetsql.css b/static/css/assetsql.css
new file mode 100644
index 0000000..e254f9d
--- /dev/null
+++ b/static/css/assetsql.css
@@ -0,0 +1,7 @@
+#comp-Asset-Search-Build #body {
+    position: relative;
+}
+
+#comp-Asset-Search-Build #pick-criteria {
+    min-height: 400px;
+}

-----------------------------------------------------------------------


More information about the Bps-public-commit mailing list