[Rt-commit] rt branch, 5.0-trunk, updated. rt-5.0.1-322-g1247618275

? sunnavy sunnavy at bestpractical.com
Tue Apr 27 16:06:20 EDT 2021


The branch, 5.0-trunk has been updated
       via  124761827549a8fa0c1e161272ec65c9e161fea2 (commit)
       via  14f6df2a42fd5c70126dda95a9d91dafd3626af7 (commit)
       via  2deffa759040895294cb26b24bc0b175efe5d9ef (commit)
       via  ab20fc32315a53f8f8e569ee16a687c3da7fffb3 (commit)
       via  2745b9ff057b2a940c1c460432fe71cec18e3864 (commit)
       via  71b9b929938bc17ea43a6def275d5fea1c5702ce (commit)
       via  4b5e8129a80f2d0a9a4d9e78eb6f382ccd6f4dba (commit)
       via  9d8ce46fb06607dd4c20e500a90743e47fd3209e (commit)
       via  8a291a52d822687f5a196a9169514816fd81ce60 (commit)
       via  2d292484f9825fe9dd05765350442e956bf2fb2e (commit)
       via  aa3a7ed6daf222ff5ad251342a647a5cdd1f66aa (commit)
       via  5b0950b6636890f259d466d6ea0c7bf0ea7338a3 (commit)
       via  6cd352d52a88111f1768ab7cb8a36d55b3a702bd (commit)
       via  cb6805b3cd3d9b32570556601cb6bf1deb0b90cb (commit)
       via  e926e786c5f97e2df5802a782cbce4a9a77f459a (commit)
       via  767e23fbd24b1bff47165f7b829a23a5ff6582d2 (commit)
       via  58e967c9e00fa136134fd8762e7c51f67d2b77db (commit)
       via  db6d58448576fe03e53118951cb255dd9e7a25d0 (commit)
       via  4a08a88e689c69f5d39f510a789aa9eec521e803 (commit)
       via  e05754cabb28164c31d04890a341d01d615cd8d2 (commit)
       via  012ac6e611e5c20559b7f74364aa4139af197f6a (commit)
       via  79c2925b5c81080a49417c59c47fdf49d101b567 (commit)
       via  6d5d520915c0ee1ee7a06dcd722329e83d8f827f (commit)
       via  643ac659c4c2c396942bac62d2e8ee89ddf22fff (commit)
       via  5901632e6354294ce901687b8cfaa111080105aa (commit)
       via  d60a62f88fe2e947d3aaaa9585be2c55bb89b3b7 (commit)
       via  38eb2962bc35c77c4fc08c46fd4211858da8d033 (commit)
       via  4b091dc84cd2291c2d6f9cc35eec2ce5329f17e3 (commit)
       via  66826c0807aaf26f4d011a4bfd2bba937907981a (commit)
       via  00ebd887cfe75274fc70bbfc1d33f0b279a73e5d (commit)
       via  db236a9dc65e745e5e22dd73ff99e1a7a98e5277 (commit)
       via  382bb28bd636a255c955374f53a0e15c35342fc8 (commit)
       via  9ecab55eebf71de8c6214126835a367b433edacd (commit)
       via  f6ce56f0bf61a9f5b4222935f015238be6042486 (commit)
       via  acfd5c9bebc61be9dea831d2effa656742913114 (commit)
       via  25d25be0dfe58855404ac707d4815d43e1af6680 (commit)
       via  36ed97239472dc23165410e33f3eba68d3cc3c70 (commit)
       via  670438e9a16ed7bae276f83ee6c94888f6cecf74 (commit)
       via  eb7a8d9b4b37bc5094bbf270786778150fc3fc16 (commit)
       via  8c94c44c88fb39ed8117674e5b16b28a9320b457 (commit)
       via  7da576b39b6187b663608705e224ca146a316e7a (commit)
       via  525375e10a895e90b0acb9862da64d8f4561cf68 (commit)
       via  353f384743508ef6d54e1eda8b8e5a416dfc977e (commit)
       via  ce3a983904b29bc4294d4681732d58ff12bed426 (commit)
       via  dd1ff8a22d328f295a0bfe1cf085fe3d60335de6 (commit)
       via  1f7b35682ab8f55942125ceada25f7d70a5856c9 (commit)
       via  3c77c68728983d0088782c08c81ca0c07f18effe (commit)
       via  6e9a2e3efff6232741743bc2b395fe916a637a22 (commit)
       via  a2f9bb615359558ea167ae762874c5f2185d32ed (commit)
       via  14054242e1ab8a909b65fb625a2419e494494fa6 (commit)
       via  8f85a19545202aafdf16a139547e739519b2c7c2 (commit)
       via  1315912cc222c21197182624222385e9bf3e00f6 (commit)
       via  fd088d2da671116161177e84141cf068fde66106 (commit)
       via  74d55055de6b6370d5836593078caddfb82af1ce (commit)
       via  5a1f467bbf7a9c934c71c487d57e8bedd3cda727 (commit)
       via  94d0b0367308cd770749a70c5abe664b78a8797c (commit)
       via  07293a6cbf2591f8c079e86a411e42c287e007b2 (commit)
       via  67141afacbc4f36e72e9aa1b847e68978259e31e (commit)
       via  ac04f3b68f2bcf75a95d628e5c8a3edab0174df5 (commit)
       via  55a0578457fad781072fba6209aac3b263599990 (commit)
       via  567134c1dff7f55c902ac466c05eb2d1b0bda0d9 (commit)
       via  f428d696d39b519a2922f22643f4581fcfdd146d (commit)
       via  064f429bf4ad51be9ac706715f7cfc2b6b24dad0 (commit)
       via  901b59900759c8bd3317a2fef61b7e793b003343 (commit)
       via  9691d1313c52a36cae7834c3928bdcd16904a686 (commit)
       via  917a486dd9e72d9d488a0483ee3617dc7b3e6adb (commit)
       via  c5a121b8f3f3331594acb035e4b5a4df862ccf67 (commit)
       via  c8f4e9d865f670b20e1c8afac3da554b6979e46f (commit)
       via  df2dd2e3bf8b20a474fccd712bc8712acde0986d (commit)
       via  015dee4c2653f6bfb4ae51152007553d1a5dd04f (commit)
       via  3ec4a98e05aa1164ab49bfe79aa9f40fdb7f0531 (commit)
       via  8badffca3dcd3a21029b14b718d6d2260f200de0 (commit)
       via  d2b5c61bd6e9f92d52f78fd6cf70fc87598bac1d (commit)
       via  4f343de68f8a90cbf7969cadbfa9bc666a85ecf4 (commit)
       via  e9a93d456b599ebdd8a874631d1b382cff2855a2 (commit)
       via  d0f17c654557996ccc35f81529468820fa0754fa (commit)
       via  ac6ee1a45f4fbbff26590c5a3a4e925457a46c2d (commit)
      from  286ef1fe7472c878b2c65e516ab325323a0071d9 (commit)

Summary of changes:
 README.PostgreSQL                                  |  12 +
 docs/UPGRADING-4.4                                 |  18 ++
 docs/customizing/articles_introduction.pod         |  15 ++
 docs/query_builder.pod                             | 110 ++++++--
 etc/upgrade/4.4.5/content                          |  15 ++
 etc/upgrade/shrink-cgm-table.in                    |  39 ++-
 lib/RT/Authen/ExternalAuth/LDAP.pm                 |   3 +
 lib/RT/CachedGroupMember.pm                        |   2 +-
 lib/RT/Condition/SLA_RequireStartsSet.pm           |   8 +-
 lib/RT/Crypt/GnuPG.pm                              |  66 +++++
 lib/RT/CustomRole.pm                               |  20 +-
 lib/RT/Date.pm                                     |  13 +-
 lib/RT/EmailParser.pm                              |  74 ++++--
 lib/RT/Group.pm                                    |  51 +++-
 lib/RT/GroupMember.pm                              |  14 +
 lib/RT/Interface/Web/MenuBuilder.pm                |  10 +
 lib/RT/Interface/Web/QueryBuilder/Tree.pm          |   4 +
 lib/RT/ObjectCustomRole.pm                         |  13 +-
 lib/RT/Principal.pm                                |  81 +++++-
 lib/RT/Record/Role/Roles.pm                        | 129 ++++++---
 lib/RT/SQL.pm                                      |   2 +-
 lib/RT/SearchBuilder.pm                            |   4 +-
 lib/RT/SearchBuilder/Role/Roles.pm                 |  72 +++++-
 lib/RT/Ticket.pm                                   |  13 +-
 lib/RT/Tickets.pm                                  | 220 +++++++++++++---
 lib/RT/Users.pm                                    |  36 ++-
 sbin/rt-validator.in                               |   4 +
 share/html/Admin/Articles/Classes/Modify.html      |  17 ++
 share/html/Admin/Elements/EditRights               |  10 +
 share/html/Admin/Groups/Members.html               |   5 +
 share/html/Admin/Queues/People.html                |   3 +-
 share/html/Admin/Tools/GnuPG.html                  | 287 +++++++++++++++++++++
 share/html/Articles/Elements/BeforeMessageBox      |   2 +-
 share/html/Asset/Elements/EditPeople               |   2 +-
 share/html/Elements/EmailInput                     |   3 +-
 share/html/Elements/Header                         |   2 +
 .../MultiUserRoleInput}                            |  20 +-
 share/html/Elements/QuickCreate                    |   2 +-
 share/html/Elements/SelectMatch                    |  21 +-
 share/html/Elements/ShowCustomFields               |   6 +-
 share/html/Elements/ShowHistoryHeader              |   2 +-
 share/html/Elements/ShowTransaction                |   2 +
 .../Helpers/Autocomplete/{Users => Principals}     |  63 +++--
 share/html/Helpers/Autocomplete/Users              |   4 +
 share/html/Prefs/Other.html                        |   6 +-
 share/html/Reports/CreatedByDates.html             |   4 +
 share/html/Reports/ResolvedByDates.html            |   8 +
 share/html/Reports/ResolvedByOwner.html            |   4 +
 share/html/Search/Bulk.html                        |  16 +-
 share/html/Search/Elements/PickBasics              |   1 +
 share/html/Search/Elements/PickCustomRoles         |   1 +
 share/html/SelfService/Create.html                 |   4 +-
 share/html/Ticket/Create.html                      |  31 ++-
 share/html/Ticket/Elements/AddWatchers             |   2 +-
 share/html/Ticket/Elements/ScrollShowHistory       |   7 +
 share/html/Ticket/Elements/ShowSummary             |   4 +
 share/html/Ticket/Update.html                      |  23 +-
 share/html/m/ticket/create                         |  23 +-
 share/html/m/ticket/reply                          |  23 +-
 share/static/js/autocomplete.js                    |   7 +-
 t/api/date.t                                       |  12 +
 t/api/group.t                                      |  65 +++++
 t/api/rights.t                                     |  80 +++++-
 t/api/sql.t                                        |  37 +++
 t/api/tickets.t                                    |  79 ++++++
 t/crypt/smime/crl-check.t                          |   4 +-
 t/crypt/smime/revoked.t                            |   4 +-
 t/customroles/rights.t                             |  10 +-
 t/shredder/02group_member.t                        |  22 +-
 t/sla/starts.t                                     |  35 +++
 t/validator/group_members.t                        |  17 ++
 t/web/admin_gnupg.t                                |  68 +++++
 t/web/ticket_role_input.t                          | 173 +++++++++++++
 73 files changed, 1957 insertions(+), 312 deletions(-)
 create mode 100644 README.PostgreSQL
 create mode 100644 share/html/Admin/Tools/GnuPG.html
 copy share/html/{Admin/Helpers/ClearMasonCache => Elements/MultiUserRoleInput} (87%)
 copy share/html/Helpers/Autocomplete/{Users => Principals} (73%)
 create mode 100644 t/api/sql.t
 create mode 100644 t/web/admin_gnupg.t
 create mode 100644 t/web/ticket_role_input.t

- Log -----------------------------------------------------------------
commit 124761827549a8fa0c1e161272ec65c9e161fea2
Merge: 286ef1fe74 14f6df2a42
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Tue Apr 27 05:56:12 2021 +0800

    Merge branch '4.4-trunk' into 5.0-trunk

diff --cc docs/query_builder.pod
index 1c66d6e89a,eb13e8c915..7e2be2ba36
--- a/docs/query_builder.pod
+++ b/docs/query_builder.pod
@@@ -140,73 -139,105 +140,145 @@@ The same pieces of information are now 
  another, which can be harder to read. So when you tell RT to display a lot of
  columns, it's usually worth adding a well-placed NEWLINE.
  
- =head1 Custom Field Searches
+ =head1 Searching for Users on Tickets
  
- Users often add custom fields to tickets to capture additional important information.
- All of these fields can be searched in the Query Builder as well. Global custom fields
- will show up by default when you start a search. To see custom fields that are applied
- to individual queues, first add the queue to your search and you'll then see the
- custom fields appear in the bottom of the Add Criteria section.
+ The Query Builder provides ways to search for tickets based on how a
+ given user is related to the ticket. This works for all ticket roles,
+ so you can find all active tickets where a given user is the Requestor,
+ meaning they opened the ticket. You can also search for all tickets
+ with a given user as the owner, meaning that's what they are working on.
  
- For example, you might have a custom field named "Transport Type" with values
- like "Car", "Bus" or "Train". You can easily build a search to show just tickets
- with a Transport Type of Train for some time period by selecting those options
- in the custom field entry.
+ =head2 Groups on Roles
  
- =head2 Custom Field Searches and Null Values
+ Ticket roles, except for Owner, can also accept a group as a value.
+ When you search for a user, by default the Query Builder does a
+ recursive or "deep" search, meaning it searches the role and it checks
+ the membership of any groups it finds in the role as well.
  
- There is a special case if you want to search for tickets with no value, called
- a "Null" value, for a custom field. If you search for all tickets that
- do not have Transport Type set to "Car", this results in a list of tickets
- with Transport Type values of 'Bus', 'Train', and '(no value)'.
+ For example, assume you have a group "Helpdesk", the staff1 user is a
+ member, and Helpdesk is set as an AdminCc on a ticket. If you perform
+ this search:
  
- If what you intended was to show all tickets that have a value and that value
- is not "Car", you can clarify your query to get the correct results. To filter
- out the empty values, add the following to your search query:
+     AdminCc.Name = 'staff1'
  
-     AND CF.{'Transport Type'} IS NOT NULL
+ the results will contain that ticket.
+ 
+ =head2 Shallow Searches
+ 
+ In some cases you may want to see tickets only when the user you are
+ searching for is a direct member of a role and not if they are in
+ any groups. For that case, use the "shallow" options, like "shallow is"
+ to generate a search like this:
+ 
+     AdminCc.Name SHALLOW = 'staff1'
+ 
+ That will show tickets where 'staff1' is directly in the AdminCc role,
+ but not the ticket from the previous example where staff1 is in the
+ Helpdesk group on the AdminCc role.
+ 
+ =head1 Searching Custom Fields on Tickets
+ 
+ If you use custom fields on your tickets, you might initially load the Query
+ Builder and not see them listed. If your custom fields are applied to
+ individual queues and not globally, they won't show up as options on a
+ new search. However, once you add a queue to your search, as shown above, the
+ custom fields applied to the queue or queues in your search will be displayed.
+ You can then add custom field values to your search just like other fields.
+ 
+ In addition to exact queue name searches like "Queue = 'RT'", the query builder
+ also allows 'LIKE' searches. This can be useful if you have several queues that
+ follow some naming pattern, like "IT Support", "Printer Support", and "Email Support".
+ To include all of these, you can use "Queue LIKE 'Support'".
+ 
+ Using the Advanced tab, you can also type one other criteria to search for queues
+ by their lifecycle. TicketSQL supports "Lifecycle = 'support'" if you type it
+ directly into the Advanced search box.
+ 
+ =head2 Basic Custom Field Searches
+ 
+ Once you add a queue to your search using one of the options above, you'll see
+ any custom fields applied to those queues at the bottom of the list of values in
+ the Add Criteria section. To search for all tickets with a custom field that
+ has some value, pick the comparision ("matches", "is", etc.), add a value,
+ and click "Add these terms" to build the search.
+ 
+ If you have a custom field named "Transport Type", for example, that has
+ the values, "Car", "Bus" or "Train", you can search for all tickets that
+ reference cars by picking "is" next to "Transport Type", typing "Car"
+ and clicking Add these terms. You should then see a search term like:
+ 
+     'CF.{Transport Type}' = 'Car'
+ 
+ For custom fields that have a dropdown with values to pick from a list,
+ the Query Builder will show the available values as a dropdown and
+ you can pick from the options without typing a value.
+ 
+ When searching for custom fields, you can also add them as a column in
+ your search results as described above.
+ 
+ =head2 Searching for Empty Custom Fields
+ 
+ Sometimes you want to search for all tickets where a given custom
+ field doesn't have a particular value. For example, you might search
+ for all tickets where "Transport Type" is not set to "Car" using the
+ "isn't" option, and that would create:
+ 
+     'CF.{Transport Type}' != 'Car'
+ 
+ The search would contain all tickets in that queue with "Transport Type"
+ values of "Bus", "Train", and no value.
+ 
+ To remove tickets where Transport Type has no set value, add the
+ following to your query:
+ 
+     AND 'CF.{Transport Type}' IS NOT NULL
+ 
+ Similarly, to see all tickets where "Transport Type" hasn't be set,
+ you can search for :
+ 
+     'CF.{Transport Type}' IS NULL
  
 -=cut
 +=head1 Transaction Query Builder
 +
 +Similar to the Ticket Query Builder, the Transaction Query Builder provides an
 +interface to search for individual transactions. Transactions are all of the
 +changes made to a ticket through its life. Each of the entries displayed in the
 +ticket history at the bottom of the ticket display page is a transaction.
 +
 +In some cases, RT users looking for a particular reply on a ticket will
 +search in their email client rather than in RT because they will remenber
 +getting the email with the information they need. On a busy ticket, it
 +can be a challenge to find the reply from Jane some time this week. The
 +Transaction Query Builder now makes that sort of search easy.
 +
 +=head2 Basic Transaction Searches
 +
 +In the example above, suppose you remember getting a reply from Jane in email
 +on a ticket and you know it was in the last week. But it's been a busy week
 +and Jane is on a bunch of active tickets, so you're not sure where to start.
 +With the Transaction Query Builder, you can easily create a search to show all
 +replies from Jane.
 +
 +First find Creator, select "is", and type Jane's username. The "Creator" of a
 +transaction is always the person who made the change. For a reply, by email or
 +in RT itself, the person who replied will be the Creator of the transaction.
 +
 +Next, for Created select "after" and type "1 week ago". RT will then automatically
 +figure out the date 7 days ago and show you only results in the last 7 days.
 +
 +Finally for Type select "is" and select "Correspond". Correspond is the name RT
 +users internally for all replies on a ticket.
 +
 +Run the search and you'll see all replies from Jane on any tickets over the
 +last week. Note that you'll see all transactions you have rights to see, even
 +if you aren't a watcher and possibly didn't get an email originally.
 +
 +=head2 Including Ticket Information
 +
 +When searching for transactions, you can also add criteria about the types of
 +tickets the transactions should be on. In our example, we probably only want
 +to see active tickets, so in the bottom Ticket Fields section you can select
 +Status "is" and "Active". This will then filter out inactive statuses.
  
  =head1 Learn More
  
diff --cc lib/RT/Interface/Web/MenuBuilder.pm
index 781565c043,0000000000..7c72d3ea07
mode 100644,000000..100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@@ -1,1700 -1,0 +1,1710 @@@
 +# BEGIN BPS TAGGED BLOCK {{{
 +#
 +# COPYRIGHT:
 +#
 +# This software is Copyright (c) 1996-2021 Best Practical Solutions, LLC
 +#                                          <sales at bestpractical.com>
 +#
 +# (Except where explicitly superseded by other copyright notices)
 +#
 +#
 +# LICENSE:
 +#
 +# This work is made available to you under the terms of Version 2 of
 +# the GNU General Public License. A copy of that license should have
 +# been provided with this software, but in any event can be snarfed
 +# from www.gnu.org.
 +#
 +# This work is distributed in the hope that it will be useful, but
 +# WITHOUT ANY WARRANTY; without even the implied warranty of
 +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 +# General Public License for more details.
 +#
 +# You should have received a copy of the GNU General Public License
 +# along with this program; if not, write to the Free Software
 +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 +# 02110-1301 or visit their web page on the internet at
 +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
 +#
 +#
 +# CONTRIBUTION SUBMISSION POLICY:
 +#
 +# (The following paragraph is not intended to limit the rights granted
 +# to you to modify and distribute this software under the terms of
 +# the GNU General Public License and is only of importance to you if
 +# you choose to contribute your changes and enhancements to the
 +# community by submitting them to Best Practical Solutions, LLC.)
 +#
 +# By intentionally submitting any modifications, corrections or
 +# derivatives to this work, or any other work intended for use with
 +# Request Tracker, to Best Practical Solutions, LLC, you confirm that
 +# you are the copyright holder for those contributions and you grant
 +# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
 +# royalty-free, perpetual, license to use, copy, create derivative
 +# works based on those contributions, and sublicense and distribute
 +# those contributions and any derivatives thereof.
 +#
 +# END BPS TAGGED BLOCK }}}
 +
 +=head1 NAME
 +
 +RT::Interface::Web::MenuBuilder
 +
 +=cut
 +
 +use strict;
 +use warnings;
 +
 +package RT::Interface::Web::MenuBuilder;
 +
 +sub loc { HTML::Mason::Commands::loc( @_ ); }
 +
 +sub QueryString {
 +    my %args = @_;
 +    my $u    = URI->new();
 +    $u->query_form(map { $_ => $args{$_} } sort keys %args);
 +    return $u->query;
 +}
 +
 +sub BuildMainNav {
 +    my $request_path = shift;
 +    my $top          = shift;
 +    my $widgets      = shift;
 +    my $page         = shift;
 +
 +    my %args = ( @_ );
 +
 +    my $query_string = $args{QueryString};
 +    my $query_args = $args{QueryArgs};
 +
 +    my $current_user = $HTML::Mason::Commands::session{CurrentUser};
 +
 +    if ($request_path =~ m{^/Asset/}) {
 +        $widgets->child( asset_search => raw_html => $HTML::Mason::Commands::m->scomp('/Asset/Elements/Search') );
 +        $widgets->child( create_asset => raw_html => $HTML::Mason::Commands::m->scomp('/Asset/Elements/CreateAsset') );
 +    }
 +    elsif ($request_path =~ m{^/Articles/}) {
 +        $widgets->child( article_search => raw_html => $HTML::Mason::Commands::m->scomp('/Articles/Elements/GotoArticle') );
 +        $widgets->child( create_article => raw_html => $HTML::Mason::Commands::m->scomp('/Articles/Elements/CreateArticleButton') );
 +    } else {
 +        $widgets->child( simple_search => raw_html => $HTML::Mason::Commands::m->scomp('SimpleSearch', Placeholder => loc('Search Tickets')) );
 +        $widgets->child( create_ticket => raw_html => $HTML::Mason::Commands::m->scomp('CreateTicket') );
 +    }
 +
 +    my $home = $top->child( home => title => loc('Homepage'), path => '/' );
 +    $home->child( create_ticket => title => loc("Create Ticket"),
 +                  path => "/Ticket/Create.html" );
 +
 +    my $search = $top->child( search => title => loc('Search'), path => '/Search/Simple.html' );
 +
 +    my $tickets = $search->child( tickets => title => loc('Tickets'), path => '/Search/Build.html' );
 +    $tickets->child( simple => title => loc('Simple Search'), path => "/Search/Simple.html" );
 +    $tickets->child( new    => title => loc('New Search'),    path => "/Search/Build.html?NewQuery=1" );
 +
 +    my $recents = $tickets->child( recent => title => loc('Recently Viewed'));
 +    for my $ticket ( $current_user->RecentlyViewedTickets ) {
 +        my $title = $ticket->{subject} || loc( "(No subject)" );
 +        if ( length $title > 50 ) {
 +            $title = substr($title, 0, 47);
 +            $title =~ s/\s+$//;
 +            $title .= "...";
 +        }
 +        $title = "#$ticket->{id}: " . $title;
 +        $recents->child( "$ticket->{id}" => title => $title, path => "/Ticket/Display.html?id=" . $ticket->{id} );
 +    }
 +
 +    $search->child( articles => title => loc('Articles'),   path => "/Articles/Article/Search.html" )
 +        if $current_user->HasRight( Right => 'ShowArticlesMenu', Object => RT->System );
 +
 +    $search->child( users => title => loc('Users'),   path => "/User/Search.html" );
 +
 +    $search->child( groups      =>
 +                    title       => loc('Groups'),
 +                    path        => "/Group/Search.html",
 +                    description => 'Group search'
 +    );
 +
 +    my $search_assets;
 +    if ($HTML::Mason::Commands::session{CurrentUser}->HasRight( Right => 'ShowAssetsMenu', Object => RT->System )) {
 +        $search_assets = $search->child( assets => title => loc("Assets"), path => "/Search/Build.html?Class=RT::Assets" );
 +        if (!RT->Config->Get('AssetHideSimpleSearch')) {
 +            $search_assets->child("asset_simple", title => loc("Simple Search"), path => "/Asset/Search/");
 +        }
 +        $search_assets->child("assetsql", title => loc("New Search"), path => "/Search/Build.html?Class=RT::Assets;NewQuery=1");
 +    }
 +
 +
 +    my $txns = $search->child( transactions => title => loc('Transactions'), path => '/Search/Build.html?Class=RT::Transactions;ObjectType=RT::Ticket' );
 +    my $txns_tickets = $txns->child( tickets => title => loc('Tickets'), path => "/Search/Build.html?Class=RT::Transactions;ObjectType=RT::Ticket" );
 +    $txns_tickets->child( new => title => loc('New Search'), path => "/Search/Build.html?Class=RT::Transactions;ObjectType=RT::Ticket;NewQuery=1" );
 +
 +    my $reports = $top->child( reports =>
 +        title       => loc('Reports'),
 +        description => loc('Reports and Dashboards'),
 +        path        => loc('/Reports'),
 +    );
 +
 +    unless ($HTML::Mason::Commands::session{'dashboards_in_menu'}) {
 +        my $dashboards_in_menu = $current_user->UserObj->Preferences(
 +            'DashboardsInMenu',
 +            {},
 +        );
 +
 +        unless ($dashboards_in_menu->{dashboards}) {
 +            my ($default_dashboards) =
 +                RT::System->new( $current_user )
 +                    ->Attributes
 +                    ->Named('DashboardsInMenu');
 +            if ($default_dashboards) {
 +                $dashboards_in_menu = $default_dashboards->Content;
 +            }
 +        }
 +
 +        $HTML::Mason::Commands::session{'dashboards_in_menu'} = $dashboards_in_menu->{dashboards} || [];
 +    }
 +
 +    my @dashboards;
 +    for my $id ( @{$HTML::Mason::Commands::session{'dashboards_in_menu'}} ) {
 +        my $dash = RT::Dashboard->new( $current_user );
 +        my ( $status, $msg ) = $dash->LoadById($id);
 +        if ( $status ) {
 +            push @dashboards, $dash;
 +        } else {
 +            $RT::Logger->debug( "Failed to load dashboard $id: $msg, removing from menu" );
 +            $home->RemoveDashboardMenuItem(
 +                DashboardId => $id,
 +                CurrentUser => $HTML::Mason::Commands::session{CurrentUser}->UserObj,
 +            );
 +            @{ $HTML::Mason::Commands::session{'dashboards_in_menu'} } =
 +              grep { $_ != $id } @{ $HTML::Mason::Commands::session{'dashboards_in_menu'} };
 +        }
 +    }
 +
 +    if (@dashboards) {
 +        for my $dash (@dashboards) {
 +            $reports->child( 'dashboard-' . $dash->id,
 +                title => $dash->Name,
 +                path  => '/Dashboards/' . $dash->id . '/' . $dash->Name
 +            );
 +        }
 +    }
 +
 +    # Get the list of reports in the Reports menu
 +    unless (   $HTML::Mason::Commands::session{'reports_in_menu'}
 +            && ref( $HTML::Mason::Commands::session{'reports_in_menu'}) eq 'ARRAY'
 +            && @{$HTML::Mason::Commands::session{'reports_in_menu'}}
 +           ) {
 +        my $reports_in_menu = $current_user->UserObj->Preferences(
 +            'ReportsInMenu',
 +            {},
 +        );
 +        unless ( $reports_in_menu && ref $reports_in_menu eq 'ARRAY' ) {
 +            my ($default_reports) =
 +                RT::System->new( RT->SystemUser )
 +                    ->Attributes
 +                    ->Named('ReportsInMenu');
 +            if ($default_reports) {
 +                $reports_in_menu = $default_reports->Content;
 +            }
 +            else {
 +                $reports_in_menu = [];
 +            }
 +        }
 +
 +        $HTML::Mason::Commands::session{'reports_in_menu'} = $reports_in_menu || [];
 +    }
 +
 +    for my $report ( @{$HTML::Mason::Commands::session{'reports_in_menu'}} ) {
 +        $reports->child(  $report->{id} =>
 +            title       => $report->{title},
 +            path        => $report->{path},
 +        );
 +    }
 +
 +    $reports->child( edit => title => loc('Update This Menu'), path => '/Prefs/DashboardsInMenu.html' );
 +    $reports->child( more => title => loc('All Dashboards'),   path => '/Dashboards/index.html' );
 +    my $dashboard = RT::Dashboard->new( $current_user );
 +    if ( $dashboard->CurrentUserCanCreateAny ) {
 +        $reports->child('dashboard_create' => title => loc('New Dashboard'), path => "/Dashboards/Modify.html?Create=1" );
 +    }
 +
 +
 +    if ($current_user->HasRight( Right => 'ShowArticlesMenu', Object => RT->System )) {
 +        my $articles = $top->child( articles => title => loc('Articles'), path => "/Articles/index.html");
 +        $articles->child( articles => title => loc('Overview'), path => "/Articles/index.html" );
 +        $articles->child( topics   => title => loc('Topics'),   path => "/Articles/Topics.html" );
 +        $articles->child( create   => title => loc('Create'),   path => "/Articles/Article/PreCreate.html" );
 +        $articles->child( search   => title => loc('Search'),   path => "/Articles/Article/Search.html" );
 +    }
 +
 +    if ($current_user->HasRight( Right => 'ShowAssetsMenu', Object => RT->System )) {
 +        my $assets = $top->child(
 +            "assets",
 +            title => loc("Assets"),
 +            path  => RT->Config->Get('AssetHideSimpleSearch') ? "/Search/Build.html?Class=RT::Assets;NewQuery=1" : "/Asset/Search/",
 +        );
 +        $assets->child( "create", title => loc("Create"), path => "/Asset/Create.html" );
 +        if (!RT->Config->Get('AssetHideSimpleSearch')) {
 +            $assets->child( "simple_search", title => loc("Simple Search"), path => "/Asset/Search/" );
 +        }
 +        $assets->child( "search", title => loc("New Search"), path => "/Search/Build.html?Class=RT::Assets;NewQuery=1" );
 +    }
 +
 +    my $tools = $top->child( tools => title => loc('Tools'), path => '/Tools/index.html' );
 +
 +    $tools->child( my_day =>
 +        title       => loc('My Day'),
 +        description => loc('Easy updating of your open tickets'),
 +        path        => '/Tools/MyDay.html',
 +    );
 +
 +    if ( RT->Config->Get('EnableReminders') ) {
 +        $tools->child( my_reminders =>
 +            title       => loc('My Reminders'),
 +            description => loc('Easy viewing of your reminders'),
 +            path        => '/Tools/MyReminders.html',
 +        );
 +    }
 +
 +    if ( $current_user->HasRight( Right => 'ShowApprovalsTab', Object => RT->System ) ) {
 +        $tools->child( approval =>
 +            title       => loc('Approval'),
 +            description => loc('My Approvals'),
 +            path        => '/Approvals/',
 +        );
 +    }
 +
 +    if ( $current_user->HasRight( Right => 'ShowConfigTab', Object => RT->System ) )
 +    {
 +        _BuildAdminMenu( $request_path, $top, $widgets, $page, %args );
 +    }
 +
 +    my $username = '<span class="current-user">'
 +                 . $HTML::Mason::Commands::m->interp->apply_escapes($current_user->Name, 'h')
 +                 . '</span>';
 +    my $about_me = $top->child( 'preferences' =>
 +        title        => loc('Logged in as [_1]', $username),
 +        escape_title => 0,
 +        path         => '/User/Summary.html?id=' . $current_user->id,
 +        sort_order   => 99,
 +    );
 +
 +    $about_me->child( rt_name => title => loc("RT for [_1]", RT->Config->Get('rtname')), path => '/' );
 +
 +    if ( $current_user->UserObj
 +         && $current_user->HasRight( Right => 'ModifySelf', Object => RT->System )) {
 +        my $settings = $about_me->child( settings => title => loc('Settings'), path => '/Prefs/Other.html' );
 +        $settings->child( options        => title => loc('Preferences'),        path => '/Prefs/Other.html' );
 +        $settings->child( about_me       => title => loc('About me'),       path => '/Prefs/AboutMe.html' );
 +        if ( $current_user->HasRight( Right => 'ManageAuthTokens', Object => RT->System ) ) {
 +            $settings->child( auth_tokens => title => loc('Auth Tokens'), path => '/Prefs/AuthTokens.html' );
 +        }
 +        $settings->child( search_options => title => loc('Search options'), path => '/Prefs/SearchOptions.html' );
 +        $settings->child( myrt           => title => loc('RT at a glance'), path => '/Prefs/MyRT.html' );
 +        $settings->child( dashboards_in_menu =>
 +            title => loc('Modify Reports menu'),
 +            path  => '/Prefs/DashboardsInMenu.html',
 +        );
 +        $settings->child( queue_list    => title => loc('Queue list'),   path => '/Prefs/QueueList.html' );
 +
 +        my $search_menu = $settings->child( 'saved-searches' => title => loc('Saved Searches') );
 +        my $searches = [ $HTML::Mason::Commands::m->comp( "/Search/Elements/SearchesForObject",
 +                          Object => RT::System->new( $current_user )) ];
 +        my $i = 0;
 +
 +        for my $search (@$searches) {
 +            $search_menu->child( "search-" . $i++ =>
 +                title => $search->[1],
 +                path  => "/Prefs/Search.html?"
 +                       . QueryString( name => ref( $search->[2] ) . '-' . $search->[2]->Id ),
 +            );
 +
 +        }
 +
 +        if ( $request_path =~ qr{/Prefs/(?:SearchOptions|CustomDateRanges)\.html} ) {
 +            $page->child(
 +                search_options => title => loc('Search Preferences'),
 +                path               => "/Prefs/SearchOptions.html"
 +            );
 +            $page->child(
 +                custom_date_ranges => title => loc('Custom Date Ranges'),
 +                path               => "/Prefs/CustomDateRanges.html"
 +            )
 +        }
 +
 +        if ( $request_path =~ m{^/Prefs/AuthTokens\.html} ) {
 +            $page->child( create_auth_token => title => loc('Create'),
 +                raw_html => q[<a class="btn menu-item" href="#create-auth-token" data-toggle="modal" rel="modal:open">].loc("Create")."</a>"
 +            );
 +        }
 +    }
 +    my $logout_url = RT->Config->Get('LogoutURL');
 +    if ( $current_user->Name
 +         && (   !RT->Config->Get('WebRemoteUserAuth')
 +              || RT->Config->Get('WebFallbackToRTLogin') )) {
 +        $about_me->child( logout => title => loc('Logout'), path => $logout_url );
 +    }
 +    if ( $request_path =~ m{^/Dashboards/(\d+)?}) {
 +        if ( my $id = ( $1 || $HTML::Mason::Commands::DECODED_ARGS->{'id'} ) ) {
 +            my $obj = RT::Dashboard->new( $current_user );
 +            $obj->LoadById($id);
 +            if ( $obj and $obj->id ) {
 +                $page->child( basics       => title => loc('Basics'),       path => "/Dashboards/Modify.html?id=" . $obj->id);
 +                $page->child( content      => title => loc('Content'),      path => "/Dashboards/Queries.html?id=" . $obj->id);
 +                $page->child( subscription => title => loc('Subscription'), path => "/Dashboards/Subscription.html?id=" . $obj->id)
 +                    if $obj->CurrentUserCanSubscribe;
 +                $page->child( show         => title => loc('Show'),         path => "/Dashboards/" . $obj->id . "/" . $obj->Name)
 +            }
 +        }
 +    }
 +
 +
 +    my $search_results_page_menu;
 +    if ( $request_path =~ m{^/Ticket/} ) {
 +        if ( ( $HTML::Mason::Commands::DECODED_ARGS->{'id'} || '' ) =~ /^(\d+)$/ ) {
 +            my $id  = $1;
 +            my $obj = RT::Ticket->new( $current_user );
 +            $obj->Load($id);
 +
 +            if ( $obj and $obj->id ) {
 +                my $actions = $page->child( actions => title => loc('Actions'), sort_order  => 95 );
 +
 +                my %can = %{ $obj->CurrentUser->PrincipalObj->HasRights( Object => $obj ) };
 +                # since CurrentUserCanSetOwner returns ($ok, $msg), the parens ensure that $can{} gets $ok
 +                ( $can{'_ModifyOwner'} ) = $obj->CurrentUserCanSetOwner();
 +                my $can = sub {
 +                    unless ($_[0] eq 'ExecuteCode') {
 +                        return $can{$_[0]} || $can{'SuperUser'};
 +                    } else {
 +                        return !RT->Config->Get('DisallowExecuteCode')
 +                            && ( $can{'ExecuteCode'} || $can{'SuperUser'} );
 +                    }
 +                };
 +
 +                $page->child( bookmark => raw_html => $HTML::Mason::Commands::m->scomp( '/Ticket/Elements/Bookmark', id => $id ), sort_order => 98 );
 +
 +                if ($can->('ModifyTicket')) {
 +                    $page->child( timer => raw_html => $HTML::Mason::Commands::m->scomp( '/Ticket/Elements/PopupTimerLink', id => $id ), sort_order => 99 );
 +                }
 +
 +                $page->child( display => title => loc('Display'), path => "/Ticket/Display.html?id=" . $id );
 +                $page->child( history => title => loc('History'), path => "/Ticket/History.html?id=" . $id );
 +
 +                # comment out until we can do it for an individual custom field
 +                #if ( $can->('ModifyTicket') || $can->('ModifyCustomField') ) {
 +                $page->child( basics => title => loc('Basics'), path => "/Ticket/Modify.html?id=" . $id );
 +
 +                #}
 +
 +                if ( $can->('ModifyTicket') || $can->('_ModifyOwner') || $can->('Watch') || $can->('WatchAsAdminCc') ) {
 +                    $page->child( people => title => loc('People'), path => "/Ticket/ModifyPeople.html?id=" . $id );
 +                }
 +
 +                if ( $can->('ModifyTicket') ) {
 +                    $page->child( dates => title => loc('Dates'), path => "/Ticket/ModifyDates.html?id=" . $id );
 +                    $page->child( links => title => loc('Links'), path => "/Ticket/ModifyLinks.html?id=" . $id );
 +                }
 +
 +                #if ( $can->('ModifyTicket') || $can->('ModifyCustomField') || $can->('_ModifyOwner') ) {
 +                $page->child( jumbo => title => loc('Jumbo'), path => "/Ticket/ModifyAll.html?id=" . $id );
 +                #}
 +
 +                if ( RT->Config->Get('EnableReminders') ) {
 +                    $page->child( reminders => title => loc('Reminders'), path => "/Ticket/Reminders.html?id=" . $id );
 +                }
 +
 +                if ( $can->('ModifyTicket') or $can->('ReplyToTicket') ) {
 +                    $actions->child( reply => title => loc('Reply'), path => "/Ticket/Update.html?Action=Respond;id=" . $id );
 +                }
 +
 +                if ( $can->('ModifyTicket') or $can->('CommentOnTicket') ) {
 +                    $actions->child( comment => title => loc('Comment'), path => "/Ticket/Update.html?Action=Comment;id=" . $id );
 +                }
 +
 +                if ( $can->('ForwardMessage') ) {
 +                    $actions->child( forward => title => loc('Forward'), path => "/Ticket/Forward.html?id=" . $id );
 +                }
 +
 +                my $hide_resolve_with_deps = RT->Config->Get('HideResolveActionsWithDependencies')
 +                    && $obj->HasUnresolvedDependencies;
 +
 +                my $current   = $obj->Status;
 +                my $lifecycle = $obj->LifecycleObj;
 +                my $i         = 1;
 +                foreach my $info ( $lifecycle->Actions($current) ) {
 +                    my $next = $info->{'to'};
 +                    next unless $lifecycle->IsTransition( $current => $next );
 +
 +                    my $check = $lifecycle->CheckRight( $current => $next );
 +                    next unless $can->($check);
 +
 +                    next if $hide_resolve_with_deps
 +                        && $lifecycle->IsInactive($next)
 +                        && !$lifecycle->IsInactive($current);
 +
 +                    my $action = $info->{'update'} || '';
 +                    my $url = '/Ticket/';
 +                    $url .= "Update.html?". QueryString(
 +                        $action
 +                            ? (Action        => $action)
 +                            : (SubmitTicket  => 1, Status => $next),
 +                        DefaultStatus => $next,
 +                        id            => $id,
 +                    );
 +                    my $key = $info->{'label'} || ucfirst($next);
 +                    $actions->child( $key => title => loc( $key ), path => $url);
 +                }
 +
 +                my ($can_take, $tmsg) = $obj->CurrentUserCanSetOwner( Type => 'Take' );
 +                my ($can_steal, $smsg) = $obj->CurrentUserCanSetOwner( Type => 'Steal' );
 +                my ($can_untake, $umsg) = $obj->CurrentUserCanSetOwner( Type => 'Untake' );
 +                if ( $can_take ){
 +                    $actions->child( take => title => loc('Take'), path => "/Ticket/Display.html?Action=Take;id=" . $id );
 +                }
 +                elsif ( $can_steal ){
 +                    $actions->child( steal => title => loc('Steal'), path => "/Ticket/Display.html?Action=Steal;id=" . $id );
 +                }
 +                elsif ( $can_untake ){
 +                    $actions->child( untake => title => loc('Untake'), path => "/Ticket/Display.html?Action=Untake;id=" . $id );
 +                }
 +
 +                # TODO needs a "Can extract article into a class applied to this queue" check
 +                $actions->child( 'extract-article' =>
 +                    title => loc('Extract Article'),
 +                    path  => "/Articles/Article/ExtractIntoClass.html?Ticket=".$obj->id,
 +                ) if $current_user->HasRight( Right => 'ShowArticlesMenu', Object => RT->System );
 +
 +                $actions->child( 'edit_assets' =>
 +                    title => loc('Edit Assets'),
 +                     path => "/Asset/Search/Bulk.html?Query=Linked=" . $obj->id,
 +                ) if $can->('ModifyTicket')
 +                  && $HTML::Mason::Commands::session{CurrentUser}->HasRight( Right => 'ShowAssetsMenu', Object => RT->System );
 +
 +                if ( defined $HTML::Mason::Commands::session{"collection-RT::Tickets"} ) {
 +                    # we have to update session data if we get new ItemMap
 +                    my $updatesession;
 +                    $updatesession = 1 unless ( $HTML::Mason::Commands::session{"collection-RT::Tickets"}->{'item_map'} );
 +
 +                    my $item_map = $HTML::Mason::Commands::session{"collection-RT::Tickets"}->ItemMap;
 +
 +                    if ($updatesession) {
 +                        $HTML::Mason::Commands::session{"collection-RT::Tickets"}->PrepForSerialization();
 +                    }
 +
 +                    my $search = $search_results_page_menu = $HTML::Mason::Commands::m->notes('search-results-page-menu', RT::Interface::Web::Menu->new());
 +
 +                    # Don't display prev links if we're on the first ticket
 +                    if ( $item_map->{$id}->{prev} ) {
 +                        $search->child(
 +                            first        => title => q{<span class="fas fa-angle-double-left"></span>},
 +                            escape_title => 0,
 +                            class        => "nav",
 +                            path         => "/Ticket/Display.html?id=" . $item_map->{first},
 +                            attributes   => {
 +                                'data-toggle'         => 'tooltip',
 +                                'data-original-title' => loc('First'),
 +                                alt                   => loc('First'),
 +                            },
 +                        );
 +                        $search->child(
 +                            prev         => title => q{<span class="fas fa-angle-left"></span>},
 +                            escape_title => 0,
 +                            class        => "nav",
 +                            path         => "/Ticket/Display.html?id=" . $item_map->{$id}->{prev},
 +                            attributes   => {
 +                                'data-toggle'         => 'tooltip',
 +                                'data-original-title' => loc('Prev'),
 +                                alt                   => loc('Prev'),
 +                            },
 +                        );
 +                    }
 +                    # Don't display next links if we're on the last ticket
 +                    if ( $item_map->{$id}->{next} ) {
 +                        $search->child(
 +                            next         => title => q{<span class="fas fa-angle-right"></span>},
 +                            escape_title => 0,
 +                            class        => "nav",
 +                            path         => "/Ticket/Display.html?id=" . $item_map->{$id}->{next},
 +                            attributes   => {
 +                                'data-toggle'         => 'tooltip',
 +                                'data-original-title' => loc('Next'),
 +                                alt                   => loc('Next'),
 +                            },
 +                        );
 +                        if ( $item_map->{last} ) {
 +                            $search->child(
 +                                last         => title => q{<span class="fas fa-angle-double-right"></span>},
 +                                escape_title => 0,
 +                                class        => "nav",
 +                                path         => "/Ticket/Display.html?id=" . $item_map->{last},
 +                                attributes   => {
 +                                    'data-toggle'         => 'tooltip',
 +                                    'data-original-title' => loc('Last'),
 +                                    alt                   => loc('Last'),
 +                                },
 +                            );
 +                        }
 +                    }
 +                }
 +            }
 +        }
 +    }
 +
 +    # display "View ticket" link in transactions
 +    if ( $request_path =~ m{^/Transaction/Display.html} ) {
 +        if ( ( $HTML::Mason::Commands::DECODED_ARGS->{'id'} || '' ) =~ /^(\d+)$/ ) {
 +            my $txn_id = $1;
 +            my $txn = RT::Transaction->new( $current_user );
 +            $txn->Load( $txn_id );
 +            my $object = $txn->Object;
 +            if ( $object->Id ) {
 +                my $object_type = $object->RecordType;
 +                if ( $object_type eq 'Ticket' ) {
 +                    $page->child( view_ticket => title => loc("View ticket"), path => "/Ticket/Display.html?id=" . $object->Id );
 +                }
 +            }
 +        }
 +    }
 +
 +    # Scope here so we can share in the Privileged callback
 +    my $args      = '';
 +    my $has_query = '';
 +    if (
 +        (
 +               $request_path =~ m{^/(?:Ticket|Transaction|Search)/}
 +            && $request_path !~ m{^/Search/Simple\.html}
 +        )
 +        || (   $request_path =~ m{^/Search/Simple\.html}
 +            && $HTML::Mason::Commands::DECODED_ARGS->{'q'} )
 +
 +        # TODO: Asset simple search and SQL search don't share query, we
 +        # can't simply link to SQL search page on asset pages without
 +        # identifying if it's from simple search or SQL search. For now,
 +        # show "Current Search" only if asset simple search is disabled.
 +
 +        || ( $search_assets && $request_path =~ m{^/Asset/(?!Search/)} && RT->Config->Get('AssetHideSimpleSearch') )
 +      )
 +    {
 +        my $class = $HTML::Mason::Commands::DECODED_ARGS->{Class}
 +            || ( $request_path =~ m{^/(Transaction|Ticket|Asset)/} ? "RT::$1s" : 'RT::Tickets' );
 +
 +        my $search;
 +        if ( $class eq 'RT::Tickets' ) {
 +            $search = $top->child('search')->child('tickets');
 +        }
 +        elsif ( $class eq 'RT::Assets' ) {
 +            $search = $search_assets;
 +        }
 +        else {
 +            $search = $txns_tickets;
 +        }
 +
 +        my $hash_name = join '-', 'CurrentSearchHash', $class,
 +            $HTML::Mason::Commands::DECODED_ARGS->{ObjectType} || ( $class eq 'RT::Transactions' ? 'RT::Ticket' : () );
 +        my $current_search = $HTML::Mason::Commands::session{$hash_name} || {};
 +        my $search_id = $HTML::Mason::Commands::DECODED_ARGS->{'SavedSearchLoad'} || $HTML::Mason::Commands::DECODED_ARGS->{'SavedSearchId'} || $current_search->{'SearchId'} || '';
 +        my $chart_id = $HTML::Mason::Commands::DECODED_ARGS->{'SavedChartSearchId'} || $current_search->{SavedChartSearchId};
 +
 +        $has_query = 1 if ( $HTML::Mason::Commands::DECODED_ARGS->{'Query'} or $current_search->{'Query'} );
 +
 +        my %query_args;
 +        my %fallback_query_args = (
 +            SavedSearchId => ( $search_id eq 'new' ) ? undef : $search_id,
 +            SavedChartSearchId => $chart_id,
 +            (
 +                map {
 +                    my $p = $_;
 +                    $p => $HTML::Mason::Commands::DECODED_ARGS->{$p} || $current_search->{$p}
 +                } qw(Query Format OrderBy Order Page Class ObjectType ResultPage ExtraQueryParams),
 +            ),
 +            RowsPerPage => (
 +                defined $HTML::Mason::Commands::DECODED_ARGS->{'RowsPerPage'}
 +                ? $HTML::Mason::Commands::DECODED_ARGS->{'RowsPerPage'}
 +                : $current_search->{'RowsPerPage'}
 +            ),
 +        );
 +
 +        if ( my $extra_params = $fallback_query_args{ExtraQueryParams} ) {
 +            for my $param ( ref $extra_params eq 'ARRAY' ? @$extra_params : $extra_params ) {
 +                $fallback_query_args{$param}
 +                    = $HTML::Mason::Commands::DECODED_ARGS->{$param} || $current_search->{$param};
 +            }
 +        }
 +
 +        $fallback_query_args{Class} ||= $class;
 +        $fallback_query_args{ObjectType} ||= 'RT::Ticket' if $class eq 'RT::Transactions';
 +
 +        if ($query_string) {
 +            $args = '?' . $query_string;
 +        }
 +        else {
 +            my %final_query_args = ();
 +            # key => callback to avoid unnecessary work
 +
 +            if ( my $extra_params = $query_args->{ExtraQueryParams} ) {
 +                $final_query_args{ExtraQueryParams} = $extra_params;
 +                for my $param ( ref $extra_params eq 'ARRAY' ? @$extra_params : $extra_params ) {
 +                    $final_query_args{$param} = $query_args->{$param};
 +                }
 +            }
 +
 +            for my $param (keys %fallback_query_args) {
 +                $final_query_args{$param} = defined($query_args->{$param})
 +                                          ? $query_args->{$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 = '?' . QueryString(%final_query_args);
 +        }
 +
 +        my $current_search_menu;
 +        if (   $class eq 'RT::Tickets' && $request_path =~ m{^/Ticket}
 +            || $class eq 'RT::Transactions' && $request_path =~ m{^/Transaction}
 +            || $class eq 'RT::Assets' && $request_path =~ m{^/Asset/(?!Search/)} )
 +        {
 +            $current_search_menu = $search->child( current_search => title => loc('Current Search') );
 +            $current_search_menu->path("/Search/Results.html$args") if $has_query;
 +
 +            if ( $search_results_page_menu && $has_query ) {
 +                $search_results_page_menu->child(
 +                    current_search => title => q{<span class="fas fa-list"></span>},
 +                    escape_title   => 0,
 +                    sort_order     => -1,
 +                    path           => "/Search/Results.html$args",
 +                    attributes     => {
 +                        'data-toggle'         => 'tooltip',
 +                        'data-original-title' => loc('Return to Search Results'),
 +                        alt                   => loc('Return to Search Results'),
 +                    },
 +                );
 +            }
 +        }
 +        else {
 +            $current_search_menu = $page;
 +        }
 +
 +        $current_search_menu->child( edit_search =>
 +            title => loc('Edit Search'), path => "/Search/Build.html" . ( ($has_query) ? $args : '' ) );
 +        if ( $current_user->HasRight( Right => 'ShowSearchAdvanced', Object => RT->System ) ) {
 +            $current_search_menu->child( advanced => title => loc('Advanced'), path => "/Search/Edit.html$args" );
 +        }
 +        if ($has_query) {
 +            my $result_page = $HTML::Mason::Commands::DECODED_ARGS->{ResultPage};
 +            if ( $result_page ) {
 +                if ( my $web_path = RT->Config->Get('WebPath') ) {
 +                    $result_page =~ s!^$web_path!!;
 +                }
 +            }
 +            else {
 +                $result_page = '/Search/Results.html';
 +            }
 +
 +            $current_search_menu->child( results => title => loc('Show Results'), path => "$result_page$args" );
 +        }
 +
 +        if ( $has_query ) {
 +            if ( $class eq 'RT::Tickets' ) {
 +                if ( $current_user->HasRight( Right => 'ShowSearchBulkUpdate', Object => RT->System ) ) {
 +                    $current_search_menu->child( bulk  => title => loc('Bulk Update'), path => "/Search/Bulk.html$args" );
 +                }
 +                $current_search_menu->child( chart => title => loc('Chart'),       path => "/Search/Chart.html$args" );
 +            }
 +            elsif ( $class eq 'RT::Assets' ) {
 +                $current_search_menu->child( bulk  => title => loc('Bulk Update'), path => "/Asset/Search/Bulk.html$args" );
 +            }
 +
 +            my $more = $current_search_menu->child( more => title => loc('Feeds') );
 +
 +            $more->child( spreadsheet => title => loc('Spreadsheet'), path => "/Search/Results.tsv$args" );
 +
 +            if ( $class eq 'RT::Tickets' ) {
 +                my %rss_data
 +                    = map { $_ => $query_args->{$_} || $fallback_query_args{$_} || '' } qw(Query Order OrderBy);
 +                my $RSSQueryString = "?"
 +                    . QueryString(
 +                    Query   => $rss_data{Query},
 +                    Order   => $rss_data{Order},
 +                    OrderBy => $rss_data{OrderBy}
 +                    );
 +                my $RSSPath = join '/', map $HTML::Mason::Commands::m->interp->apply_escapes( $_, 'u' ),
 +                    $current_user->UserObj->Name,
 +                    $current_user->UserObj->GenerateAuthString(
 +                    $rss_data{Query} . $rss_data{Order} . $rss_data{OrderBy} );
 +
 +                $more->child( rss => title => loc('RSS'), path => "/NoAuth/rss/$RSSPath/$RSSQueryString" );
 +                my $ical_path = join '/', map $HTML::Mason::Commands::m->interp->apply_escapes( $_, 'u' ),
 +                    $current_user->UserObj->Name,
 +                    $current_user->UserObj->GenerateAuthString( $rss_data{Query} ),
 +                    $rss_data{Query};
 +                $more->child( ical => title => loc('iCal'), path => '/NoAuth/iCal/' . $ical_path );
 +
 +                #XXX TODO better abstraction of SuperUser right check
 +                if ( $current_user->HasRight( Right => 'SuperUser', Object => RT->System ) ) {
 +                    my $shred_args = QueryString(
 +                        Search          => 1,
 +                        Plugin          => 'Tickets',
 +                        'Tickets:query' => $rss_data{'Query'},
 +                        'Tickets:limit' => $query_args->{'Rows'},
 +                    );
 +
 +                    $more->child(
 +                        shredder => title => loc('Shredder'),
 +                        path     => '/Admin/Tools/Shredder/?' . $shred_args
 +                    );
 +                }
 +            }
 +        }
 +    }
 +
 +    if ( $request_path =~ m{^/Article/} ) {
 +        if ( $HTML::Mason::Commands::DECODED_ARGS->{'id'} && $HTML::Mason::Commands::DECODED_ARGS->{'id'} =~ /^\d+$/ ) {
 +            my $id = $HTML::Mason::Commands::DECODED_ARGS->{'id'};
 +            $page->child( display => title => loc('Display'), path => "/Articles/Article/Display.html?id=".$id );
 +            $page->child( history => title => loc('History'), path => "/Articles/Article/History.html?id=".$id );
 +            $page->child( modify  => title => loc('Modify'),  path => "/Articles/Article/Edit.html?id=".$id );
 +        }
 +    }
 +
 +    if ( $request_path =~ m{^/Articles/} ) {
 +        $page->child( search => title => loc("Search"),       path => "/Articles/Article/Search.html" );
 +        if ( $request_path =~ m{^/Articles/Article/} and ( $HTML::Mason::Commands::DECODED_ARGS->{'id'} || '' ) =~ /^(\d+)$/ ) {
 +            my $id  = $1;
 +            my $obj = RT::Article->new( $current_user );
 +            $obj->Load($id);
 +
 +            if ( $obj and $obj->id ) {
 +                $page->child( display => title => loc("Display"), path => "/Articles/Article/Display.html?id=" . $id );
 +                $page->child( history => title => loc('History'), path => '/Articles/Article/History.html?id=' . $id );
 +
 +                if ( $obj->CurrentUserHasRight('ModifyArticle') ) {
 +                    $page->child(modify => title => loc('Modify'), path => '/Articles/Article/Edit.html?id=' . $id );
 +                }
 +            }
 +        }
 +
 +    }
 +
 +    if ($request_path =~ m{^/Asset/} and $HTML::Mason::Commands::DECODED_ARGS->{id} and $HTML::Mason::Commands::DECODED_ARGS->{id} !~ /\D/) {
 +        _BuildAssetMenu( $request_path, $top, $widgets, $page, %args );
 +    } elsif ( $request_path =~ m{^/Asset/Search/(?:index\.html)?$}
 +        || ( $request_path =~ m{^/Asset/Search/Bulk\.html$} && $HTML::Mason::Commands::DECODED_ARGS->{Catalog} ) ) {
 +        my %search = map @{$_},
 +            grep defined $_->[1] && length $_->[1],
 +            map {ref $HTML::Mason::Commands::DECODED_ARGS->{$_} ? [$_, $HTML::Mason::Commands::DECODED_ARGS->{$_}[0]] : [$_, $HTML::Mason::Commands::DECODED_ARGS->{$_}] }
 +            grep /^(?:q|SearchAssets|!?(Name|Description|Catalog|Status|Role\..+|CF\..+)|Order(?:By)?|Page)$/,
 +            keys %$HTML::Mason::Commands::DECODED_ARGS;
 +
 +        if ( $request_path =~ /Bulk/) {
 +            $page->child('search',
 +                title => loc('Show Results'),
 +                path => '/Asset/Search/?' . (keys %search ? QueryString(%search) : ''),
 +            );
 +        } else {
 +            $page->child('bulk',
 +                title => loc('Bulk Update'),
 +                path => '/Asset/Search/Bulk.html?' . (keys %search ? QueryString(%search) : ''),
 +            );
 +        }
 +
 +        $page->child('csv',
 +            title => loc('Download Spreadsheet'),
 +            path  => '/Search/Results.tsv?' . QueryString(%search, Class => 'RT::Assets'),
 +        );
 +    } elsif ($request_path =~ m{^/Asset/Search/}) {
 +        my %search = map @{$_},
 +            grep defined $_->[1] && length $_->[1],
 +            map {ref $HTML::Mason::Commands::DECODED_ARGS->{$_} ? [$_, $HTML::Mason::Commands::DECODED_ARGS->{$_}[0]] : [$_, $HTML::Mason::Commands::DECODED_ARGS->{$_}] }
 +            grep /^(?:q|SearchAssets|!?(Name|Description|Catalog|Status|Role\..+|CF\..+)|Order(?:By)?|Page)$/,
 +            keys %$HTML::Mason::Commands::DECODED_ARGS;
 +
 +        my $current_search = $HTML::Mason::Commands::session{"CurrentSearchHash-RT::Assets"} || {};
 +        my $search_id = $HTML::Mason::Commands::DECODED_ARGS->{'SavedSearchLoad'} || $HTML::Mason::Commands::DECODED_ARGS->{'SavedSearchId'} || $current_search->{'SearchId'} || '';
 +        my $args      = '';
 +        my $has_query;
 +        $has_query = 1 if ( $HTML::Mason::Commands::DECODED_ARGS->{'Query'} or $current_search->{'Query'} );
 +
 +        my %query_args;
 +        my %fallback_query_args = (
 +            Class => 'RT::Assets',
 +            SavedSearchId => ( $search_id eq 'new' ) ? undef : $search_id,
 +            (
 +                map {
 +                    my $p = $_;
 +                    $p => $HTML::Mason::Commands::DECODED_ARGS->{$p} || $current_search->{$p}
 +                } qw(Query Format OrderBy Order Page)
 +            ),
 +            RowsPerPage => (
 +                defined $HTML::Mason::Commands::DECODED_ARGS->{'RowsPerPage'}
 +                ? $HTML::Mason::Commands::DECODED_ARGS->{'RowsPerPage'}
 +                : $current_search->{'RowsPerPage'}
 +            ),
 +        );
 +
 +        if ($query_string) {
 +            $args = '?' . $query_string;
 +        }
 +        else {
 +            my %final_query_args = ();
 +            # key => callback to avoid unnecessary work
 +
 +            for my $param (keys %fallback_query_args) {
 +                $final_query_args{$param} = defined($query_args->{$param})
 +                                          ? $query_args->{$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 = '?' . QueryString(%final_query_args);
 +        }
 +
 +        $page->child('edit_search',
 +            title      => loc('Edit Search'),
 +            path       => '/Search/Build.html' . $args,
 +        );
 +        $page->child( advanced => title => loc('Advanced'), path => '/Search/Edit.html' . $args );
 +        if ($has_query) {
 +            $page->child( results => title => loc('Show Results'), path => '/Search/Results.html' . $args );
 +            $page->child('bulk',
 +                title => loc('Bulk Update'),
 +                path => '/Asset/Search/Bulk.html' . $args,
 +            );
 +            my $more = $page->child( more => title => loc('Feeds') );
 +            $more->child( spreadsheet => title => loc('Spreadsheet'), path => "/Search/Results.tsv$args" );
 +        }
 +    } elsif ($request_path =~ m{^/Admin/Global/CustomFields/Catalog-Assets\.html$}) {
 +        $page->child("create", title => loc("Create New"), path => "/Admin/CustomFields/Modify.html?Create=1;LookupType=" . RT::Asset->CustomFieldLookupType);
 +    } elsif ($request_path =~ m{^/Admin/CustomFields(/|/index\.html)?$}
 +            and $HTML::Mason::Commands::DECODED_ARGS->{'Type'} and $HTML::Mason::Commands::DECODED_ARGS->{'Type'} eq RT::Asset->CustomFieldLookupType) {
 +        $page->child("create")->path( $page->child("create")->path . ";LookupType=" . RT::Asset->CustomFieldLookupType );
 +    } elsif ($request_path =~ m{^/Admin/Assets/Catalogs/}) {
 +        my $actions = $request_path =~ m{/((index|Create)\.html)?$}
 +            ? $page
 +            : $page->child("catalogs", title => loc("Catalogs"), path => "/Admin/Assets/Catalogs/");
 +
 +        $actions->child("select", title => loc("Select"), path => "/Admin/Assets/Catalogs/");
 +        $actions->child("create", title => loc("Create"), path => "/Admin/Assets/Catalogs/Create.html");
 +
 +        my $catalog = RT::Catalog->new( $current_user );
 +        $catalog->Load($HTML::Mason::Commands::DECODED_ARGS->{id}) if $HTML::Mason::Commands::DECODED_ARGS->{id};
 +
 +        if ($catalog->id and $catalog->CurrentUserCanSee) {
 +            my $query = "id=" . $catalog->id;
 +            $page->child("modify", title => loc("Basics"), path => "/Admin/Assets/Catalogs/Modify.html?$query");
 +            $page->child("people", title => loc("Roles"),  path => "/Admin/Assets/Catalogs/Roles.html?$query");
 +
 +            $page->child("cfs", title => loc("Asset Custom Fields"), path => "/Admin/Assets/Catalogs/CustomFields.html?$query");
 +
 +            $page->child("group-rights", title => loc("Group Rights"), path => "/Admin/Assets/Catalogs/GroupRights.html?$query");
 +            $page->child("user-rights",  title => loc("User Rights"),  path => "/Admin/Assets/Catalogs/UserRights.html?$query");
 +
 +            $page->child("default-values", title => loc('Default Values'), path => "/Admin/Assets/Catalogs/DefaultValues.html?$query");
 +        }
 +    }
 +
 +    if ( $request_path =~ m{^/User/(Summary|History)\.html} ) {
 +        if ($page->child('summary')) {
 +            # Already set up from having AdminUser and ShowConfigTab;
 +            # but rename "Basics" to "Edit" in this context
 +            $page->child( 'basics' )->title( loc('Edit') );
 +        } elsif ( $current_user->HasRight( Object => $RT::System, Right => 'ShowUserHistory' ) ) {
 +            $page->child( display => title => loc('Summary'), path => '/User/Summary.html?id=' . $HTML::Mason::Commands::DECODED_ARGS->{'id'} );
 +            $page->child( history => title => loc('History'), path => '/User/History.html?id=' . $HTML::Mason::Commands::DECODED_ARGS->{'id'} );
 +        }
 +    }
 +
 +    if ( $request_path =~ /^\/(?:index.html|$)/ ) {
 +        my $alt = loc('Edit');
 +        $page->child( edit => raw_html => q[<a id="page-edit" class="menu-item" href="] . RT->Config->Get('WebPath') . qq[/Prefs/MyRT.html"><span class="fas fa-cog" alt="$alt" data-toggle="tooltip" data-placement="top" data-original-title="$alt"></span></a>] );
 +    }
 +
 +    if ( $request_path =~ m{^/Admin/Tools/(Configuration|EditConfig|ConfigHistory)} ) {
 +        $page->child( display => title => loc('View'), path => "/Admin/Tools/Configuration.html" );
 +        $page->child( modify => title => loc('Edit'), path => "/Admin/Tools/EditConfig.html" ) if RT->Config->Get('ShowEditSystemConfig');
 +        $page->child( history => title => loc('History'), path => "/Admin/Tools/ConfigHistory.html" );
 +    }
 +
 +    # due to historical reasons of always having been in /Elements/Tabs
 +    $HTML::Mason::Commands::m->callback( CallbackName => 'Privileged', Path => $request_path, Search_Args => $args, Has_Query => $has_query, ARGSRef => \%args, CallbackPage => '/Elements/Tabs' );
 +}
 +
 +sub _BuildAssetMenu {
 +    my $request_path = shift;
 +    my $top          = shift;
 +    my $widgets      = shift;
 +    my $page         = shift;
 +
 +    my %args = ( @_ );
 +
 +    my $current_user = $HTML::Mason::Commands::session{CurrentUser};
 +
 +    my $id    = $HTML::Mason::Commands::DECODED_ARGS->{id};
 +    my $asset = RT::Asset->new( $current_user );
 +    $asset->Load($id);
 +
 +    if ($asset->id) {
 +        $page->child("display",     title => HTML::Mason::Commands::loc("Display"),        path => "/Asset/Display.html?id=$id");
 +        $page->child("history",     title => HTML::Mason::Commands::loc("History"),        path => "/Asset/History.html?id=$id");
 +        $page->child("basics",      title => HTML::Mason::Commands::loc("Basics"),         path => "/Asset/Modify.html?id=$id");
 +        $page->child("links",       title => HTML::Mason::Commands::loc("Links"),          path => "/Asset/ModifyLinks.html?id=$id");
 +        $page->child("people",      title => HTML::Mason::Commands::loc("People"),         path => "/Asset/ModifyPeople.html?id=$id");
 +        $page->child("dates",       title => HTML::Mason::Commands::loc("Dates"),          path => "/Asset/ModifyDates.html?id=$id");
 +
 +        for my $grouping (RT::CustomField->CustomGroupings($asset)) {
 +            my $cfs = $asset->CustomFields;
 +            $cfs->LimitToGrouping( $asset => $grouping );
 +            next unless $cfs->Count;
 +            $page->child(
 +                "cf-grouping-$grouping",
 +                title   => HTML::Mason::Commands::loc($grouping),
 +                path    => "/Asset/ModifyCFs.html?id=$id;Grouping=" . $HTML::Mason::Commands::m->interp->apply_escapes($grouping, 'u'),
 +            );
 +        }
 +
 +        _BuildAssetMenuActionSubmenu( $request_path, $top, $widgets, $page, %args, Asset => $asset );
 +    }
 +}
 +
 +sub _BuildAssetMenuActionSubmenu {
 +    my $request_path = shift;
 +    my $top          = shift;
 +    my $widgets      = shift;
 +    my $page         = shift;
 +
 +    my %args = (
 +        Asset => undef,
 +        @_
 +    );
 +
 +    my $asset = $args{Asset};
 +    my $id    = $asset->id;
 +
 +    my $actions = $page->child("actions", title => HTML::Mason::Commands::loc("Actions"));
 +    $actions->child("create-linked-ticket", title => HTML::Mason::Commands::loc("Create linked ticket"), path => "/Asset/CreateLinkedTicket.html?Asset=$id");
 +
 +    my $status    = $asset->Status;
 +    my $lifecycle = $asset->LifecycleObj;
 +    for my $action ( $lifecycle->Actions($status) ) {
 +        my $next = $action->{'to'};
 +        next unless $lifecycle->IsTransition( $status => $next );
 +
 +        my $check = $lifecycle->CheckRight( $status => $next );
 +        next unless $asset->CurrentUserHasRight($check);
 +
 +        my $label = $action->{'label'} || ucfirst($next);
 +        $actions->child(
 +            $label,
 +            title   => HTML::Mason::Commands::loc($label),
 +            path    => "/Asset/Modify.html?id=$id;Update=1;DisplayAfter=1;Status="
 +                        . $HTML::Mason::Commands::m->interp->apply_escapes($next, 'u'),
 +
 +            class       => "asset-lifecycle-action",
 +            attributes  => {
 +                'data-current-status'   => $status,
 +                'data-next-status'      => $next,
 +            },
 +        );
 +    }
 +}
 +
 +sub _BuildAdminMenu {
 +    my $request_path = shift;
 +    my $top          = shift;
 +    my $widgets      = shift;
 +    my $page         = shift;
 +
 +    my %args = ( @_ );
 +
 +    my $current_user = $HTML::Mason::Commands::session{CurrentUser};
 +
 +    my $admin = $top->child( admin => title => loc('Admin'), path => '/Admin/' );
 +    if ( $current_user->HasRight( Object => RT->System, Right => 'AdminUsers' ) ) {
 +        my $users = $admin->child( users =>
 +            title       => loc('Users'),
 +            description => loc('Manage users and passwords'),
 +            path        => '/Admin/Users/',
 +        );
 +        $users->child( select => title => loc('Select'), path => "/Admin/Users/" );
 +        $users->child( create => title => loc('Create'), path => "/Admin/Users/Modify.html?Create=1" );
 +    }
 +    my $groups = $admin->child( groups =>
 +        title       => loc('Groups'),
 +        description => loc('Manage groups and group membership'),
 +        path        => '/Admin/Groups/',
 +    );
 +    $groups->child( select => title => loc('Select'), path => "/Admin/Groups/" );
 +    $groups->child( create => title => loc('Create'), path => "/Admin/Groups/Modify.html?Create=1" );
 +
 +    my $queues = $admin->child( queues =>
 +        title       => loc('Queues'),
 +        description => loc('Manage queues and queue-specific properties'),
 +        path        => '/Admin/Queues/',
 +    );
 +    $queues->child( select => title => loc('Select'), path => "/Admin/Queues/" );
 +    $queues->child( create => title => loc('Create'), path => "/Admin/Queues/Modify.html?Create=1" );
 +
 +    if ( $current_user->HasRight( Object => RT->System, Right => 'AdminCustomField' ) ) {
 +        my $cfs = $admin->child( 'custom-fields' =>
 +            title       => loc('Custom Fields'),
 +            description => loc('Manage custom fields and custom field values'),
 +            path        => '/Admin/CustomFields/',
 +        );
 +        $cfs->child( select => title => loc('Select'), path => "/Admin/CustomFields/" );
 +        $cfs->child( create => title => loc('Create'), path => "/Admin/CustomFields/Modify.html?Create=1" );
 +    }
 +
 +    if ( $current_user->HasRight( Object => RT->System, Right => 'AdminCustomRoles' ) ) {
 +        my $roles = $admin->child( 'custom-roles' =>
 +            title       => loc('Custom Roles'),
 +            description => loc('Manage custom roles'),
 +            path        => '/Admin/CustomRoles/',
 +        );
 +        $roles->child( select => title => loc('Select'), path => "/Admin/CustomRoles/" );
 +        $roles->child( create => title => loc('Create'), path => "/Admin/CustomRoles/Modify.html?Create=1" );
 +    }
 +
 +    if ( $current_user->HasRight( Object => RT->System, Right => 'ModifyScrips' ) ) {
 +        my $scrips = $admin->child( 'scrips' =>
 +            title       => loc('Scrips'),
 +            description => loc('Manage scrips'),
 +            path        => '/Admin/Scrips/',
 +        );
 +        $scrips->child( select => title => loc('Select'), path => "/Admin/Scrips/" );
 +        $scrips->child( create => title => loc('Create'), path => "/Admin/Scrips/Create.html" );
 +    }
 +
 +    if ( RT->Config->Get('ShowEditLifecycleConfig')
 +        && $current_user->HasRight( Object => RT->System, Right => 'SuperUser' ) )
 +    {
 +        my $lifecycles = $admin->child(
 +            lifecycles => title => loc('Lifecycles'),
 +            path       => '/Admin/Lifecycles/',
 +        );
 +
 +        $lifecycles->child( select => title => loc('Select'), path => '/Admin/Lifecycles/' );
 +        $lifecycles->child( create => title => loc('Create'), path => '/Admin/Lifecycles/Create.html' );
 +    }
 +
 +    my $admin_global = $admin->child( global =>
 +        title       => loc('Global'),
 +        description => loc('Manage properties and configuration which apply to all queues'),
 +        path        => '/Admin/Global/',
 +    );
 +
 +    my $scrips = $admin_global->child( scrips =>
 +        title       => loc('Scrips'),
 +        description => loc('Modify scrips which apply to all queues'),
 +        path        => '/Admin/Global/Scrips.html',
 +    );
 +    $scrips->child( select => title => loc('Select'), path => "/Admin/Global/Scrips.html" );
 +    $scrips->child( create => title => loc('Create'), path => "/Admin/Scrips/Create.html?Global=1" );
 +
 +    my $conditions = $admin_global->child( conditions =>
 +        title => loc('Conditions'),
 +        description => loc('Edit system conditions'),
 +        path        => '/Admin/Global/Conditions.html',
 +    );
 +    $conditions->child( select => title => loc('Select'), path => "/Admin/Global/Conditions.html" );
 +    $conditions->child( create => title => loc('Create'), path => "/Admin/Conditions/Create.html" );
 +
 +    my $actions   = $admin_global->child( actions =>
 +        title => loc('Actions'),
 +        description => loc('Edit system actions'),
 +        path        => '/Admin/Global/Actions.html',
 +    );
 +    $actions->child( select => title => loc('Select'), path => "/Admin/Global/Actions.html" );
 +    $actions->child( create => title => loc('Create'), path => "/Admin/Actions/Create.html" );
 +
 +    my $templates = $admin_global->child( templates =>
 +        title       => loc('Templates'),
 +        description => loc('Edit system templates'),
 +        path        => '/Admin/Global/Templates.html',
 +    );
 +    $templates->child( select => title => loc('Select'), path => "/Admin/Global/Templates.html" );
 +    $templates->child( create => title => loc('Create'), path => "/Admin/Global/Template.html?Create=1" );
 +
 +    my $cfadmin = $admin_global->child( 'custom-fields' =>
 +        title       => loc('Custom Fields'),
 +        description => loc('Modify global custom fields'),
 +        path        => '/Admin/Global/CustomFields/index.html',
 +    );
 +    $cfadmin->child( users =>
 +        title       => loc('Users'),
 +        description => loc('Select custom fields for all users'),
 +        path        => '/Admin/Global/CustomFields/Users.html',
 +    );
 +    $cfadmin->child( groups =>
 +        title       => loc('Groups'),
 +        description => loc('Select custom fields for all user groups'),
 +        path        => '/Admin/Global/CustomFields/Groups.html',
 +    );
 +    $cfadmin->child( queues =>
 +        title       => loc('Queues'),
 +        description => loc('Select custom fields for all queues'),
 +        path        => '/Admin/Global/CustomFields/Queues.html',
 +    );
 +    $cfadmin->child( tickets =>
 +        title       => loc('Tickets'),
 +        description => loc('Select custom fields for tickets in all queues'),
 +        path        => '/Admin/Global/CustomFields/Queue-Tickets.html',
 +    );
 +    $cfadmin->child( transactions =>
 +        title       => loc('Ticket Transactions'),
 +        description => loc('Select custom fields for transactions on tickets in all queues'),
 +        path        => '/Admin/Global/CustomFields/Queue-Transactions.html',
 +    );
 +    $cfadmin->child( 'custom-fields' =>
 +        title       => loc('Articles'),
 +        description => loc('Select Custom Fields for Articles in all Classes'),
 +        path        => '/Admin/Global/CustomFields/Class-Article.html',
 +    );
 +    $cfadmin->child( 'assets' =>
 +        title       => loc('Assets'),
 +        description => loc('Select Custom Fields for Assets in all Catalogs'),
 +        path        => '/Admin/Global/CustomFields/Catalog-Assets.html',
 +    );
 +
 +    my $article_admin = $admin->child( articles => title => loc('Articles'), path => "/Admin/Articles/index.html" );
 +    my $class_admin = $article_admin->child(classes => title => loc('Classes'), path => '/Admin/Articles/Classes/' );
 +    $class_admin->child( select =>
 +        title       => loc('Select'),
 +        description => loc('Modify and Create Classes'),
 +        path        => '/Admin/Articles/Classes/',
 +    );
 +    $class_admin->child( create =>
 +        title       => loc('Create'),
 +        description => loc('Modify and Create Custom Fields for Articles'),
 +        path        => '/Admin/Articles/Classes/Modify.html?Create=1',
 +    );
 +
 +
 +    my $cfs = $article_admin->child( 'custom-fields' =>
 +        title => loc('Custom Fields'),
 +        path  => '/Admin/CustomFields/index.html?'.$HTML::Mason::Commands::m->comp('/Elements/QueryString', Type => 'RT::Class-RT::Article'),
 +    );
 +    $cfs->child( select =>
 +        title => loc('Select'),
 +        path => '/Admin/CustomFields/index.html?'.$HTML::Mason::Commands::m->comp('/Elements/QueryString', Type => 'RT::Class-RT::Article'),
 +    );
 +    $cfs->child( create =>
 +        title => loc('Create'),
 +        path => '/Admin/CustomFields/Modify.html?'.$HTML::Mason::Commands::m->comp("/Elements/QueryString", Create=>1, LookupType=> "RT::Class-RT::Article" ),
 +    );
 +
 +    my $assets_admin = $admin->child( assets => title => loc("Assets"), path => '/Admin/Assets/' );
 +    my $catalog_admin = $assets_admin->child( catalogs =>
 +        title       => loc("Catalogs"),
 +        description => loc("Modify asset catalogs"),
 +        path        => "/Admin/Assets/Catalogs/"
 +    );
 +    $catalog_admin->child( "select", title => loc("Select"), path => $catalog_admin->path );
 +    $catalog_admin->child( "create", title => loc("Create"), path => "Create.html" );
 +
 +
 +    my $assets_cfs = $assets_admin->child( "cfs",
 +        title => loc("Custom Fields"),
 +        description => loc("Modify asset custom fields"),
 +        path => "/Admin/CustomFields/?Type=" . RT::Asset->CustomFieldLookupType
 +    );
 +    $assets_cfs->child( "select", title => loc("Select"), path => $assets_cfs->path );
 +    $assets_cfs->child( "create", title => loc("Create"), path => "/Admin/CustomFields/Modify.html?Create=1;LookupType=" . RT::Asset->CustomFieldLookupType);
 +
 +    $admin_global->child( 'group-rights' =>
 +        title       => loc('Group Rights'),
 +        description => loc('Modify global group rights'),
 +        path        => '/Admin/Global/GroupRights.html',
 +    );
 +    $admin_global->child( 'user-rights' =>
 +        title       => loc('User Rights'),
 +        description => loc('Modify global user rights'),
 +        path        => '/Admin/Global/UserRights.html',
 +    );
 +    $admin_global->child( 'my-rt' =>
 +        title       => loc('RT at a glance'),
 +        description => loc('Modify the default "RT at a glance" view'),
 +        path        => '/Admin/Global/MyRT.html',
 +    );
 +
 +    if (RT->Config->Get('SelfServiceUseDashboard')) {
 +        if ($current_user->HasRight( Right => 'ModifyDashboard', Object => RT->System ) ) {
 +            my $self_service = $admin_global->child( selfservice_home =>
 +                                                     title       => loc('Self Service Home Page'),
 +                                                     description => loc('Edit self service home page dashboard'),
 +                                                     path        => '/Admin/Global/SelfServiceHomePage.html');
 +            if ( $request_path =~ m{^/Admin/Global/SelfServiceHomePage} ) {
 +                $page->child(content => title => loc('Content'), path => '/Admin/Global/SelfServiceHomePage.html');
 +                $page->child(show    => title => loc('Show'), path => '/SelfService');
 +            }
 +        }
 +    }
 +    $admin_global->child( 'dashboards-in-menu' =>
 +        title       => loc('Modify Reports menu'),
 +        description => loc('Customize dashboards in menu'),
 +        path        => '/Admin/Global/DashboardsInMenu.html',
 +    );
 +    $admin_global->child( 'topics' =>
 +        title       => loc('Topics'),
 +        description => loc('Modify global article topics'),
 +        path        => '/Admin/Global/Topics.html',
 +    );
 +
 +    my $admin_tools = $admin->child( tools =>
 +        title       => loc('Tools'),
 +        description => loc('Use other RT administrative tools'),
 +        path        => '/Admin/Tools/',
 +    );
 +    $admin_tools->child( configuration =>
 +        title       => loc('System Configuration'),
 +        description => loc('Detailed information about your RT setup'),
 +        path        => '/Admin/Tools/Configuration.html',
 +    );
 +    $admin_tools->child( theme =>
 +        title       => loc('Theme'),
 +        description => loc('Customize the look of your RT'),
 +        path        => '/Admin/Tools/Theme.html',
 +    );
 +    if (RT->Config->Get('StatementLog')
 +        && $current_user->HasRight( Right => 'SuperUser', Object => RT->System )) {
 +       $admin_tools->child( 'sql-queries' =>
 +           title       => loc('SQL Queries'),
 +           description => loc('Browse the SQL queries made in this process'),
 +           path        => '/Admin/Tools/Queries.html',
 +       );
 +    }
 +    $admin_tools->child( rights_inspector =>
 +        title => loc('Rights Inspector'),
 +        description => loc('Search your configured rights'),
 +        path  => '/Admin/Tools/RightsInspector.html',
 +    );
 +    $admin_tools->child( shredder =>
 +        title       => loc('Shredder'),
 +        description => loc('Permanently wipeout data from RT'),
 +        path        => '/Admin/Tools/Shredder',
 +    );
 +
++    if ( RT->Config->Get('GnuPG')->{'Enable'}
++        && $current_user->HasRight( Right => 'SuperUser', Object => RT->System ) )
++    {
++        $admin_tools->child(
++            'gnupg'     => title => loc('Manage GnuPG Keys'),
++            description => loc('Manage GnuPG keys'),
++            path        => '/Admin/Tools/GnuPG.html',
++        );
++    }
++
 +    if ( $request_path =~ m{^/Admin/(Queues|Users|Groups|CustomFields|CustomRoles)} ) {
 +        my $type = $1;
 +
 +        my %labels = (
 +            Queues       => loc("Queues"),
 +            Users        => loc("Users"),
 +            Groups       => loc("Groups"),
 +            CustomFields => loc("Custom Fields"),
 +            CustomRoles  => loc("Custom Roles"),
 +        );
 +
 +        my $section;
 +        if ( $request_path =~ m|^/Admin/$type/?(?:index.html)?$|
 +             || (    $request_path =~ m|^/Admin/$type/(?:Modify.html)$|
 +                  && $HTML::Mason::Commands::DECODED_ARGS->{'Create'} )
 +           )
 +        {
 +            $section = $page;
 +
 +        } else {
 +            $section = $page->child( select => title => $labels{$type},
 +                                     path => "/Admin/$type/" );
 +        }
 +
 +        $section->child( select => title => loc('Select'), path => "/Admin/$type/" );
 +        $section->child( create => title => loc('Create'), path => "/Admin/$type/Modify.html?Create=1" );
 +    }
 +
 +    if ( $request_path =~ m{^/Admin/Queues} ) {
 +        if ( $HTML::Mason::Commands::DECODED_ARGS->{'id'} && $HTML::Mason::Commands::DECODED_ARGS->{'id'} =~ /^\d+$/
 +                ||
 +              $HTML::Mason::Commands::DECODED_ARGS->{'Queue'} && $HTML::Mason::Commands::DECODED_ARGS->{'Queue'} =~ /^\d+$/
 +                ) {
 +            my $id = $HTML::Mason::Commands::DECODED_ARGS->{'Queue'} || $HTML::Mason::Commands::DECODED_ARGS->{'id'};
 +            my $queue_obj = RT::Queue->new( $current_user );
 +            $queue_obj->Load($id);
 +
 +            if ( $queue_obj and $queue_obj->id ) {
 +                my $queue = $page;
 +                $queue->child( basics => title => loc('Basics'),   path => "/Admin/Queues/Modify.html?id=" . $id );
 +                $queue->child( people => title => loc('Watchers'), path => "/Admin/Queues/People.html?id=" . $id );
 +
 +                my $templates = $queue->child(templates => title => loc('Templates'), path => "/Admin/Queues/Templates.html?id=" . $id);
 +                $templates->child( select => title => loc('Select'), path => "/Admin/Queues/Templates.html?id=".$id);
 +                $templates->child( create => title => loc('Create'), path => "/Admin/Queues/Template.html?Create=1;Queue=".$id);
 +
 +                my $scrips = $queue->child( scrips => title => loc('Scrips'), path => "/Admin/Queues/Scrips.html?id=" . $id);
 +                $scrips->child( select => title => loc('Select'), path => "/Admin/Queues/Scrips.html?id=" . $id );
 +                $scrips->child( create => title => loc('Create'), path => "/Admin/Scrips/Create.html?Queue=" . $id);
 +
 +                my $cfs = $queue->child( 'custom-fields' => title => loc('Custom Fields') );
 +                my $ticket_cfs = $cfs->child( 'tickets' => title => loc('Tickets'),
 +                    path => '/Admin/Queues/CustomFields.html?SubType=RT::Ticket;id=' . $id );
 +
 +                my $txn_cfs = $cfs->child( 'transactions' => title => loc('Transactions'),
 +                    path => '/Admin/Queues/CustomFields.html?SubType=RT::Ticket-RT::Transaction;id='.$id );
 +
 +                $queue->child( 'group-rights' => title => loc('Group Rights'), path => "/Admin/Queues/GroupRights.html?id=".$id );
 +                $queue->child( 'user-rights' => title => loc('User Rights'), path => "/Admin/Queues/UserRights.html?id=" . $id );
 +                $queue->child( 'history' => title => loc('History'), path => "/Admin/Queues/History.html?id=" . $id );
 +                $queue->child( 'default-values' => title => loc('Default Values'), path => "/Admin/Queues/DefaultValues.html?id=" . $id );
 +
 +                # due to historical reasons of always having been in /Elements/Tabs
 +                $HTML::Mason::Commands::m->callback( CallbackName => 'PrivilegedQueue', queue_id => $id, page_menu => $queue, CallbackPage => '/Elements/Tabs' );
 +            }
 +        }
 +    }
 +    if ( $request_path =~ m{^(/Admin/Users|/User/(Summary|History)\.html)} and $admin->child("users") ) {
 +        if ( $HTML::Mason::Commands::DECODED_ARGS->{'id'} && $HTML::Mason::Commands::DECODED_ARGS->{'id'} =~ /^\d+$/ ) {
 +            my $id = $HTML::Mason::Commands::DECODED_ARGS->{'id'};
 +            my $obj = RT::User->new( $current_user );
 +            $obj->Load($id);
 +
 +            if ( $obj and $obj->id ) {
 +                $page->child( basics      => title => loc('Basics'),         path => "/Admin/Users/Modify.html?id=" . $id );
 +                $page->child( memberships => title => loc('Memberships'),    path => "/Admin/Users/Memberships.html?id=" . $id );
 +                $page->child( history     => title => loc('History'),        path => "/Admin/Users/History.html?id=" . $id );
 +                $page->child( 'my-rt'     => title => loc('RT at a glance'), path => "/Admin/Users/MyRT.html?id=" . $id );
 +                $page->child( 'dashboards-in-menu' =>
 +                    title => loc('Modify Reports menu'),
 +                    path  => '/Admin/Users/DashboardsInMenu.html?id=' . $id,
 +                );
 +                if ( RT->Config->Get('Crypt')->{'Enable'} ) {
 +                    $page->child( keys    => title => loc('Private keys'),   path => "/Admin/Users/Keys.html?id=" . $id );
 +                }
 +                $page->child( 'summary'   => title => loc('User Summary'),   path => "/User/Summary.html?id=" . $id );
 +
 +                if ( $current_user->HasRight( Right => 'ManageAuthTokens', Object => RT->System ) ) {
 +                    my $auth_tokens = $page->child(
 +                        auth_tokens => title => loc('Auth Tokens'),
 +                        path        => '/Admin/Users/AuthTokens.html?id=' . $id
 +                    );
 +
 +                    if ( $request_path =~ m{^/Admin/Users/AuthTokens\.html} ) {
 +                        $auth_tokens->child(
 +                            select_auth_token => title => loc('Select'),
 +                            path              => '/Admin/Users/AuthTokens.html?id=' . $id,
 +                        );
 +                        $auth_tokens->child(
 +                            create_auth_token => title => loc('Create'),
 +                            raw_html =>
 +                                q[<a class="btn menu-item" href="#create-auth-token" data-toggle="modal" rel="modal:open">]
 +                                . loc("Create") . "</a>"
 +                        );
 +                    }
 +                }
 +            }
 +        }
 +
 +    }
 +
 +    if ( $request_path =~ m{^(/Admin/Groups|/Group/(Summary|History)\.html)} ) {
 +        if ( $HTML::Mason::Commands::DECODED_ARGS->{'id'} && $HTML::Mason::Commands::DECODED_ARGS->{'id'} =~ /^\d+$/ ) {
 +            my $id = $HTML::Mason::Commands::DECODED_ARGS->{'id'};
 +            my $obj = RT::Group->new( $current_user );
 +            $obj->Load($id);
 +
 +            if ( $obj and $obj->id ) {
 +                $page->child( basics         => title => loc('Basics'),       path => "/Admin/Groups/Modify.html?id=" . $obj->id );
 +                $page->child( members        => title => loc('Members'),      path => "/Admin/Groups/Members.html?id=" . $obj->id );
 +                $page->child( memberships    => title => loc('Memberships'),  path => "/Admin/Groups/Memberships.html?id=" . $obj->id );
 +                $page->child( 'links'     =>
 +                              title       => loc("Links"),
 +                              path        => "/Admin/Groups/ModifyLinks.html?id=" . $obj->id,
 +                              description => loc("Group links"),
 +                );
 +                $page->child( 'group-rights' => title => loc('Group Rights'), path => "/Admin/Groups/GroupRights.html?id=" . $obj->id );
 +                $page->child( 'user-rights'  => title => loc('User Rights'),  path => "/Admin/Groups/UserRights.html?id=" . $obj->id );
 +                $page->child( history        => title => loc('History'),      path => "/Admin/Groups/History.html?id=" . $obj->id );
 +                $page->child( 'summary'   =>
 +                              title       => loc("Group Summary"),
 +                              path        => "/Group/Summary.html?id=" . $obj->id,
 +                              description => loc("Group summary page"),
 +                );
 +            }
 +        }
 +    }
 +
 +    if ( $request_path =~ m{^/Admin/CustomFields/} ) {
 +        if ( $HTML::Mason::Commands::DECODED_ARGS->{'id'} && $HTML::Mason::Commands::DECODED_ARGS->{'id'} =~ /^\d+$/ ) {
 +            my $id = $HTML::Mason::Commands::DECODED_ARGS->{'id'};
 +            my $obj = RT::CustomField->new( $current_user );
 +            $obj->Load($id);
 +
 +            if ( $obj and $obj->id ) {
 +                $page->child( basics           => title => loc('Basics'),       path => "/Admin/CustomFields/Modify.html?id=".$id );
 +                $page->child( 'group-rights'   => title => loc('Group Rights'), path => "/Admin/CustomFields/GroupRights.html?id=" . $id );
 +                $page->child( 'user-rights'    => title => loc('User Rights'),  path => "/Admin/CustomFields/UserRights.html?id=" . $id );
 +                unless ( $obj->IsOnlyGlobal ) {
 +                    $page->child( 'applies-to' => title => loc('Applies to'),   path => "/Admin/CustomFields/Objects.html?id=" . $id );
 +                }
 +            }
 +        }
 +    }
 +
 +    if ( $request_path =~ m{^/Admin/CustomRoles} ) {
 +        if ( $HTML::Mason::Commands::DECODED_ARGS->{'id'} && $HTML::Mason::Commands::DECODED_ARGS->{'id'} =~ /^\d+$/ ) {
 +            my $id = $HTML::Mason::Commands::DECODED_ARGS->{'id'};
 +            my $obj = RT::CustomRole->new( $current_user );
 +            $obj->Load($id);
 +
 +            if ( $obj and $obj->id ) {
 +                $page->child( basics       => title => loc('Basics'),       path => "/Admin/CustomRoles/Modify.html?id=".$id );
 +                $page->child( 'applies-to' => title => loc('Applies to'),   path => "/Admin/CustomRoles/Objects.html?id=" . $id );
 +                $page->child( 'visibility' => title => loc('Visibility'),   path => "/Admin/CustomRoles/Visibility.html?id=" . $id );
 +            }
 +        }
 +    }
 +
 +    if ( $request_path =~ m{^/Admin/Scrips/} ) {
 +        if ( $HTML::Mason::Commands::m->request_args->{'id'} && $HTML::Mason::Commands::m->request_args->{'id'} =~ /^\d+$/ ) {
 +            my $id = $HTML::Mason::Commands::m->request_args->{'id'};
 +            my $obj = RT::Scrip->new( $current_user );
 +            $obj->Load($id);
 +
 +            my ( $admin_cat, $create_path_arg, $from_query_param );
 +            my $from_arg = $HTML::Mason::Commands::DECODED_ARGS->{'From'} || q{};
 +            my ($from_queue) = $from_arg =~ /^(\d+)$/;
 +            if ( $from_queue ) {
 +                $admin_cat = "Queues/Scrips.html?id=$from_queue";
 +                $create_path_arg = "?Queue=$from_queue";
 +                $from_query_param = ";From=$from_queue";
 +            }
 +            elsif ( $from_arg eq 'Global' ) {
 +                $admin_cat = 'Global/Scrips.html';
 +                $create_path_arg = '?Global=1';
 +                $from_query_param = ';From=Global';
 +            }
 +            else {
 +                $admin_cat = 'Scrips';
 +                $from_query_param = $create_path_arg = q{};
 +            }
 +            my $scrips = $page->child( scrips => title => loc('Scrips'), path => "/Admin/${admin_cat}" );
 +            $scrips->child( select => title => loc('Select'), path => "/Admin/${admin_cat}" );
 +            $scrips->child( create => title => loc('Create'), path => "/Admin/Scrips/Create.html${create_path_arg}" );
 +
 +            $page->child( basics => title => loc('Basics') => path => "/Admin/Scrips/Modify.html?id=" . $id . $from_query_param );
 +            $page->child( 'applies-to' => title => loc('Applies to'), path => "/Admin/Scrips/Objects.html?id=" . $id . $from_query_param );
 +        }
 +        elsif ( $request_path =~ m{^/Admin/Scrips/(index\.html)?$} ) {
 +            HTML::Mason::Commands::PageMenu->child( select => title => loc('Select') => path => "/Admin/Scrips/" );
 +            HTML::Mason::Commands::PageMenu->child( create => title => loc('Create') => path => "/Admin/Scrips/Create.html" );
 +        }
 +        elsif ( $request_path =~ m{^/Admin/Scrips/Create\.html$} ) {
 +            my ($queue) = $HTML::Mason::Commands::DECODED_ARGS->{'Queue'} && $HTML::Mason::Commands::DECODED_ARGS->{'Queue'} =~ /^(\d+)$/;
 +            my $global_arg = $HTML::Mason::Commands::DECODED_ARGS->{'Global'};
 +            if ($queue) {
 +                HTML::Mason::Commands::PageMenu->child( select => title => loc('Select') => path => "/Admin/Queues/Scrips.html?id=$queue" );
 +                HTML::Mason::Commands::PageMenu->child( create => title => loc('Create') => path => "/Admin/Scrips/Create.html?Queue=$queue" );
 +            } elsif ($global_arg) {
 +                HTML::Mason::Commands::PageMenu->child( select => title => loc('Select') => path => "/Admin/Global/Scrips.html" );
 +                HTML::Mason::Commands::PageMenu->child( create => title => loc('Create') => path => "/Admin/Scrips/Create.html?Global=1" );
 +            } else {
 +                HTML::Mason::Commands::PageMenu->child( select => title => loc('Select') => path => "/Admin/Scrips" );
 +                HTML::Mason::Commands::PageMenu->child( create => title => loc('Create') => path => "/Admin/Scrips/Create.html" );
 +            }
 +        }
 +    }
 +
 +    if ( $request_path =~ m{^/Admin/Lifecycles} && $current_user->HasRight( Object => RT->System, Right => 'SuperUser' ) ) {
 +        if (defined($HTML::Mason::Commands::DECODED_ARGS->{'Name'}) && defined($HTML::Mason::Commands::DECODED_ARGS->{'Type'}) ) {
 +            my $lifecycles = $page->child( 'lifecycles' =>
 +                title       => loc('Lifecycles'),
 +                description => loc('Manage lifecycles'),
 +                path        => '/Admin/Lifecycles/',
 +            );
 +            $lifecycles->child( select => title => loc('Select'), path => "/Admin/Lifecycles/" );
 +            $lifecycles->child( create => title => loc('Create'), path => "/Admin/Lifecycles/Create.html" );
 +
 +            my $LifecycleObj = RT::Lifecycle->new();
 +            $LifecycleObj->Load(Name => $HTML::Mason::Commands::DECODED_ARGS->{'Name'}, Type => $HTML::Mason::Commands::DECODED_ARGS->{'Type'});
 +
 +            if ($LifecycleObj->Name && $LifecycleObj->{data}{type} eq $HTML::Mason::Commands::DECODED_ARGS->{'Type'}) {
 +                my $Name_uri = $LifecycleObj->Name;
 +                my $Type_uri = $LifecycleObj->Type;
 +                RT::Interface::Web::EscapeURI(\$Name_uri);
 +                RT::Interface::Web::EscapeURI(\$Type_uri);
 +
 +                unless ( RT::Interface::Web->ClientIsIE ) {
 +                    $page->child( basics => title => loc('Modify'),  path => "/Admin/Lifecycles/Modify.html?Type=" . $Type_uri . ";Name=" . $Name_uri );
 +                }
 +                $page->child( actions => title => loc('Actions'), path => "/Admin/Lifecycles/Actions.html?Type=" . $Type_uri . ";Name=" . $Name_uri );
 +                $page->child( rights => title => loc('Rights'), path => "/Admin/Lifecycles/Rights.html?Type=" . $Type_uri . ";Name=" . $Name_uri );
 +                $page->child( mappings => title => loc('Mappings'),  path => "/Admin/Lifecycles/Mappings.html?Type=" . $Type_uri . ";Name=" . $Name_uri );
 +                $page->child( advanced => title => loc('Advanced'),  path => "/Admin/Lifecycles/Advanced.html?Type=" . $Type_uri . ";Name=" . $Name_uri );
 +            }
 +        }
 +        else {
 +            $page->child( select => title => loc('Select'), path => "/Admin/Lifecycles/" );
 +            $page->child( create => title => loc('Create'), path => "/Admin/Lifecycles/Create.html" );
 +        }
 +    }
 +
 +    if ( $request_path =~ m{^/Admin/Global/Scrips\.html} ) {
 +        $page->child( select => title => loc('Select'), path => "/Admin/Global/Scrips.html" );
 +        $page->child( create => title => loc('Create'), path => "/Admin/Scrips/Create.html?Global=1" );
 +    }
 +
 +    if ( $request_path =~ m{^/Admin(?:/Global)?/Conditions} ) {
 +        $page->child( select => title => loc('Select'), path => "/Admin/Global/Conditions.html" );
 +        $page->child( create => title => loc('Create'), path => "/Admin/Conditions/Create.html" );
 +    }
 +
 +    if ( $request_path =~ m{^/Admin(?:/Global)?/Actions} ) {
 +        $page->child( select => title => loc('Select'), path => "/Admin/Global/Actions.html" );
 +        $page->child( create => title => loc('Create'), path => "/Admin/Actions/Create.html" );
 +    }
 +
 +    if ( $request_path =~ m{^/Admin/Global/Templates?\.html} ) {
 +        $page->child( select => title => loc('Select'), path => "/Admin/Global/Templates.html" );
 +        $page->child( create => title => loc('Create'), path => "/Admin/Global/Template.html?Create=1" );
 +    }
 +
 +    if ( $request_path =~ m{^/Admin/Articles/Classes/} ) {
 +        if ( my $id = $HTML::Mason::Commands::DECODED_ARGS->{'id'} ) {
 +            my $obj = RT::Class->new( $current_user );
 +            $obj->Load($id);
 +
 +            if ( $obj and $obj->id ) {
 +                my $section = $page->child( select => title => loc("Classes"), path => "/Admin/Articles/Classes/" );
 +                $section->child( select => title => loc('Select'), path => "/Admin/Articles/Classes/" );
 +                $section->child( create => title => loc('Create'), path => "/Admin/Articles/Classes/Modify.html?Create=1" );
 +
 +                $page->child( basics          => title => loc('Basics'),        path => "/Admin/Articles/Classes/Modify.html?id=".$id );
 +                $page->child( topics          => title => loc('Topics'),        path => "/Admin/Articles/Classes/Topics.html?id=".$id );
 +                $page->child( 'custom-fields' => title => loc('Custom Fields'), path => "/Admin/Articles/Classes/CustomFields.html?id=".$id );
 +                $page->child( 'group-rights'  => title => loc('Group Rights'),  path => "/Admin/Articles/Classes/GroupRights.html?id=".$id );
 +                $page->child( 'user-rights'   => title => loc('User Rights'),   path => "/Admin/Articles/Classes/UserRights.html?id=".$id );
 +                $page->child( 'applies-to'    => title => loc('Applies to'),    path => "/Admin/Articles/Classes/Objects.html?id=$id" );
 +            }
 +        } else {
 +            $page->child( select => title => loc('Select'), path => "/Admin/Articles/Classes/" );
 +            $page->child( create => title => loc('Create'), path => "/Admin/Articles/Classes/Modify.html?Create=1" );
 +        }
 +    }
 +}
 +
 +sub BuildSelfServiceNav {
 +    my $request_path = shift;
 +    my $top          = shift;
 +    my $widgets      = shift;
 +    my $page         = shift;
 +
 +    my %args = ( @_ );
 +
 +    my $current_user = $HTML::Mason::Commands::session{CurrentUser};
 +
 +    if (   RT->Config->Get('SelfServiceUseDashboard')
 +        && $request_path =~ m{^/SelfService/(?:index\.html)?$}
 +        && $current_user->HasRight(
 +            Right  => 'ShowConfigTab',
 +            Object => RT->System
 +        )
 +        && $current_user->HasRight( Right => 'ModifyDashboard', Object => RT->System )
 +       )
 +    {
 +        $page->child( content => title => loc('Content'), path => '/Admin/Global/SelfServiceHomePage.html' );
 +        $page->child( show    => title => loc('Show'),    path => '/SelfService/' );
 +    }
 +
 +    my $queues = RT::Queues->new( $current_user );
 +    $queues->UnLimit;
 +
 +    my $queue_count = 0;
 +    my $queue_id;
 +
 +    while ( my $queue = $queues->Next ) {
 +        next unless $queue->CurrentUserHasRight('CreateTicket');
 +        $queue_id = $queue->id;
 +        $queue_count++;
 +        last if ( $queue_count > 1 );
 +    }
 +
 +    my $home = $top->child( home => title => loc('Homepage'), path => '/' );
 +
 +    if ( $queue_count > 1 ) {
 +        $home->child( new => title => loc('Create Ticket'), path => '/SelfService/CreateTicketInQueue.html' );
 +    } elsif ( $queue_id ) {
 +        $home->child( new => title => loc('Create Ticket'), path => '/SelfService/Create.html?Queue=' . $queue_id );
 +    }
 +
 +    my $menu_label = loc('Tickets');
 +    my $menu_path = '/SelfService/';
 +    if ( RT->Config->Get('SelfServiceUseDashboard') ) {
 +        $menu_path = '/SelfService/Open.html';
 +    }
 +    my $tickets = $top->child( tickets => title => $menu_label, path => $menu_path );
 +    $tickets->child( open   => title => loc('Open tickets'),   path => '/SelfService/Open.html' );
 +    $tickets->child( closed => title => loc('Closed tickets'), path => '/SelfService/Closed.html' );
 +
 +    $top->child( "assets", title => loc("Assets"), path => "/SelfService/Asset/" )
 +        if $current_user->HasRight( Right => 'ShowAssetsMenu', Object => RT->System );
 +
 +    my $username = '<span class="current-user">'
 +                 . $HTML::Mason::Commands::m->interp->apply_escapes($current_user->Name, 'h')
 +                 . '</span>';
 +    my $about_me = $top->child( preferences =>
 +        title        => loc('Logged in as [_1]', $username),
 +        escape_title => 0,
 +        sort_order   => 99,
 +    );
 +
 +    if ( ( RT->Config->Get('SelfServiceUserPrefs') || '' ) eq 'view-info' ||
 +        $current_user->HasRight( Right => 'ModifySelf', Object => RT->System ) ) {
 +        $about_me->child( prefs => title => loc('Preferences'), path => '/SelfService/Prefs.html' );
 +    }
 +
 +    my $logout_url = RT->Config->Get('LogoutURL');
 +    if ( $current_user->Name
 +         && (   !RT->Config->Get('WebRemoteUserAuth')
 +              || RT->Config->Get('WebFallbackToRTLogin') )) {
 +        $about_me->child( logout => title => loc('Logout'), path => $logout_url );
 +    }
 +
 +    if ( RT->Config->Get('SelfServiceShowArticleSearch') ) {
 +        $widgets->child( 'goto-article' => raw_html => $HTML::Mason::Commands::m->scomp('/SelfService/Elements/SearchArticle') );
 +    }
 +
 +    $widgets->child( goto => raw_html => $HTML::Mason::Commands::m->scomp('/SelfService/Elements/GotoTicket') );
 +
 +    if ($request_path =~ m{^/SelfService/Asset/} and $HTML::Mason::Commands::DECODED_ARGS->{id}) {
 +        my $id   = $HTML::Mason::Commands::DECODED_ARGS->{id};
 +        $page->child("display",     title => loc("Display"),        path => "/SelfService/Asset/Display.html?id=$id");
 +        $page->child("history",     title => loc("History"),        path => "/SelfService/Asset/History.html?id=$id");
 +
 +        if (Menu->child("new")) {
 +            my $actions = $page->child("actions", title => loc("Actions"));
 +            $actions->child("create-linked-ticket", title => loc("Create linked ticket"), path => "/SelfService/Asset/CreateLinkedTicket.html?Asset=$id");
 +        }
 +    }
 +
 +    # due to historical reasons of always having been in /Elements/Tabs
 +    $HTML::Mason::Commands::m->callback( CallbackName => 'SelfService', Path => $request_path, ARGSRef => \%args, CallbackPage => '/Elements/Tabs' );
 +}
 +
 +1;
diff --cc lib/RT/Ticket.pm
index dd340f1826,d1b83e42d8..e6cecdabd3
--- a/lib/RT/Ticket.pm
+++ b/lib/RT/Ticket.pm
@@@ -2614,13 -2611,10 +2616,12 @@@ sub SeenUpTo 
      my $self = shift;
      my $uid = $self->CurrentUser->id;
      my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
 -    return if $attr && $attr->Content gt $self->LastUpdated;
 +    if ( $attr && $attr->Content gt $self->LastUpdated ) {
 +        return wantarray ? ( undef, 0 ) : undef;
 +    }
  
      my $txns = $self->Transactions;
-     $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
-     $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
+     $txns->Limit( FIELD => 'Type', VALUE => [ 'Comment', 'Correspond' ], OPERATOR => 'IN' );
      $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
      $txns->Limit(
          FIELD => 'Created',
diff --cc share/html/Admin/Articles/Classes/Modify.html
index 02e5187775,6c985a5144..f122f60ca3
--- a/share/html/Admin/Articles/Classes/Modify.html
+++ b/share/html/Admin/Articles/Classes/Modify.html
@@@ -57,113 -57,81 +57,128 @@@
  <input type="hidden" name="id" value="<%$ClassObj->Id%>" />
  % }
  
 -<table>
 -<tr>
 -<td align="right"><&|/l&>Class Name</&>: </td>
 -<td><input name="Name" value="<% $ClassObj->Name || $Name || ''%>" /></td>
 -</tr>
 -<tr>
 -<td align="right"><&|/l&>Description</&>:</td>
 -<td><input name="Description" value="<% $ClassObj->Description || $Description || '' %>" size=60 /></td>
 -</tr>
 -<tr>
 -<td> </td>
 -<td>
 -    <input type="checkbox" id="Enabled" name="Enabled" value="1" <%$EnabledChecked%>>
 -    <label for="Enabled"><&|/l&>Enabled (Unchecking this box disables this class)</&></label>
 -</td>
 -</tr>
 -<tr>
 -<td> </td>
 -<td>
 -    <input type="checkbox" id="HotList" name="HotList" value="1" <%$HotListChecked%>>
 -    <label for="HotList"><&|/l&>All Articles in this class should be listed in a dropdown of the ticket reply page</&></label>
 -</td>
 -</tr>
 -</table>
 -
 -<h3><&|/l&>When inserting articles in this class into emails:</&></h3>
 -<ul>
 -<li>
 -    <input type="checkbox" id="Include-LinkToTicket" name="Include-LinkToTicket" value="1" <% $include{LinkToTicket} %>
 +<&| /Widgets/TitleBox, class => 'article-class-info-basics', content_class => 'mx-auto width-sm' &>
 +
 +  <div class="form-row">
 +    <div class="label col-3">
 +      <&|/l&>Class Name</&>:
 +    </div>
 +    <div class="value col-9">
 +      <input type="text" class="form-control" name="Name" value="<% $ClassObj->Name || $Name || ''%>" />
 +    </div>
 +  </div>
 +
 +  <div class="form-row">
 +    <div class="label col-3">
 +      <&|/l&>Description</&>:
 +    </div>
 +    <div class="value col-9">
 +      <input type="text" class="form-control" name="Description" value="<% $ClassObj->Description || $Description || '' %>" size=60 />
 +    </div>
 +  </div>
 +
 +  <div class="form-row">
 +    <div class="label col-3">
 +    </div>
 +    <div class="value col-9">
 +      <div class="custom-control custom-checkbox">
 +        <input type="checkbox" class="custom-control-input checkbox" id="Enabled" name="Enabled" value="1" <%$EnabledChecked%>>
 +        <label class="custom-control-label" for="Enabled"><&|/l&>Enabled (Unchecking this box disables this class)</&></label>
 +      </div>
 +    </div>
 +  </div>
 +
 +  <div class="form-row">
 +    <div class="col-12">
 +      <h5 class="mt-3"><&|/l&>When inserting articles in this class into emails:</&></h5>
 +    </div>
 +  </div>
 +
++  <div class="form-row">
++    <div class="label col-3">
++    </div>
++    <div class="value col-9">
++      <div class="custom-control custom-checkbox">
++        <input type="checkbox" class="custom-control-input checkbox" id="Include-LinkToTicket" name="Include-LinkToTicket" value="1" <% $include{LinkToTicket} %>
+ % if ( !RT->Config->Get('LinkArticlesOnInclude') ) {
+       disabled="disabled"
+ % }
 -    >
 -    <label for="Include-LinkToTicket"><&|/l&>Link article to ticket</&></label>
 -</li>
 -<li>
 -    <input type="checkbox" id="Include-Name" name="Include-Name" value="1" <% $include{Name} %>>
 -    <label for="Include-Name"><&|/l&>Include article name</&></label>
 -</li>
 -<li>
 -    <input type="checkbox" id="Include-Summary" name="Include-Summary" value="1" <% $include{Summary} %>>
 -    <label for="Include-Summary"><&|/l&>Include article summary</&></label>
 -</li>
 -<li>
 -     <input type="checkbox" id="Include-EscapeHTML" name="Include-EscapeHTML" value="1" <% $include{EscapeHTML} %>>
 -     <label for="Include-EscapeHTML"><&|/l&>Escape HTML (Unchecking this box is potentially unsafe)</&></label>
 -</li>
++>
++        <label class="custom-control-label" for="Include-LinkToTicket"><&|/l&>Link article to ticket</&></label>
++      </div>
++    </div>
++  </div>
++
 +  <div class="form-row">
 +    <div class="label col-3">
 +    </div>
 +    <div class="value col-9">
 +      <div class="custom-control custom-checkbox">
 +        <input type="checkbox" class="custom-control-input checkbox" id="Include-Name" name="Include-Name" value="1" <% $include{Name} %>>
 +        <label class="custom-control-label" for="Include-Name"><&|/l&>Include article name</&></label>
 +      </div>
 +    </div>
 +  </div>
 +
 +  <div class="form-row">
 +    <div class="label col-3">
 +    </div>
 +    <div class="value col-9">
 +      <div class="custom-control custom-checkbox">
 +        <input type="checkbox" class="custom-control-input checkbox" id="Include-Summary" name="Include-Summary" value="1" <% $include{Summary} %>>
 +        <label class="custom-control-label" for="Include-Summary"><&|/l&>Include article summary</&></label>
 +      </div>
 +    </div>
 +  </div>
 +
 +  <div class="form-row">
 +    <div class="label col-3">
 +    </div>
 +    <div class="value col-9">
 +      <div class="custom-control custom-checkbox">
 +        <input type="checkbox" class="custom-control-input checkbox" id="Include-EscapeHTML" name="Include-EscapeHTML" value="1" <% $include{EscapeHTML} %>>
 +        <label class="custom-control-label" for="Include-EscapeHTML"><&|/l&>Escape HTML (Unchecking this box is potentially unsafe)</&></label>
 +      </div>
 +    </div>
 +  </div>
 +
  % if ( $cfs ) {
 +  <h5 class="mt-3"><&|/l&>Include custom field:</&></h5>
  % while (my $cf = $cfs->Next) {
 -<li><&|/l, $cf->Name &>Include custom field '[_1]'</&>
 -  <ul>
 -    <li>
 -        <input type="checkbox" id="Include-CF-Title-<% $cf->Id %>" name="Include-CF-Title-<% $cf->Id %>" <% $include{"CF-Title-".$cf->Id} %> />
 -        <label for="Include-CF-Title-<% $cf->Id %>"><&|/l&>Title</&></label>
 -    </li>
 -    <li>
 -        <input type="checkbox" id="Include-CF-Value-<% $cf->Id %>" name="Include-CF-Value-<% $cf->Id %>" <% $include{"CF-Value-".$cf->Id} %> />
 -        <label for="Include-CF-Value-<% $cf->Id %>"><&|/l&>Value</&></label>
 -    </li>
 -  </ul>
 -</li>
 +  <div class="form-row">
 +    <div class="label col-3 pt-0">
 +      <% $cf->Name %>:
 +    </div>
 +    <div class="value col-9">
 +      <div class="form-row">
 +        <div class="col-auto">
 +          <div class="custom-control custom-checkbox">
 +            <input type="checkbox" class="custom-control-input checkbox" id="Include-CF-Title-<% $cf->Id %>" name="Include-CF-Title-<% $cf->Id %>" <% $include{"CF-Title-".$cf->Id} %> />
 +            <label class="custom-control-label" for="Include-CF-Title-<% $cf->Id %>"><&|/l&>Title</&></label>
 +          </div>
 +        </div>
 +        <div class="col-auto">
 +          <div class="custom-control custom-checkbox">
 +            <input type="checkbox" class="custom-control-input checkbox" id="Include-CF-Value-<% $cf->Id %>" name="Include-CF-Value-<% $cf->Id %>" <% $include{"CF-Value-".$cf->Id} %> />
 +            <label class="custom-control-label" for="Include-CF-Value-<% $cf->Id %>"><&|/l&>Value</&></label>
 +          </div>
 +        </div>
 +      </div>
 +    </div>
 +  </div>
  % } }
 -</ul>
  
  % if ( @$subject_cfs ) {
 -<h3><&|/l&>Change email subject:</&></h3>
 +<h5 class="mt-3"><&|/l&>Change email subject:</&></h5>
  <p><&|/l&>If a Custom Field is selected, the Subject of your outgoing email will be overridden by this article.</&></p>
  
 -<& /Widgets/Form/Select,
 -    Name         => 'SubjectOverride',
 -    DefaultLabel => loc('No Subject Override'),
 -    Values       => $subject_cfs,
 -    ValuesLabel  => $subject_cf_labels,
 -    CurrentValue => $ClassObj->SubjectOverride,
 -&>
 +  <& /Widgets/Form/Select,
 +      Name         => 'SubjectOverride',
 +      DefaultLabel => loc('No Subject Override'),
 +      Values       => $subject_cfs,
 +      ValuesLabel  => $subject_cf_labels,
 +      CurrentValue => $ClassObj->SubjectOverride,
 +  &>
  % }
  
  %$m->callback( CallbackName => 'BeforeSubmit', CustomFields => $cfs, ClassObj => $ClassObj );
diff --cc share/html/Admin/Elements/EditRights
index bb5adcc896,ba7f206103..e60afa5343
--- a/share/html/Admin/Elements/EditRights
+++ b/share/html/Admin/Elements/EditRights
@@@ -193,9 -180,14 +193,14 @@@ if ($anchor =~ /AddPrincipal/) 
  for my $category (@$Principals) {
      my ($name, $collection, $col, $loc) = @$category;
  </%perl>
 -<li class="category"><% loc($name) %></li>
 +          <li class="category"><% loc($name) %></li>
  <%perl>
      while ( my $obj = $collection->Next ) {
+         next
+           if $obj->isa( 'RT::Group' )
+           && $obj->_CustomRoleObj
+           && ( $obj->_CustomRoleObj->Disabled
+             || ( $Context->isa( 'RT::Queue' ) && !$obj->_CustomRoleObj->IsAdded( $Context->id ) ) );
          my $display = ref $col eq 'CODE' ? $col->($obj) : $obj->$col;
          my $id = "acl-" . $obj->PrincipalId;
  </%perl>
diff --cc share/html/Admin/Groups/Members.html
index 69463dd779,806eee1a48..b3b5720f6a
--- a/share/html/Admin/Groups/Members.html
+++ b/share/html/Admin/Groups/Members.html
@@@ -52,16 -55,27 +55,18 @@@
  <form action="<% RT->Config->Get('WebPath') %>/Admin/Groups/Members.html" method="post">
  <input type="hidden" class="hidden" name="id" value="<%$Group->Id%>" />
  
+ % $m->callback(CallbackName => 'BeforeEditMembership', %ARGS, Actions => \@results, ARGSRef => \%ARGS, Group => $Group);
+ 
  <&| /Widgets/TitleBox, title => loc('Editing membership for group [_1]', $Group->Label) &>
  
 -<table width="100%">
 -<tr>
 -<td>
 -<h3><&|/l&>Current members</&></h3>
 -</td>
 -<td>
 -<h3><&|/l&>Add members</&></h3>
 -</td>
 -</tr>
 -
 -<tr>
 -<td valign="top">
 +<div class="form-row">
 +  <div class="col-6">
 +    <h3><&|/l&>Current members</&></h3>
  
  % if ($Group->MembersObj->Count == 0 ) {
 -<em><&|/l&>(No members)</&></em>
 +      <em><&|/l&>(No members)</&></em>
  % } else {
 -<&|/l&>Users</&>
 +      <h4><&|/l&>Users</&></h4>
  % my $Users = $Group->UserMembersObj( Recursively => 0 );
  <%perl>
  my @users = map {$_->[1]}
diff --cc share/html/Admin/Tools/GnuPG.html
index 0000000000,d0f1dc2066..cf54a18d1b
mode 000000,100644..100644
--- a/share/html/Admin/Tools/GnuPG.html
+++ b/share/html/Admin/Tools/GnuPG.html
@@@ -1,0 -1,265 +1,287 @@@
+ %# BEGIN BPS TAGGED BLOCK {{{
+ %#
+ %# COPYRIGHT:
+ %#
+ %# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
+ %#                                          <sales at bestpractical.com>
+ %#
+ %# (Except where explicitly superseded by other copyright notices)
+ %#
+ %#
+ %# LICENSE:
+ %#
+ %# This work is made available to you under the terms of Version 2 of
+ %# the GNU General Public License. A copy of that license should have
+ %# been provided with this software, but in any event can be snarfed
+ %# from www.gnu.org.
+ %#
+ %# This work is distributed in the hope that it will be useful, but
+ %# WITHOUT ANY WARRANTY; without even the implied warranty of
+ %# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ %# General Public License for more details.
+ %#
+ %# You should have received a copy of the GNU General Public License
+ %# along with this program; if not, write to the Free Software
+ %# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ %# 02110-1301 or visit their web page on the internet at
+ %# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+ %#
+ %#
+ %# CONTRIBUTION SUBMISSION POLICY:
+ %#
+ %# (The following paragraph is not intended to limit the rights granted
+ %# to you to modify and distribute this software under the terms of
+ %# the GNU General Public License and is only of importance to you if
+ %# you choose to contribute your changes and enhancements to the
+ %# community by submitting them to Best Practical Solutions, LLC.)
+ %#
+ %# By intentionally submitting any modifications, corrections or
+ %# derivatives to this work, or any other work intended for use with
+ %# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+ %# you are the copyright holder for those contributions and you grant
+ %# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+ %# royalty-free, perpetual, license to use, copy, create derivative
+ %# works based on those contributions, and sublicense and distribute
+ %# those contributions and any derivatives thereof.
+ %#
+ %# END BPS TAGGED BLOCK }}}
+ <& /Admin/Elements/Header, Title => $title &>
+ <& /Elements/Tabs &>
+ 
+ <& /Elements/ListActions &>
+ 
 -<&|/Widgets/TitleBox, title => loc('Import Keys') &>
+ 
 -<form action="<% RT->Config->Get('WebPath')%>/Admin/Tools/GnuPG.html" name="ImportKeys" method="POST" enctype="multipart/form-data">
 -    <table>
++<form action="<% RT->Config->Get('WebPath')%>/Admin/Tools/GnuPG.html" name="ImportKeys" method="POST" enctype="multipart/form-data" class="mx-auto max-width-lg">
++<&|/Widgets/TitleBox, title => loc('Import Keys'), content_class => 'mx-auto width-md' &>
+ % my %options = RT->Config->Get('GnuPGOptions');
+ % if ( $options{'keyserver'} ) {
 -        <tr>
 -            <td align="right"></td>
 -            <td>
 -                <input name="Query" value="<% $ARGS{Query} // '' %>" size="40" />
 -                <input name="Search" type="submit" class="button" value="<&|/l&>Search</&>" />
 -            </td>
 -        </tr>
++  <div class="form-row">
++    <div class="label col-3"></div>
++    <div class="input-group col-9">
++      <input name="Query" class="form-control" value="<% $ARGS{Query} // '' %>" size="40" />
++      <input name="Search" type="submit" class="button btn btn-primary" value="<&|/l&>Search</&>" />
++    </div>
++  </div>
+ % if ( @search_results ) {
 -        <tr>
 -            <td></td>
 -            <td>
 -                <input type="checkbox" name="KeyAll" value="1" onclick="setCheckbox(this, 'Key')"></th>
 -                <span><&|/l&>Check All</&></span>
 -            </td>
 -        </tr>
++  <div class="form-row">
++    <div class="label col-3"></div>
++    <div class="value col-9">
++      <div class="custom-control custom-checkbox">
++        <input type="checkbox" class="custom-control-input checkbox" id="KeyAll" name="KeyAll" value="1" onclick="setCheckbox(this, 'Key')" />
++        <label class="custom-control-label" for="KeyAll"><&|/l&>Check All</&></label>
++      </div>
++    </div>
++  </div>
+ % for my $item ( @search_results ) {
 -        <tr>
 -            <td align="right"><&|/l&>Key</&>:</td>
 -            <td>
 -                <input type="checkbox" name="Key" value="<% $item->{Key} %>" />
 -                <span><% $item->{Summary} %></span>
 -            </td>
 -        </tr>
++  <div class="form-row">
++    <div class="label col-3">
++      <&|/l&>Key</&>:
++    </div>
++    <div class="value col-9">
++      <div class="custom-control custom-checkbox">
++        <input type="checkbox" class="custom-control-input checkbox" id="Key-<% $item->{Key} %>" name="Key" value="<% $item->{Key} %>" />
++        <label class="custom-control-label" for="Key-<% $item->{Key} %>"><% $item->{Summary} %></label>
++      </div>
++    </div>
++  </div>
+ % }
+ % }
+ 
+ % }
 -        <tr>
 -            <td align="right"><&|/l&>Content</&>:</td>
 -            <td><textarea name="Content" rows="8" cols="72"><% $ARGS{Content} // '' %></textarea></td>
 -        </tr>
 -    </table>
 -    <& /Elements/Submit, Name => 'Import', Label => loc('Import') &>
 -</form>
+ 
++  <div class="form-row">
++    <div class="label col-3">
++      <&|/l&>Content</&>:
++    </div>
++    <div class="value col-9">
++      <textarea class="form-control" name="Content" rows="8"><% $ARGS{Content} // '' %></textarea>
++    </div>
++  </div>
++
++  <div class="form-row">
++    <div class="col-12">
++      <& /Elements/Submit, Name => 'Import', Label => loc('Import') &>
++    </div>
++  </div>
+ </&>
++</form>
+ 
+ <&|/Widgets/TitleBox, title => loc('GnuPG Public Keys') &>
+ 
+ % if ( $public{info} && @{$public{info}} ) {
+ <form action="<% RT->Config->Get('WebPath')%>/Admin/Tools/GnuPG.html" name="PublicKeys" method="POST" enctype="multipart/form-data">
 -    <table>
++    <table class="table collection-as-table">
+         <tr>
+             <th><input type="checkbox" name="PublicKeyAll" value="1" onclick="setCheckbox(this, 'PublicKey')"></th>
+             <th><% loc('Summary') %></th>
+             <th><% loc('Trust Level') %></th>
+         </tr>
+ % for my $item ( @{$public{info}} ) {
+         <tr>
+             <td><input type="checkbox" name="PublicKey" value="<% $item->{Fingerprint} %>" <% $delete{$item->{Fingerprint}} ? 'checked="checked"' : '' |n %> /></td>
+             <td><% $item->{Formatted} %></td>
+             <td><% $owner_trust_level{$item->{OwnerTrustChar}} || loc('Not set') %></td>
+         </tr>
+ % }
+     </table>
 -
+     <hr />
 -    <table>
 -        <tr>
 -            <td class="label"><% loc('Trust Level' ) %></td>
 -            <td class="value">
 -                <select name="OwnerTrustLevel">
 -                    <option value=""  <% $OwnerTrustLevel eq '' ? 'selected="selected"' : '' |n %> >-</option>
 -                    <option value="1" <% $OwnerTrustLevel eq 1 ? 'selected="selected"' : '' |n %> ><% $owner_trust_level{1} %></option>
 -                    <option value="2" <% $OwnerTrustLevel eq 2 ? 'selected="selected"' : '' |n %> ><% $owner_trust_level{2} %></option>
 -                    <option value="3" <% $OwnerTrustLevel eq 3 ? 'selected="selected"' : '' |n %> ><% $owner_trust_level{3} %></option>
 -                    <option value="4" <% $OwnerTrustLevel eq 4 ? 'selected="selected"' : '' |n %> ><% $owner_trust_level{4} %></option>
 -                    <option value="5" <% $OwnerTrustLevel eq 5 ? 'selected="selected"' : '' |n %> ><% $owner_trust_level{5} %></option>
 -                </select>
 -            </td>
 -        </tr>
 -    </table>
 -    <& /Elements/Submit, Label => loc('Delete'), Name => 'DeletePublic', CheckboxNameRegex => '/^PublicKey(All)?$/', CheckAll => 1, ClearAll => 1 &>
 -    <& /Elements/Submit, Label => loc('Save Changes'), Name => 'TrustPublic' &>
++
++    <div class="form-row">
++      <div class="label col-3">
++        <% loc('Trust Level' ) %>:
++      </div>
++      <div class="input-group col-6">
++        <select name="OwnerTrustLevel" class="form-control selectpicker">
++            <option value=""  <% $OwnerTrustLevel eq '' ? 'selected="selected"' : '' |n %> >-</option>
++            <option value="1" <% $OwnerTrustLevel eq 1 ? 'selected="selected"' : '' |n %> ><% $owner_trust_level{1} %></option>
++            <option value="2" <% $OwnerTrustLevel eq 2 ? 'selected="selected"' : '' |n %> ><% $owner_trust_level{2} %></option>
++            <option value="3" <% $OwnerTrustLevel eq 3 ? 'selected="selected"' : '' |n %> ><% $owner_trust_level{3} %></option>
++            <option value="4" <% $OwnerTrustLevel eq 4 ? 'selected="selected"' : '' |n %> ><% $owner_trust_level{4} %></option>
++            <option value="5" <% $OwnerTrustLevel eq 5 ? 'selected="selected"' : '' |n %> ><% $owner_trust_level{5} %></option>
++        </select>
++      </div>
++    </div>
++
++    <div class="form-row">
++      <div class="col-12">
++        <& /Elements/Submit, Label => loc('Delete'), Name => 'DeletePublic', CheckboxNameRegex => '/^PublicKey(All)?$/', CheckAll => 1, ClearAll => 1 &>
++        <& /Elements/Submit, Label => loc('Save Changes'), Name => 'TrustPublic' &>
++      </div>
++    </div>
+ </form>
+ % } else {
 -<p><&|/l&>No public keys found.</&></p>
++<p class="mt-3 mb-1 ml-3"><&|/l&>No public keys found.</&></p>
+ % }
+ 
+ </&>
+ 
+ <&|/Widgets/TitleBox, title => loc('GnuPG Private Keys') &>
+ 
+ % if ( $private{info} && @{$private{info}} ) {
+ <form action="<% RT->Config->Get('WebPath')%>/Admin/Tools/GnuPG.html" name="PrivateKeys" method="POST" enctype="multipart/form-data">
 -    <table>
++    <table class="table collection-as-table">
+         <tr>
+             <th><input type="checkbox" name="PrivateKeyAll" value="1" onclick="setCheckbox(this, 'DeletePrivateKey')"></th>
+             <th><% loc('Summary') %></th>
+         </tr>
+ % for my $item ( @{$private{info}} ) {
+         <tr>
+             <td><input type="checkbox" name="PrivateKey" value="<% $item->{Fingerprint} %>" <% $delete{$item->{Fingerprint}} ? 'checked="checked"' : '' |n %> /></td>
+             <td><% $item->{Formatted} %></td>
+         </tr>
+ % }
+     </table>
+     <hr />
 -    <& /Elements/Submit, Label => loc('Delete'), Name => 'DeletePrivate', CheckboxNameRegex => '/^PrivateKey(All)?$/', CheckAll => 1, ClearAll => 1 &>
++    <div class="form-row">
++      <div class="col-12">
++        <& /Elements/Submit, Label => loc('Delete'), Name => 'DeletePrivate', CheckboxNameRegex => '/^PrivateKey(All)?$/', CheckAll => 1, ClearAll => 1 &>
++      </div>
++    </div>
+ </form>
+ % } else {
 -<p><&|/l&>No private keys found.</&></p>
++<p class="mt-3 mb-1 ml-3"><&|/l&>No private keys found.</&></p>
+ % }
+ 
+ </&>
+ <%INIT>
+ 
+ my $title = loc('Manage GnuPG Keys');
+ unless ( $session{'CurrentUser'}->HasRight( Object => $RT::System, Right => 'SuperUser' ) ) {
+     Abort( loc('This feature is only available to system administrators.') );
+ }
+ 
+ my @results;
+ my %delete;
+ 
+ my %owner_trust_level = (
+     1 => loc("I don't know or won't say"),
+     2 => loc("I do NOT trust"),
+     3 => loc("I trust marginally"),
+     4 => loc("I trust fully"),
+     5 => loc("I trust ultimately"),
+ 
+     q => loc("I don't know or won't say"),
+     n => loc("I do NOT trust"),
+     m => loc("I trust marginally"),
+     f => loc("I trust fully"),
+     u => loc("I trust ultimately"),
+ );
+ 
+ require RT::Crypt::GnuPG;
+ 
+ my @search_results;
+ if ( $ARGS{Search} ) {
+     if ( $ARGS{Query} ) {
+         my %ret = RT::Crypt::GnuPG->SearchKey( $ARGS{Query} );
+         @search_results = @{$ret{results}};
+         push @results, split /\n+/, $ret{logger} unless @search_results;
+     }
+ }
+ elsif ( $ARGS{Import} ) {
+ 
+     # show admin detailed imported messages
+     if ( $ARGS{Key} ) {
+         for my $key ( ref $ARGS{Key} ? @{ $ARGS{Key} } : $ARGS{Key} ) {
+             my %ret = RT::Crypt::GnuPG->ReceiveKey( $key );
+             push @results, split /\n+/, $ret{logger};
+         }
+     }
+ 
+     if ( $ARGS{Content} ) {
+         my %ret = RT::Crypt::GnuPG->ImportKey( $ARGS{Content} );
+         push @results, split /\n+/, $ret{logger};
+     }
+ }
+ elsif ( $ARGS{TrustPublic} ) {
+     if ( length $ARGS{OwnerTrustLevel} && $ARGS{PublicKey} ) {
+         for my $key ( ref $ARGS{PublicKey} ? @{ $ARGS{PublicKey} } : $ARGS{PublicKey} ) {
+             my %ret = RT::Crypt::GnuPG->TrustKey( $key, $ARGS{OwnerTrustLevel} );
+             if ( $ret{exit_code} == 0 ) {
+                 if ( $ret{logger} ) {
+ 
+                     # success messages are like "changing ownertrust
+                     # from 6 to 4", which is useless and misleading to
+                     # end users, so we hide them here.
+ 
+                     RT->Logger->debug( $ret{logger} );
+                     push @results, loc( "Key [_1] trust level is updated", substr( $key, -8 ) );
+                 }
+             }
+             elsif ( $ret{logger} ) {
+                 push @results, split /\n+/, $ret{logger};
+             }
+         }
+     }
+ }
+ else {
+     for my $type (qw/Public Private/) {
+         next unless $ARGS{"Delete$type"};
+         my $value = $ARGS{"${type}Key"};
+         for my $key ( ref $value ? @$value : $value ) {
+             $delete{$key} ||= 1;
+             my %ret = RT::Crypt::GnuPG->DeleteKey($key);
+ 
+             if ( $ret{exit_code} == 0 ) {
+                 # delete is silent, no extra debug messages
+                 push @results, loc( "Key [_1] is deleted", substr( $key, -8 ) );
+             }
+             elsif ( $ret{logger} ) {
+                 push @results, split /\n+/, $ret{logger};
+             }
+         }
+     }
+ }
+ 
+ MaybeRedirectForResults(
+     Actions => \@results,
+     Path    => '/Admin/Tools/GnuPG.html',
+ );
+ 
+ my %public = RT::Crypt::GnuPG->GetKeysInfo( Force => 1 );
+ my %private = RT::Crypt::GnuPG->GetKeysInfo( Force => 1, Type => 'private' );
+ 
+ </%INIT>
+ 
+ <%ARGS>
+ $OwnerTrustLevel => ''
+ </%ARGS>
diff --cc share/html/Articles/Elements/BeforeMessageBox
index 50fa9f9081,94d7c684aa..21f7de257b
--- a/share/html/Articles/Elements/BeforeMessageBox
+++ b/share/html/Articles/Elements/BeforeMessageBox
@@@ -119,11 -156,15 +119,11 @@@ my %uri
  if ( $ARGS{id} && $ARGS{id} ne 'new' ) {
      $uri{$_}++ for split ' ', ($ARGS{$ARGS{'id'}.'-RefersTo'} || '');
  
 -    foreach my $arg (keys %ARGS) {
 -        next if $name_prefix && substr($arg, 0, length($name_prefix)) ne $name_prefix;
 +    my $article = RT::Article->new($session{'CurrentUser'});
 +    my ($ret, $msg) = $article->Load($ARGS{'IncludeArticleId'});
  
-     if ($ret && $article->Id) {
 -        my $article = RT::Article->new($session{'CurrentUser'});
 -        $article->LoadByInclude(
 -            Field => substr($arg, length($name_prefix)),
 -            Value => $ARGS{$arg},
 -        );
 -        if ($article->Id and not $article->ClassObj->FirstAttribute('Skip-LinkToTicket')) {
++    if ($ret && $article->Id and not $article->ClassObj->FirstAttribute('Skip-LinkToTicket')) {
 +        unless ( $QueueObj && !$article->ClassObj->IsApplied(0) && !$article->ClassObj->IsApplied( $QueueObj->id ) ) {
              $uri{$article->URI}++;
          }
      }
diff --cc share/html/Asset/Elements/EditPeople
index 597ba2eca1,89ace8e207..90ce4c6eb3
--- a/share/html/Asset/Elements/EditPeople
+++ b/share/html/Asset/Elements/EditPeople
@@@ -45,15 -45,20 +45,15 @@@
  %# those contributions and any derivatives thereof.
  %#
  %# END BPS TAGGED BLOCK }}}
 -<table border="0" cellpadding="0" cellspacing="0">
  % for my $role ( $AssetObj->Roles ) {
 -<tr class="asset-people-<% CSSClass($role) %>">
 -<td class="label">
 -<% loc($role) %>:
 -</td>
 -<td class="value" colspan="5">
 -<& /Elements/EmailInput, Name => $role, Size => undef, Default => $ARGS{$role}, Autocomplete => 1, ($AssetObj->Role($role)->{Single} ? () : (AutocompleteType => 'Principals', AutocompleteMultiple => 1)) &>
 -</td>
 -</tr>
 +<div class="asset-people-<% CSSClass($role) %> form-row">
 +  <div class="label col-3"><% loc($role) %>:</div>
 +  <div class="value col-9">
-     <& /Elements/EmailInput, Name => $role, Size => undef, Default => $ARGS{$role}, Autocomplete => 1 &>
++    <& /Elements/EmailInput, Name => $role, Size => undef, Default => $ARGS{$role}, Autocomplete => 1, ($AssetObj->Role($role)->{Single} ? () : (AutocompleteType => 'Principals', AutocompleteMultiple => 1)) &>
 +  </div>
 +</div>
  % }
  
 -</table>
 -
  <%args>
  $AssetObj
  </%args>
diff --cc share/html/Elements/Header
index 0d8c09cf20,b1b762172e..5dea1eb938
--- a/share/html/Elements/Header
+++ b/share/html/Elements/Header
@@@ -96,13 -96,10 +96,15 @@@
  
  <& /Elements/Framekiller &>
  
+ % $m->callback( CallbackName => 'EndOfHead', ARGSRef => \%ARGS );
+ 
  </head>
 -  <body class="<% join( ' ',@{$ARGS{'BodyClass'}}) %>" <% $id && qq[id="comp-$id"] |n %>>
 +  <body class="<% join( ' ',@{$ARGS{'BodyClass'}}, RT->Config->Get('WebDefaultStylesheet', $session{CurrentUser}) =~ /-dark$/ ? 'darkmode' : ()) %>" <% $id && qq[id="comp-$id"] |n %>>
 +    <div class="main-container">
 +% # The close div for this is in PageLayout right before the rt body div
 +% if ( $ShowBar || $ShowTitle ) {
 +<div id='rt-header-container'>
 +% }
  
  % if ($ShowBar) {
  <& /Elements/Logo, %ARGS &>
diff --cc share/html/Elements/QuickCreate
index 66cbb9c7ca,947e5ed88c..01637259d5
--- a/share/html/Elements/QuickCreate
+++ b/share/html/Elements/QuickCreate
@@@ -53,42 -53,29 +53,42 @@@
  % $m->callback(CallbackName => 'InFormElement');
  >
  <input type="hidden" class="hidden" name="QuickCreate" value="1" />
 -<table>
 -<tr class="input-row"><td class="label"><&|/l&>Subject</&>:</td>
 -    <td colspan="3" class="value"><input type="text" size="50" name="Subject" value="<% $args->{Subject} || '' %>" /></td>
 -    </tr><tr class="input-row">
 -<td class="label"><&|/l&>Queue</&>:</td><td class="value"><& /Elements/SelectNewTicketQueue, Name => 'Queue', Default => $args->{Queue} &>
 -</td>
 -<td class="label"><&|/l&>Owner</&>:</td><td class="value">
 -<select type="select" name="Owner">  
 +<div>
 +  <div class="form-row input-row">
 +    <div class="label col-3"><&|/l&>Subject</&>:</div>
 +    <div class="value col-9"><input type="text" size="50" class="form-control" name="Subject" value="<% $args->{Subject} || '' %>" /></div>
 +  </div>
 +  <div class="form-row input-row">
 +    <div class="label col-3"><&|/l&>Queue</&>:</div>
 +    <div class="value col-9">
 +      <& /Elements/SelectNewTicketQueue, Name => 'Queue', Default => $args->{Queue} &>
 +    </div>
 +  </div>
 +  <div class="form-row input-row">
 +      <div class="label col-3"><&|/l&>Owner</&>:</div>
 +      <div class="value col-9">
 +        <select type="select" name="Owner" class="selectpicker form-control">
  % my $default_owner = $args->{Owner} || $session{'CurrentUser'}->id;
 -<option value="<%$session{'CurrentUser'}->id%>" <% $default_owner == $session{'CurrentUser'}->id ? 'selected="selected"' : '' |n %>><&|/l&>Me</&></option>
 -<option value="<%RT->Nobody->id%>" <% $default_owner == RT->Nobody->id ? 'selected="selected"' : '' |n %>><%loc('Nobody')%></option>
 -</select>
 -</td>
 -</tr>
 -<tr class="input-row">
 -    <td class="label"><&|/l&>Requestors</&>:</td>
 -    <td colspan="3" class="value"><& /Elements/EmailInput, Name => 'Requestors', AutocompleteType => 'Principals', Size => '40', Default => $args->{Requestors} || $session{CurrentUser}->EmailAddress, AutocompleteMultiple => 1 &></td>
 -</tr>
 -<tr class="input-row">
 -<td class="labeltop"><&|/l&>Content</&>:</td>
 -<td colspan="3" class="value"><textarea name="Content" cols="50" rows="3"><% $args->{Content} || ''%></textarea></td></tr>
 -</table>
 -<& /Elements/Submit, Label => loc('Create') &>
 +          <option value="<%$session{'CurrentUser'}->id%>" <% $default_owner == $session{'CurrentUser'}->id ? 'selected="selected"' : '' |n %>><&|/l&>Me</&></option>
 +          <option value="<%RT->Nobody->id%>" <% $default_owner == RT->Nobody->id ? 'selected="selected"' : '' |n %>><%loc('Nobody')%></option>
 +        </select>
 +      </div>
 +  </div>
 +  <div class="form-row input-row">
 +    <div class="label col-3"><&|/l&>Requestors</&>:</div>
-     <div class="value col-9"><& /Elements/EmailInput, Name => 'Requestors', Size => '40', Default => $args->{Requestors} || $session{CurrentUser}->EmailAddress, AutocompleteMultiple => 1 &></div>
++    <div class="value col-9"><& /Elements/EmailInput, Name => 'Requestors', AutocompleteType => 'Principals', Size => '40', Default => $args->{Requestors} || $session{CurrentUser}->EmailAddress, AutocompleteMultiple => 1 &></div>
 +  </div>
 +  <div class="form-row input-row">
 +    <div class="label col-3"><&|/l&>Content</&>:</div>
 +    <div class="value col-9"><textarea class="form-control" name="Content" cols="50" rows="3"><% $args->{Content} || ''%></textarea></div>
 +  </div>
 +</div>
 +
 +<div class="form-row">
 +  <div class="col-12">
 +    <& /Elements/Submit, Name => 'SubmitTicket', Label => loc('Create') &>
 +  </div>
 +</div>
  </form>
  </&>
  </div>
diff --cc share/html/Elements/SelectMatch
index fa1fbbe8a4,551e8ed91e..d7b30b01e2
--- a/share/html/Elements/SelectMatch
+++ b/share/html/Elements/SelectMatch
@@@ -45,11 -45,17 +45,17 @@@
  %# those contributions and any derivatives thereof.
  %#
  %# END BPS TAGGED BLOCK }}}
 -<select name="<%$Name%>">
 +<select name="<%$Name%>" class="selectpicker form-control">
- <option value="LIKE" <%$LikeDefault|n%>><%$Like%></option>
- <option value="NOT LIKE" <%$NotLikeDefault|n%>><%$NotLike%></option>
- <option value="=" <%$TrueDefault|n%>><%$True%></option>
- <option value="!=" <%$FalseDefault|n%>><%$False%></option>
+ <option value="LIKE" <% $default_is_shallow ? '' : $LikeDefault |n%>><%$Like%></option>
+ <option value="NOT LIKE" <% $default_is_shallow ? '' : $NotLikeDefault |n%>><%$NotLike%></option>
+ <option value="=" <% $default_is_shallow ? '' : $TrueDefault |n%>><%$True%></option>
+ <option value="!=" <% $default_is_shallow ? '' : $FalseDefault |n%>><%$False%></option>
+ % if ( $IncludeShallow ) {
+ <option value="SHALLOW LIKE" <% $default_is_shallow ? $LikeDefault : '' |n%>><%$ShallowLike%></option>
+ <option value="SHALLOW NOT LIKE" <% $default_is_shallow ? $NotLikeDefault : '' |n%>><%$ShallowNotLike%></option>
+ <option value="SHALLOW =" <% $default_is_shallow ? $TrueDefault : '' |n%>><%$ShallowTrue%></option>
+ <option value="SHALLOW !=" <% $default_is_shallow ? $FalseDefault : '' |n%>><%$ShallowFalse%></option>
+ % }
  </select>
  
  <%ARGS>
diff --cc share/html/Elements/ShowCustomFields
index 71d4610732,0cd70ed42b..676164a230
--- a/share/html/Elements/ShowCustomFields
+++ b/share/html/Elements/ShowCustomFields
@@@ -65,10 -65,9 +65,14 @@@
  % push @classes, 'unset-field' if not $count;
  % $m->callback( CallbackName => 'ModifyFieldClasses', CustomField => $CustomField,
  %               Object => $Object, Classes => \@classes, Grouping => $Grouping );
 -  <tr class="<% join(' ', @classes) %>" id="CF-<%$CustomField->id%>-ShowRow">
 -    <td class="label" title="<% $CustomField->EntryHint %>"><% $CustomField->Name %>:</td>
 -    <td class="value<% $count ? '' : ' no-value' %>">
 +  <div class="form-row <% join(' ', @classes) %>" id="CF-<%$CustomField->id%>-ShowRow">
-     <div class="label col-2"><% $CustomField->Name %>:</div>
++    <div class="label col-2"><% $CustomField->Name %>:
++%   if ( $CustomField->EntryHint ) {
++      <span class="far fa-question-circle icon-helper" data-toggle="tooltip" data-placement="top" data-original-title="<% $CustomField->EntryHint %>"></span>
++%   }
++    </div>
 +    <div class="value col-10 <% $count ? '' : ' no-value' %>">
 +      <span class="current-value">
  % unless ( $count ) {
  <&|/l&>(no value)</&>
  % } elsif ( $count == 1 ) {
diff --cc share/html/Elements/ShowHistoryHeader
index 16dd216b4a,2c65a5c076..1fc930f342
--- a/share/html/Elements/ShowHistoryHeader
+++ b/share/html/Elements/ShowHistoryHeader
@@@ -106,31 -96,9 +106,31 @@@ if ( $ShowDisplayModes or $ShowTitle o
          }
      }
  
 +    # Don't need to reverse history when showing a single transaction
 +    unless ( $SingleTransaction ) {
 +        push( @elements, qq{<a href="?ForceShowHistory=1;ReverseTxns=$reverse_txns;id=} .
 +                         $Object->id.qq{#$histid">} .
 +                         loc("Reverse history order") .
 +                         qq{</a>} );
 +    }
 +
 +    my $titleright;
 +    if ( @elements ) {
 +        # build the new link
 +        my $alt = loc('Options');
 +        $titleright = qq{<div class="btn-group dropdown"><a id="history-dropdown" href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><span class="fas fa-cog icon-bordered fa-2x" alt="$alt" data-toggle="tooltip" data-placement="top" data-original-title="$alt"></span></a><ul class="dropdown-menu dropdown-menu-right">};
 +
 +        # foreach of the elements, build a new <li>$element</li> and append to the output.
 +        foreach my $element ( @elements ) {
 +            $titleright .= qq{<li class="dropdown-item">$element</li>};
 +        }
 +
 +        $titleright .= q{</ul></div>};
 +    }
 +
  </%perl>
- % $m->callback( CallbackName => 'BeforeTitle', %ARGS, title => \$title, titleright => \$titleright );
+ % $m->callback( CallbackName => 'BeforeTitle', %ARGS, title => \$title, titleright => \$titleright, ARGSRef => \%ARGS );
 -<& /Widgets/TitleBoxStart, title => $title, titleright_raw => $titleright &>
 +<& /Widgets/TitleBoxStart, title => $title, titleright_raw => $titleright, class => 'fullwidth' &>
  % }
  
  <div class="history-container">
diff --cc share/html/Search/Bulk.html
index 9d066d86ba,e5be348696..5e8d503278
--- a/share/html/Search/Bulk.html
+++ b/share/html/Search/Bulk.html
@@@ -79,90 -71,29 +79,90 @@@
  
  <hr />
  
 -<& /Elements/Submit, Label => loc('Update'), CheckboxNameRegex => '/^UpdateTicket(All)?$/', CheckAll => 1, ClearAll => 1 &>
 -<br />
 +<div class="form-row">
 +  <div class="col-12">
 +    <& /Elements/Submit, Label => loc('Update'), CheckboxNameRegex => '/^UpdateTicket(All)?$/', CheckAll => 1, ClearAll => 1 &>
 +  </div>
 +</div>
 +
  <&|/Widgets/TitleBox, title => $title &>
 -<table>
 -<tr>
 -<td valign="top">
 -<table>
 -<tr><td class="label"> <&|/l&>Make Owner</&>: </td>
 -<td class="value"> <& /Elements/SelectOwner, Name => "Owner", Default => $ARGS{Owner} || '' &>
 -<label>(<input type="checkbox" class="checkbox" name="ForceOwnerChange"
 -<% $ARGS{ForceOwnerChange} ? 'checked="checked"' : '' %> /> <&|/l&>Force change</&>)</label></td></tr>
 -<tr><td class="label"> <&|/l&>Add Requestor</&>: </td>
 -<td class="value"> <& /Elements/EmailInput, Name => "AddRequestor", Size=> 20, Default => $ARGS{AddRequestor}, AutocompleteType => 'Principals' &> </td></tr>
 -<tr><td class="label"> <&|/l&>Remove Requestor</&>: </td>
 -<td class="value"> <& /Elements/EmailInput, Name => "DeleteRequestor", Size=> 20, Default => $ARGS{DeleteRequestor}, AutocompleteType => 'Principals' &> </td></tr>
 -<tr><td class="label"> <&|/l&>Add Cc</&>: </td>
 -<td class="value"> <& /Elements/EmailInput, Name => "AddCc", Size=> 20, Default => $ARGS{AddCc}, AutocompleteType => 'Principals' &> </td></tr>
 -<tr><td class="label"> <&|/l&>Remove Cc</&>: </td>
 -<td class="value"> <& /Elements/EmailInput, Name => "DeleteCc", Size=> 20, Default => $ARGS{DeleteCc}, AutocompleteType => 'Principals' &> </td></tr>
 -<tr><td class="label"> <&|/l&>Add AdminCc</&>: </td>
 -<td class="value"> <& /Elements/EmailInput, Name => "AddAdminCc", Size=> 20, Default => $ARGS{AddAdminCc}, AutocompleteType => 'Principals' &> </td></tr>
 -<tr><td class="label"> <&|/l&>Remove AdminCc</&>: </td>
 -<td class="value"> <& /Elements/EmailInput, Name => "DeleteAdminCc", Size=> 20, Default => $ARGS{DeleteAdminCc}, AutocompleteType => 'Principals' &> </td></tr>
 +  <div class="form-row">
 +    <div class="col-6">
 +
 +      <div class="form-row">
 +        <div class="label col-3">
 +          <&|/l&>Make Owner</&>:
 +        </div>
 +        <div class="value col-9">
 +          <& /Elements/SelectOwner, Name => "Owner", Default => $ARGS{Owner} || '' &>
 +        </div>
 +      </div>
 +
 +      <div class="form-row">
 +        <div class="label col-3"></div>
 +        <div class="value col-9">
 +          <div class="custom-control custom-checkbox">
 +            <input type="checkbox" id="ForceOwnerChange" class="checkbox custom-control-input" name="ForceOwnerChange"
 +              <% $ARGS{ForceOwnerChange} ? 'checked="checked"' : '' %>
 +            />
 +            <label for="ForceOwnerChange" class="custom-control-label">(<&|/l&>Force change</&>)</label>
 +          </div>
 +        </div>
 +      </div>
 +
 +      <div class="form-row">
 +        <div class="label col-3">
 +          <&|/l&>Add Requestor</&>:
 +        </div>
 +        <div class="value col-9">
-           <& /Elements/EmailInput, Name => "AddRequestor", Size=> 20, Default => $ARGS{AddRequestor} &>
++          <& /Elements/EmailInput, Name => "AddRequestor", Size=> 20, Default => $ARGS{AddRequestor}, AutocompleteType => 'Principals' &>
 +        </div>
 +      </div>
 +
 +      <div class="form-row">
 +        <div class="label col-3">
 +          <&|/l&>Remove Requestor</&>:
 +        </div>
 +        <div class="value col-9">
-           <& /Elements/EmailInput, Name => "DeleteRequestor", Size=> 20, Default => $ARGS{DeleteRequestor} &>
++          <& /Elements/EmailInput, Name => "DeleteRequestor", Size=> 20, Default => $ARGS{DeleteRequestor}, AutocompleteType => 'Principals' &>
 +        </div>
 +      </div>
 +
 +      <div class="form-row">
 +        <div class="label col-3">
 +          <&|/l&>Add Cc</&>:
 +        </div>
 +        <div class="value col-9">
-           <& /Elements/EmailInput, Name => "AddCc", Size=> 20, Default => $ARGS{AddCc} &>
++          <& /Elements/EmailInput, Name => "AddCc", Size=> 20, Default => $ARGS{AddCc}, AutocompleteType => 'Principals' &>
 +        </div>
 +      </div>
 +
 +      <div class="form-row">
 +        <div class="label col-3">
 +          <&|/l&>Remove Cc</&>:
 +        </div>
 +        <div class="value col-9">
-           <& /Elements/EmailInput, Name => "DeleteCc", Size=> 20, Default => $ARGS{DeleteCc} &>
++          <& /Elements/EmailInput, Name => "DeleteCc", Size=> 20, Default => $ARGS{DeleteCc}, AutocompleteType => 'Principals' &>
 +        </div>
 +      </div>
 +
 +      <div class="form-row">
 +        <div class="label col-3">
 +          <&|/l&>Add AdminCc</&>:
 +        </div>
 +        <div class="value col-9">
-           <& /Elements/EmailInput, Name => "AddAdminCc", Size=> 20, Default => $ARGS{AddAdminCc} &>
++          <& /Elements/EmailInput, Name => "AddAdminCc", Size=> 20, Default => $ARGS{AddAdminCc}, AutocompleteType => 'Principals' &>
 +        </div>
 +      </div>
 +
 +      <div class="form-row">
 +        <div class="label col-3">
 +          <&|/l&>Remove AdminCc</&>:
 +        </div>
 +        <div class="value col-9">
-           <& /Elements/EmailInput, Name => "DeleteAdminCc", Size=> 20, Default => $ARGS{DeleteAdminCc} &>
++          <& /Elements/EmailInput, Name => "DeleteAdminCc", Size=> 20, Default => $ARGS{DeleteAdminCc}, AutocompleteType => 'Principals' &>
 +        </div>
 +      </div>
  
  % my $single_roles = RT::CustomRoles->new($session{CurrentUser});
  % $single_roles->LimitToSingleValue;
@@@ -182,155 -109,74 +182,155 @@@
  % $multi_roles->LimitToMultipleValue;
  % $multi_roles->LimitToObjectId($_) for keys %$seen_queues;
  % while (my $role = $multi_roles->Next) {
 -<tr>
 -<td class="label"> <&|/l, $role->Name &>Add [_1]</&>: </td>
 -<td class="value"> <& /Elements/MultiUserRoleInput, role => $role, Name => "AddRT::CustomRole-" . $role->Id, Size => 20, Default => $ARGS{"AddRT::CustomRole-" . $role->Id}, AutocompleteMultiple => 0 &> </td>
 -</tr>
 -<tr>
 -<td class="label"> <&|/l, $role->Name &>Remove [_1]</&>: </td>
 -<td class="value"> <& /Elements/MultiUserRoleInput, role => $role, Name => "DeleteRT::CustomRole-" . $role->Id, Size => 20, Default => $ARGS{"DeleteRT::CustomRole-" . $role->Id}, AutocompleteMultiple => 0 &> </td>
 -</tr>
 +      <div class="form-row">
 +        <div class="label col-3">
 +          <&|/l, $role->Name &>Add [_1]</&>:
 +        </div>
 +        <div class="value col-9">
-           <& /Elements/EmailInput, Name => "AddRT::CustomRole-" . $role->Id, Size=> 20, Default => $ARGS{"AddRT::CustomRole-" . $role->Id} &>
++          <& /Elements/MultiUserRoleInput, role => $role, Name => "AddRT::CustomRole-" . $role->Id, Size=> 20, Default => $ARGS{"AddRT::CustomRole-" . $role->Id}, AutocompleteMultiple => 0 &>
 +        </div>
 +      </div>
 +
 +      <div class="form-row">
 +        <div class="label col-3">
 +          <&|/l, $role->Name &>Remove [_1]</&>:
 +        </div>
 +        <div class="value col-9">
-           <& /Elements/EmailInput, Name => "DeleteRT::CustomRole-" . $role->Id, Size=> 20, Default => $ARGS{"DeleteRT::CustomRole-" . $role->Id} &>
++          <& /Elements/MultiUserRoleInput, role => $role, Name => "DeleteRT::CustomRole-" . $role->Id, Size=> 20, Default => $ARGS{"DeleteRT::CustomRole-" . $role->Id}, AutocompleteMultiple => 0 &>
 +        </div>
 +      </div>
  % }
  
 -</table>
 -</td>
 -<td valign="top">
 -<table>
 -<tr><td class="label"> <&|/l&>Make subject</&>: </td>
 -<td class="value"> <input name="Subject" size="20" value="<% $ARGS{Subject} || '' %>"/> </td></tr>
 -<tr><td class="label"> <&|/l&>Make priority</&>: </td>
 -<td class="value"> <& /Elements/SelectPriority, Name => "Priority", Default => $ARGS{Priority} &> </td></tr>
 -<tr><td class="label"> <&|/l&>Make queue</&>: </td>
 -<td class="value"> <& /Elements/SelectQueue, Name => "Queue", Default => $ARGS{Queue} &> </td></tr>
 -<tr><td class="label"> <&|/l&>Make Status</&>: </td>
 -<td class="value"> <& /Ticket/Elements/SelectStatus, Name => "Status", Default => $ARGS{Status}, Queues => $seen_queues &> </td></tr>
 +    </div>
 +    <div class="col-6">
 +      <div class="form-row">
 +        <div class="label col-3">
 +          <&|/l&>Make subject</&>:
 +        </div>
 +        <div class="value col-9">
 +          <input class="form-control" type="text" name="Subject" size="20" value="<% $ARGS{Subject} || '' %>"/>
 +        </div>
 +      </div>
 +
 +      <div class="form-row">
 +        <div class="label col-3">
 +          <&|/l&>Make priority</&>:
 +        </div>
 +        <div class="value col-9">
 +          <& /Elements/SelectPriority, Name => "Priority", Default => $ARGS{Priority}, Queues => $seen_queues &>
 +        </div>
 +      </div>
 +
 +      <div class="form-row">
 +        <div class="label col-3">
 +          <&|/l&>Make queue</&>:
 +        </div>
 +        <div class="value col-9">
 +          <& /Elements/SelectQueue, Name => "Queue", Default => $ARGS{Queue} &>
 +        </div>
 +      </div>
 +
 +      <div class="form-row">
 +        <div class="label col-3">
 +          <&|/l&>Make Status</&>:
 +        </div>
 +        <div class="value col-9">
 +          <& /Ticket/Elements/SelectStatus, Name => "Status", Default => $ARGS{Status}, Queues => $seen_queues &>
 +        </div>
 +      </div>
 +
  % if ($hasSLA) {
 -<tr><td class="label"> <&|/l&>Make SLA</&>: </td>
 -<td class="value"> <& /Elements/SelectSLA, Name => "SLA", Default => $ARGS{SLA} &> </td></tr>
 +      <div class="form-row">
 +        <div class="label col-3">
 +          <&|/l&>Make SLA</&>:
 +        </div>
 +        <div class="value col-9">
 +          <& /Elements/SelectSLA, Name => "SLA", Default => $ARGS{SLA} &>
 +        </div>
 +      </div>
  % }
 -<tr><td class="label"> <&|/l&>Make date Starts</&>: </td>
 -<td class="value"> <& /Elements/SelectDate, Name => "Starts_Date", Default => $ARGS{Starts_Date} || '' &> </td></tr>
 -<tr><td class="label"> <&|/l&>Make date Started</&>: </td>
 -<td class="value"> <& /Elements/SelectDate, Name => "Started_Date", Default => $ARGS{Started_Date} || '' &> </td></tr>
 -<tr><td class="label"> <&|/l&>Make date Told</&>: </td>
 -<td class="value"> <& /Elements/SelectDate, Name => "Told_Date", Default => $ARGS{Told_Date} || '' &> </td></tr>
 -<tr><td class="label"> <&|/l&>Make date Due</&>: </td>
 -<td class="value"> <& /Elements/SelectDate, Name => "Due_Date", Default => $ARGS{Due_Date} || '' &> </td></tr>
 -</table>
 -
 -</td>
 -</tr>
 -</table>
 +
 +      <div class="form-row">
 +        <div class="label col-3">
 +          <&|/l&>Make date Starts</&>:
 +        </div>
 +        <div class="value col-9">
 +          <& /Elements/SelectDate, Name => "Starts_Date", Default => $ARGS{Starts_Date} || '' &>
 +        </div>
 +      </div>
 +
 +      <div class="form-row">
 +        <div class="label col-3">
 +          <&|/l&>Make date Started</&>:
 +        </div>
 +        <div class="value col-9">
 +          <& /Elements/SelectDate, Name => "Started_Date", Default => $ARGS{Started_Date} || '' &>
 +        </div>
 +      </div>
 +
 +      <div class="form-row">
 +        <div class="label col-3">
 +          <&|/l&>Make date Told</&>:
 +        </div>
 +        <div class="value col-9">
 +          <& /Elements/SelectDate, Name => "Told_Date", Default => $ARGS{Told_Date} || '' &>
 +        </div>
 +      </div>
 +
 +      <div class="form-row">
 +        <div class="label col-3">
 +          <&|/l&>Make date Due</&>:
 +        </div>
 +        <div class="value col-9">
 +          <& /Elements/SelectDate, Name => "Due_Date", Default => $ARGS{Due_Date} || '' &>
 +        </div>
 +      </div>
 +    </div>
 +  </div>
  </&>
  <&| /Widgets/TitleBox, title => loc('Add comments or replies to selected tickets') &>
 -<table>
 -<tr><td align="right"><&|/l&>Update Type</&>:</td>
 -<td><select name="UpdateType" id="UpdateType">
 -  <option value="private" <% $ARGS{UpdateType} && $ARGS{UpdateType} eq 'private' ? 'selected="selected"' : '' %> ><&|/l&>Comments (Not sent to requestors)</&></option>
 -<option value="response" <% $ARGS{UpdateType} && $ARGS{UpdateType} eq 'response' ? 'selected="selected"' : '' %>><&|/l&>Reply to requestors</&></option>
 -</select> 
 -</td></tr>
 -<tr>
 -    <td align="right"><&|/l&>Subject</&>:</td>
 -    <td>
 -        <input name="UpdateSubject" size="60" value="<% $ARGS{UpdateSubject} || "" %>" />
 +  <div class="form-row">
 +    <div class="form-group label col-2">
 +      <&|/l&>Update Type</&>:
 +    </div>
 +    <div class="value col-9">
 +      <select class="form-control selectpicker" name="UpdateType" id="UpdateType">
 +          <option value="private" <% $ARGS{UpdateType} && $ARGS{UpdateType} eq 'private' ? 'selected="selected"' : '' %> ><&|/l&>Comments (Not sent to requestors)</&></option>
 +        <option value="response" <% $ARGS{UpdateType} && $ARGS{UpdateType} eq 'response' ? 'selected="selected"' : '' %>><&|/l&>Reply to requestors</&></option>
 +      </select> 
 +    </div>
 +  </div>
 +
 +  <div class="form-row">
 +    <div class="form-group label col-2">
 +      <&|/l&>Subject</&>:
 +    </div>
 +    <div class="value col-9">
 +      <input class="form-control" type="text" name="UpdateSubject" size="60" value="<% $ARGS{UpdateSubject} || "" %>" />
 +    </div>
 +  </div>
  % $m->callback( %ARGS, CallbackName => 'AfterUpdateSubject' );
 -    </td>
 -</tr>
 +
  % $m->callback( CallbackName => 'BeforeTransactionCustomFields', CustomFields => $TxnCFs );
  % while (my $CF = $TxnCFs->Next()) {
 -<tr>
 -<td align="right"><% $CF->Name %>:</td>
 -<td><& /Elements/EditCustomField,
 -    CustomField => $CF,
 -    Object => RT::Transaction->new( $session{'CurrentUser'} ),
 -    &><em><% $CF->EntryHint // '' %></em></td>
 -</td></tr>
 +  <div class="form-row">
 +    <div class="form-group label col-2">
 +      <span class="name"><% $CF->Name %>:</span>
 +% if ( $CF->EntryHint ) {
 +      <span class="far fa-question-circle icon-helper" data-toggle="tooltip" data-placement="top" data-original-title="<% $CF->EntryHint %>"></span>
 +% }
 +    </div>
 +    <div class="value col-9">
 +      <& /Elements/EditCustomField,
 +        CustomField => $CF,
 +        Object => RT::Transaction->new( $session{'CurrentUser'} ),
 +      &>
 +    </div>
 +  </div>
  % } # end if while
  
 - <tr><td class="labeltop"><&|/l&>Message</&>:</td>
 - <td class="messagebox-container action-<% $ARGS{UpdateType} || 'private' %>">
 +  <div class="form-group">
 +    <div class="messagebox-container action-<% $ARGS{UpdateType} || 'private' %>">
  % $m->callback( %ARGS, CallbackName => 'BeforeMessageBox' );
  %# Currently, bulk update always starts with Comment not Reply selected, so we check this unconditionally
  % my $IncludeSignature = RT->Config->Get('MessageBoxIncludeSignatureOnComment');
diff --cc share/html/Search/Elements/PickBasics
index 1cb404c03a,4b02f3ed00..155ea71754
--- a/share/html/Search/Elements/PickBasics
+++ b/share/html/Search/Elements/PickBasics
@@@ -50,485 -50,193 +50,486 @@@
  % }
  <%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 => '/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 => 'Queue',
 -        Field => loc('Queue'),
 -        Op => {
 -            Type => 'component',
 -            Path => '/Elements/SelectMatch',
 -            Arguments => { Default => '=' },
 -        },
 -        Value => {
 -            Type => 'component',
 -            Path => '/Elements/SelectQueue',
 -            Arguments => { NamedValues => 1, },
 -        },
 -    },
 -    {
 -        Name => 'Status',
 -        Field => loc('Status'),
 -        Op => {
 -            Type => 'component',
 -            Path => '/Elements/SelectBoolean',
 -            Arguments => { TrueVal=> '=', FalseVal => '!=' },
 -        },
 -        Value => {
 -            Type => 'component',
 -            Path => '/Ticket/Elements/SelectStatus',
 -            Arguments => { SkipDeleted => 1, Queues => \%queues, ShowActiveInactive => 1 },
 -        },
 -    },
 -    {
 -        Name => 'SLA',
 -        Field => loc('SLA'),
 -        Op => {
 -            Type => 'component',
 -            Path => '/Elements/SelectMatch',
 -            Arguments => { Default => '=' },
 -        },
 -        Value => {
 -            Type => 'component',
 -            Path => '/Elements/SelectSLA',
 -            Arguments => { NamedValues => 1 },
 -        },
 -    },
 -    {
 -        Name => 'Actor',
 -        Field => {
 -            Type    => 'select',
 -            Options => [
 -                Owner => loc('Owner'),
 -                Creator => loc('Creator'),
 -                LastUpdatedBy => loc('Last updated by'),
 -                UpdatedBy => loc('Updated by'),
 -            ],
 +my @lines;
 +
 +if ( $Class eq 'RT::Transactions' ) {
 +    my $cfs = RT::CustomFields->new( $session{'CurrentUser'} );
 +    $cfs->LimitToLookupType( $ObjectType->CustomFieldLookupType );
 +    if ( %queues && $ObjectType eq 'RT::Ticket' ) {
 +        my @ids = 0;
 +        for my $name ( keys %queues ) {
 +            my $queue = RT::Queue->new( $session{CurrentUser} );
 +            $queue->Load($name);
 +            push @ids, $queue->id if $queue->id;
 +        }
 +        $cfs->LimitToAdded( @ids );
 +    }
 +
 +    @lines = (
 +        {
 +            Name => 'id',
 +            Field => loc('id'),
 +            Op => {
 +                Type => 'component',
 +                Path => '/Elements/SelectEqualityOperator',
 +            },
 +            Value => { Type => 'text', Size => 5 }
 +        },
 +        {
 +            Name => 'Attachment',
 +            Field => {
 +                Type => 'component',
 +                Path => '/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 => 'Creator',
 +            Field => loc('Creator'),
 +            Op => {
 +                Type => 'component',
 +                Path => '/Elements/SelectBoolean',
 +                Arguments => { TrueVal=> '=', FalseVal => '!=' },
 +            },
 +            Value => {
 +                Type => 'component',
 +                Path => '/Elements/EmailInput',
 +                Arguments => { AutocompleteReturn => 'Name' },
 +            },
 +        },
 +        {
 +            Name => 'Created',
 +            Field => loc('Created'),
 +            Op => {
 +                Type => 'component',
 +                Path => '/Elements/SelectDateRelation',
 +            },
 +            Value => {
 +                Type => 'component',
 +                Path => '/Elements/SelectDate',
 +                Arguments => { ShowTime => 0, Default => '' },
 +            },
          },
 -        Op => {
 -            Type => 'component',
 -            Path => '/Elements/SelectBoolean',
 -            Arguments => { TrueVal=> '=', FalseVal => '!=' },
 -        },
 -        Value => {
 -            Type => 'component',
 -            Path => '/Elements/SelectOwner',
 -            Arguments => { ValueAttribute => 'Name', Queues => \%queues },
 -        },
 -    },
 -    {
 -        Name => 'Watcher',
 -        Field => {
 -            Type => 'component',
 -            Path => 'SelectPersonType',
 -            Arguments => { Default => 'Requestor' },
 -        },
 -        Op => {
 -            Type => 'component',
 -            Path => '/Elements/SelectMatch',
 -            Arguments => { IncludeShallow => 1 },
 -        },
 -        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 => '/Elements/SelectDateType',
 -        },
 -        Op => {
 -            Type => 'component',
 -            Path => '/Elements/SelectDateRelation',
 -        },
 -        Value => {
 -            Type => 'component',
 -            Path => '/Elements/SelectDate',
 -            Arguments => { ShowTime => 0, Default => '' },
 -        },
 -    },
 -    {
 -        Name => 'Time',
 -        Field => {
 -            Type    => 'select',
 -            Options => [
 -                TimeWorked => loc('Time Worked'),
 -                TimeEstimated => loc('Time Estimated'),
 -                TimeLeft => loc('Time Left'),
 +        {
 +            Name => 'TimeTaken',
 +            Field => loc('Time Taken'),
 +            Op => {
 +                Type => 'component',
 +                Path => '/Elements/SelectEqualityOperator',
 +            },
 +            Value => [
 +                {
 +                    Type => 'component',
 +                    Path => '/Elements/EditTimeValue',
 +                },
              ],
          },
 -        Op => {
 -            Type => 'component',
 -            Path => '/Elements/SelectEqualityOperator',
 -        },
 -        Value => [
 -            { Type => 'text', Size => 5 },
 -            {
 -                Type => 'component',
 -                Path => '/Elements/SelectTimeUnits',
 -            },
 -        ],
 -    },
 -    {
 -        Name => 'Priority',
 -        Field => {
 -            Type    => 'select',
 -            Options => [
 -                Priority => loc('Priority'),
 -                InitialPriority => loc('Initial Priority'),
 -                FinalPriority => loc('Final Priority'),
 +        {
 +            Name => 'Type',
 +            Field => loc('Type'),
 +            Op => {
 +                Type => 'component',
 +                Path => '/Elements/SelectBoolean',
 +                Arguments => { TrueVal=> '=', FalseVal => '!=' },
 +            },
 +            Value => {
 +                Type    => 'select',
 +                Options => [
 +                    # The key could be ObjectType-Type-Field or Type-Field or Type.
 +                    # We just want Type
 +                    '' => '-',
 +                    map {  $_ => loc($_) } List::MoreUtils::uniq map { s/RT::.*?-//; s/-.*//; $_ } sort keys %RT::Transaction::_BriefDescriptions,
 +                ],
 +            },
 +        },
 +        {
 +            Name => 'Field',
 +            Field => loc('Field'),
 +            Op => {
 +                Type => 'component',
 +                Path => '/Elements/SelectMatch',
 +            },
 +            Value => { Type => 'text', Size => 5 }
 +        },
 +        {
 +            Name => 'OldValue',
 +            Field => loc('Old Value'),
 +            Op => {
 +                Type => 'component',
 +                Path => '/Elements/SelectMatch',
 +            },
 +            Value => { Type => 'text', Size => 5 }
 +        },
 +        {
 +            Name => 'NewValue',
 +            Field => loc('New Value'),
 +            Op => {
 +                Type => 'component',
 +                Path => '/Elements/SelectMatch',
 +            },
 +            Value => { Type => 'text', Size => 5 }
 +        },
 +        {
 +            Name => 'CFName',
 +            Field => loc('CF Name'),
 +            Op => {
 +                Type => 'component',
 +                Path => '/Elements/SelectBoolean',
 +                Arguments => { TrueVal=> '=', FalseVal => '!=' },
 +            },
 +            Value => {
 +                Type    => 'select',
 +                Options => [ '' => '-', map {  $_->Name => $_->Name } @{$cfs->ItemsArrayRef} ],
 +            },
 +        },
 +        {
 +            Name => 'OldCFValue',
 +            Field => loc('Old CF Value'),
 +            Op => {
 +                Type => 'component',
 +                Path => '/Elements/SelectMatch',
 +            },
 +            Value => { Type => 'text', Size => 5 }
 +        },
 +        {
 +            Name => 'NewCFValue',
 +            Field => loc('New CF Value'),
 +            Op => {
 +                Type => 'component',
 +                Path => '/Elements/SelectMatch',
 +            },
 +            Value => { Type => 'text', Size => 5 }
 +        },
 +    );
 +}
 +elsif ( $Class eq 'RT::Assets' ) {
 +    @lines = (
 +        {
 +            Name => 'id',
 +            Field => loc('id'),
 +            Op => {
 +                Type => 'component',
 +                Path => '/Elements/SelectEqualityOperator',
 +            },
 +            Value => { Type => 'text', Size => 5 }
 +        },
 +        {
 +            Name => 'Attachment',
 +            Field => {
 +                Type => 'component',
 +                Path => '/Elements/SelectAttachmentField',
 +                Arguments => { Class => 'RT::Assets' },
 +            },
 +            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', Class => 'RT::Assets' },
 +            },
 +            Op => {
 +                Type => 'component',
 +                Path => '/Elements/SelectMatch',
 +            },
 +            Value => { Type => 'text', Size => 20 }
 +        },
 +        {
 +            Name => 'WatcherGroup',
 +            Field => {
 +                Type => 'component',
 +                Path => 'SelectPersonType',
 +                Arguments => { Default => 'Owner', Suffix => 'Group', Class => 'RT::Assets' },
 +            },
 +            Op => {
 +                Type => 'select',
 +                Options => [ '=' => loc('is') ],
 +            },
 +            Value => { Type => 'text', Size => 20, "data-autocomplete" => "Groups" }
 +        },
 +        {
 +            Name => 'Date',
 +            Field => {
 +                Type => 'component',
 +                Path => '/Elements/SelectDateType',
 +                Arguments => { Class => 'RT::Assets' },
 +            },
 +            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 }
 +        },
 +    );
 +}
 +else {
 +    @lines = (
 +        {
 +            Name => 'id',
 +            Field => loc('id'),
 +            Op => {
 +                Type => 'component',
 +                Path => '/Elements/SelectEqualityOperator',
 +            },
 +            Value => { Type => 'text', Size => 5 }
 +        },
 +        {
 +            Name => 'Attachment',
 +            Field => {
 +                Type => 'component',
 +                Path => '/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 => 'Queue',
 +            Field => loc('Queue'),
 +            Op => {
 +                Type => 'component',
 +                Path => '/Elements/SelectMatch',
 +                Arguments => { Default => '=' },
 +            },
 +            Value => {
 +                Type => 'component',
 +                Path => '/Elements/SelectQueue',
 +                Arguments => { NamedValues => 1, },
 +            },
 +        },
 +        {
 +            Name => 'Lifecycle',
 +            Field => loc('Lifecycle'),
 +            Op => {
 +                Type      => 'component',
 +                Path      => '/Elements/SelectBoolean',
 +                Arguments => { TrueVal => '=', FalseVal => '!=' },
 +            },
 +            Value => {
 +                Type => 'component',
 +                Path => '/Elements/SelectLifecycle',
 +            },
 +        },
 +        {
 +            Name => 'Status',
 +            Field => loc('Status'),
 +            Op => {
 +                Type => 'component',
 +                Path => '/Elements/SelectBoolean',
 +                Arguments => { TrueVal=> '=', FalseVal => '!=' },
 +            },
 +            Value => {
 +                Type => 'component',
 +                Path => '/Ticket/Elements/SelectStatus',
 +                Arguments => { SkipDeleted => 1, Queues => \%queues, ShowActiveInactive => 1 },
 +            },
 +        },
 +        {
 +            Name => 'SLA',
 +            Field => loc('SLA'),
 +            Op => {
 +                Type => 'component',
 +                Path => '/Elements/SelectMatch',
 +                Arguments => { Default => '=' },
 +            },
 +            Value => {
 +                Type => 'component',
 +                Path => '/Elements/SelectSLA',
 +                Arguments => { NamedValues => 1 },
 +            },
 +        },
 +        {
 +            Name => 'Actor',
 +            Field => {
 +                Type    => 'select',
 +                Options => [
 +                    Owner => loc('Owner'),
 +                    Creator => loc('Creator'),
 +                    LastUpdatedBy => loc('Last updated by'),
 +                    UpdatedBy => loc('Updated by'),
 +                ],
 +            },
 +            Op => {
 +                Type => 'component',
 +                Path => '/Elements/SelectBoolean',
 +                Arguments => { TrueVal=> '=', FalseVal => '!=' },
 +            },
 +            Value => {
 +                Type => 'component',
 +                Path => '/Elements/SelectOwner',
 +                Arguments => { ValueAttribute => 'Name', Queues => \%queues },
 +            },
 +        },
 +        {
 +            Name => 'Watcher',
 +            Field => {
 +                Type => 'component',
 +                Path => 'SelectPersonType',
 +                Arguments => { Default => 'Requestor' },
 +            },
 +            Op => {
 +                Type => 'component',
 +                Path => '/Elements/SelectMatch',
++                Arguments => { IncludeShallow => 1 },
 +            },
 +            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 => '/Elements/SelectDateType',
 +            },
 +            Op => {
 +                Type => 'component',
 +                Path => '/Elements/SelectDateRelation',
 +            },
 +            Value => {
 +                Type => 'component',
 +                Path => '/Elements/SelectDate',
 +                Arguments => { ShowTime => 0, Default => '' },
 +            },
 +        },
 +        {
 +            Name => 'Time',
 +            Field => {
 +                Type    => 'select',
 +                Options => [
 +                    TimeWorked => loc('Time Worked'),
 +                    TimeEstimated => loc('Time Estimated'),
 +                    TimeLeft => loc('Time Left'),
 +                ],
 +            },
 +            Op => {
 +                Type => 'component',
 +                Path => '/Elements/SelectEqualityOperator',
 +            },
 +            Value => [
 +                {
 +                    Type => 'component',
 +                    Path => '/Elements/EditTimeValue',
 +                },
              ],
          },
 -        Op => {
 -            Type => 'component',
 -            Path => '/Elements/SelectEqualityOperator',
 -        },
 -        Value => {
 -            Type => 'component',
 -            Path => '/Elements/SelectPriority',
 -        },
 -    },
 -    {
 -        Name => 'Links',
 -        Field => { Type => 'component', Path => 'SelectLinks' },
 -        Op => {
 -            Type => 'component',
 -            Path => '/Elements/SelectBoolean',
 -            Arguments => { TrueVal=> '=', FalseVal => '!=' },
 -        },
 -        Value => { Type => 'text', Size => 5 }
 -    },
 -);
 +        {
 +            Name => 'Priority',
 +            Field => {
 +                Type    => 'select',
 +                Options => [
 +                    Priority => loc('Priority'),
 +                    InitialPriority => loc('Initial Priority'),
 +                    FinalPriority => loc('Final Priority'),
 +                ],
 +            },
 +            Op => {
 +                Type => 'component',
 +                Path => '/Elements/SelectEqualityOperator',
 +            },
 +            Value => {
 +                Type => 'component',
 +                Path => '/Elements/SelectPriority',
 +                Arguments => { Queues => \%queues, ValueAsString => 1 },
 +            },
 +        },
 +        {
 +            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 );
  
diff --cc share/html/SelfService/Create.html
index f016cef677,df3692cd8a..71fa8c0c26
--- a/share/html/SelfService/Create.html
+++ b/share/html/SelfService/Create.html
@@@ -56,66 -55,64 +56,66 @@@
  <input type="hidden" class="hidden" name="<% $key %>" value="<% $ARGS{$key} %>" />
  % }
  
 -<table width="100%">
 -<tr>
 -<td class="label">
 +<&|/Widgets/TitleBox, title => $title, class => 'ticket-info-basics' &>
 +<div>
 +<div class="queue form-row">
 +<div class="label col-2">
  <&|/l&>Queue</&>:
 -</td>
 -<td class="value">
 -    <input type="hidden" class="hidden" name="Queue" value="<%$queue_obj->id || ''%>" />
 -    <strong><%$queue_obj->Name || ''%></strong> <% $queue_obj->Description ? '('.$queue_obj->Description.')' : '' %>
 -</td>
 -</tr>
 +</div>
 +<div class="value col-8">
 +    <span class="current-value form-control"><strong><%$queue_obj->Name || ''%></strong> <% $queue_obj->Description ? '('.$queue_obj->Description.')' : '' %></span>
 +</div>
 +</div>
  <% $m->callback( CallbackName => 'AfterQueue', %ARGS, QueueObj => $queue_obj ) %>
 -<tr>
 -<td class="label">
 +<div class="form-row">
 +<div class="form-group label col-2">
  <&|/l&>Requestors</&>:
 -</td>
 -<td class="value">
 -<& /Elements/EmailInput, Name => 'Requestors', Size => '20', Default => $ARGS{Requestors} || $session{CurrentUser}->EmailAddress, AutocompleteMultiple => 1, AutocompleteType => 'Principals' &>
 -</td>
 -</tr>
 -<tr>
 -<td class="label">
 +</div>
 +<div class="value col-8">
- <& /Elements/EmailInput, Name => 'Requestors', Size => '20', Default => $ARGS{Requestors} || $session{CurrentUser}->EmailAddress, AutocompleteMultiple => RT->Config->Get('AllowUserAutocompleteForUnprivileged') ? 1 : 0 &>
++<& /Elements/EmailInput, Name => 'Requestors', Size => '20', Default => $ARGS{Requestors} || $session{CurrentUser}->EmailAddress, AutocompleteMultiple => RT->Config->Get('AllowUserAutocompleteForUnprivileged') ? 1 : 0, AutocompleteType => 'Principals' &>
 +</div>
 +</div>
 +<div class="form-row">
 +<div class="form-group label col-2">
  <&|/l&>Cc</&>:
 -</td>
 -<td class="value">
 -<& /Elements/EmailInput, Name => 'Cc', Size => '20', Default => $ARGS{Cc} || '', AutocompleteMultiple => 1, AutocompleteType => 'Principals' &>
 -</td>
 -</tr>
 -<tr>
 -<td class="label">
 +</div>
 +<div class="value col-8">
- <& /Elements/EmailInput, Name => 'Cc', Size => '20', Default => $ARGS{Cc} || '', AutocompleteMultiple => RT->Config->Get('AllowUserAutocompleteForUnprivileged') ? 1 : 0 &>
++<& /Elements/EmailInput, Name => 'Cc', Size => '20', Default => $ARGS{Cc} || '', AutocompleteMultiple => RT->Config->Get('AllowUserAutocompleteForUnprivileged') ? 1 : 0, AutocompleteType => 'Principals' &>
 +</div>
 +</div>
 +<div class="form-row">
 +<div class="form-group label col-2">
  <&|/l&>Subject</&>:
 -</td>
 -<td class="value">
 -<input name="Subject" size="60" maxsize="200" value="<%$ARGS{Subject} || ''%>" />
 -</td>
 -</tr>
 -<tr>
 -    <td colspan="2">
 -        <& /Elements/EditCustomFields,
 -            %ARGS,
 -            Object          => RT::Ticket->new($session{CurrentUser}),
 -            CustomFields    => $queue_obj->TicketCustomFields,
 -            AsTable         => 0,
 -            ForCreation     => 1,
 -            &>
 -    </td>
 -</tr>
 -<& /Ticket/Elements/AddAttachments, %ARGS, QueueObj => $queue_obj &>
 -</table>
 -<table width="100%">
 -<tr>
 -<td colspan="2">
 -<&|/l&>Describe the issue below</&>:<br />
 -<& /Elements/MessageBox, Default => $ARGS{Content} || ''  &>
 -</td>
 -</tr>
 -</table>
 -<& /Elements/Submit, Label => loc("Create ticket")&>
 +</div>
 +<div class="form-group value col-8">
 +<input class="form-control" type="text" name="Subject" size="60" maxsize="200" value="<%$ARGS{Subject} || ''%>" />
 +</div>
 +</div>
 +<div class="form-group">
 +<& /Elements/EditCustomFields,
 +    %ARGS,
 +    Object          => RT::Ticket->new($session{CurrentUser}),
 +    CustomFields    => $queue_obj->TicketCustomFields,
 +    InTable         => 1,
 +    ForCreation     => 1,
 +    LabelCols       => 2,
 +    ValueCols       => 8,
 +    &>
 +</div>
 +</div>
 +<div class="form-group">
 +  <& /Elements/MessageBox, Default => $ARGS{Content} || ''  &>
 +</div>
 +<div class="form-group">
 +  <& /Ticket/Elements/AddAttachments, %ARGS, QueueObj => $queue_obj &>
 +</div>
 +<div class="form-row">
 +  <div class="col-12">
 +    <& /Elements/Submit, Label => loc("Create ticket"), Name => 'SubmitTicket' &>
 +  </div>
 +</div>
  
 +</&>
  
  </form>
  <%args>
diff --cc share/html/Ticket/Create.html
index c0758b344e,85e04c881c..a4c8135109
--- a/share/html/Ticket/Create.html
+++ b/share/html/Ticket/Create.html
@@@ -149,47 -142,70 +149,47 @@@
  
  <div id="ticket-create-message">
    <&| /Widgets/TitleBox, title => $title, class => 'messagedetails' &>
 -<table border="0" cellpadding="0" cellspacing="0">
 +<div>
  % $m->callback(CallbackName => 'BeforeRequestors', QueueObj => $QueueObj, ARGSRef => \%ARGS);
 -<tr>
 -<td class="label">
 -<&|/l&>Requestors</&>:
 -</td>
 -<td class="value" colspan="5">
 +
 +<div class="form-row">
 +<div class="label col-2"><&|/l&>Requestors</&>:</div>
 +<div class="value col-9">
- <& /Elements/EmailInput, Name => 'Requestors', Size => undef, Default => $ARGS{Requestors} // $session{CurrentUser}->EmailAddress, AutocompleteMultiple => 1 &>
+ <& /Elements/EmailInput, Name => 'Requestors', Size => undef, Default => $ARGS{Requestors} // $session{CurrentUser}->EmailAddress, AutocompleteMultiple => 1, AutocompleteType => 'Principals' &>
  % $m->callback( CallbackName => 'AfterRequestors', QueueObj => $QueueObj, ARGSRef => \%ARGS );
 -</td>
 -</tr>
 -<tr>
 -<td class="label">
 -<&|/l&>Cc</&>:
 -</td>
 -<td class="value" colspan="5"><& /Elements/EmailInput, Name => 'Cc', Size => undef, Default => $ARGS{Cc}, AutocompleteMultiple => 1, AutocompleteType => 'Principals' &></td>
 -</tr>
 -
 -<tr>
 -  <td class="label"> </td>
 -  <td class="comment" colspan="5">
 -    <i><font size="-2">
 -      <&|/l&>(Sends a carbon-copy of this update to a comma-delimited list of email addresses. These people <strong>will</strong> receive future updates.)</&>
 -    </font></i>
 -  </td>
 -</tr>
 -
 -<tr>
 -<td class="label">
 -<&|/l&>Admin Cc</&>:
 -</td>
 -<td class="value" colspan="5"><& /Elements/EmailInput, Name => 'AdminCc', Size => undef, Default => $ARGS{AdminCc}, AutocompleteMultiple => 1, AutocompleteType => 'Principals' &></td>
 -</tr>
 -
 -<tr>
 -  <td class="label"> </td>
 -  <td class="comment" colspan="5">
 -    <i><font size="-2">
 -      <&|/l&>(Sends a carbon-copy of this update to a comma-delimited list of administrative email addresses. These people <strong>will</strong> receive future updates.)</&>
 -    </font></i>
 -  </td>
 -</tr>
 +</div>
 +</div>
 +
 +<div class="form-row">
 +<div class="label col-2"><&|/l&>Cc</&>:
 +<span class="far fa-question-circle icon-helper" data-toggle="tooltip" data-placement="top" data-original-title="<&|/l&>Sends a copy of this update to a list of email addresses. These people will receive future updates.</&>"></span>
 +</div>
- <div class="value col-9"><& /Elements/EmailInput, Name => 'Cc', Size => undef, Default => $ARGS{Cc}, AutocompleteMultiple => 1 &></div>
++<div class="value col-9"><& /Elements/EmailInput, Name => 'Cc', Size => undef, Default => $ARGS{Cc}, AutocompleteMultiple => 1, AutocompleteType => 'Principals' &></div>
 +</div>
 +
 +<div class="form-row">
 +<div class="label col-2"><&|/l&>Admin Cc</&>:
 +<span class="far fa-question-circle icon-helper" data-toggle="tooltip" data-placement="top" data-original-title="<&|/l&>Sends a copy of this update to a list of administrative email addresses. These people will receive future updates.</&>"></span>
 +</div>
- <div class="value col-9"><& /Elements/EmailInput, Name => 'AdminCc', Size => undef, Default => $ARGS{AdminCc}, AutocompleteMultiple => 1 &></div>
++<div class="value col-9"><& /Elements/EmailInput, Name => 'AdminCc', Size => undef, Default => $ARGS{AdminCc}, AutocompleteMultiple => 1, AutocompleteType => 'Principals' &></div>
 +</div>
  
  % my $roles = $QueueObj->CustomRoles;
  % $roles->LimitToMultipleValue;
 +
 +% my @hidden = $QueueObj->HiddenCustomRoleIDsForURL;
 +% $roles->Limit(FIELD => 'id', OPERATOR => 'NOT IN', VALUE => \@hidden) if @hidden;
 +
  % $m->callback( CallbackName => 'ModifyCustomRoles', ARGSRef => \%ARGS, CustomRoles => $roles );
  % while (my $role = $roles->Next) {
 -<tr>
 -<td class="label">
 -<% $role->Name %>:
 -</td>
 -<td class="value" colspan="5"><& /Elements/MultiUserRoleInput, role => $role, Default => $ARGS{$role->GroupType} &></td>
 -</tr>
 -
 +<div class="form-row">
 +  <div class="form-group label col-2"><% $role->Name %>:
  % if ($role->EntryHint) {
 -<tr>
 -  <td class="label"> </td>
 -  <td class="comment" colspan="5">
 -    <i><font size="-2">
 -      <% $role->EntryHint %>
 -    </font></i>
 -  </td>
 -</tr>
 +    <span class="far fa-question-circle icon-helper" data-toggle="tooltip" data-placement="top" data-original-title="<% $role->EntryHint %>"></span>
  % }
 +  </div>
-   <div class="form-group value col-9"><& /Elements/EmailInput, Name => $role->GroupType, Size => undef, Default => $ARGS{$role->GroupType}, AutocompleteMultiple => 1 &></div>
++  <div class="form-group value col-9"><& /Elements/MultiUserRoleInput, role => $role, Default => $ARGS{$role->GroupType} &></div>
 +</div>
  % }
  
  <& /Elements/EditCustomFields,
diff --cc share/html/Ticket/Elements/AddWatchers
index acc2669823,c5a10c7fcd..e45562ec74
--- a/share/html/Ticket/Elements/AddWatchers
+++ b/share/html/Ticket/Elements/AddWatchers
@@@ -115,28 -93,21 +115,28 @@@
  % my $counter = 4;
  % for my $email (@extras) {
  % $counter++;
 -<tr><td>
 -<&/Elements/SelectWatcherType, Name => "WatcherTypeEmail".$counter, Queue => $Ticket->QueueObj &>
 -</td><td>
 -<input type="hidden" name="WatcherAddressEmail<%$counter%>" value="<%$email->format%>">
 -<%$email->format%>
 -</td></tr>
 +  <div class="form-row">
 +    <div class="col-3">
 +      <&/Elements/SelectWatcherType, Name => "WatcherTypeEmail".$counter, Queue => $Ticket->QueueObj &>
 +    </div>
 +    <div class="col-9">
 +      <input type="hidden" name="WatcherAddressEmail<%$counter%>" value="<%$email->format%>">
 +      <span class="form-control current-value">
 +        <%$email->format%>
 +      </span>
 +    </div>
 +  </div>
  % }
  % for my $i (1 .. 3) {
 -<tr><td>
 -<&/Elements/SelectWatcherType, Name => "WatcherTypeEmail" . $i, Queue => $Ticket->QueueObj &>
 -</td><td>
 -<& /Elements/EmailInput, Name => 'WatcherAddressEmail' . $i, Size => '20', AutocompleteType => 'Principals' &>
 -</td></tr>
 +  <div class="form-row">
 +    <div class="col-3">
 +      <&/Elements/SelectWatcherType, Name => "WatcherTypeEmail" . $i, Queue => $Ticket->QueueObj &>
 +    </div>
 +    <div class="col-6">
-       <& /Elements/EmailInput, Name => 'WatcherAddressEmail' . $i, Size => '20' &>
++      <& /Elements/EmailInput, Name => 'WatcherAddressEmail' . $i, Size => '20', AutocompleteType => 'Principals' &>
 +    </div>
 +  </div>
  % }
 -</table>
  
  <%INIT>
  my ($Users, $Groups);
diff --cc share/html/Ticket/Elements/ShowSummary
index 525b23f15c,d8258f4a79..b0eb4252bc
--- a/share/html/Ticket/Elements/ShowSummary
+++ b/share/html/Ticket/Elements/ShowSummary
@@@ -236,11 -113,8 +236,15 @@@ my ($can_modify_owner) = $Ticket->Curre
  my $can_modify_people = $Ticket->CurrentUserHasRight('Watch')
                       || $Ticket->CurrentUserHasRight('WatchAsAdminCc');
  
+ $m->callback( CallbackName => 'ModifyRights', %ARGS, TicketObj => $Ticket, ARGSRef => \%ARGS,
+     CanModify => \$can_modify, CanModifyCF => \$can_modify_cf,
+     CanModifyOwner => \$can_modify_owner, CanModifyPeople => \$can_modify_people );
+ 
 +my $edit_label = $m->interp->apply_escapes( loc("Edit"), 'h' );
 +my $cancel_label = $m->interp->apply_escapes( loc("Cancel"), 'h' );
 +
 +my %inline_edit_behavior;
 +if (RT->Config->Get('InlineEditPanelBehavior')) {
 +    %inline_edit_behavior = %{ RT->Config->Get('InlineEditPanelBehavior')->{'RT::Ticket'} || {} };
 +}
  </%INIT>
diff --cc share/static/js/autocomplete.js
index fffefddb7c,a3981300d6..d4b1eb4711
--- a/share/static/js/autocomplete.js
+++ b/share/static/js/autocomplete.js
@@@ -7,39 -6,9 +7,40 @@@ window.RT.Autocomplete.Classes = 
      Groups: 'group',
      Tickets: 'tickets',
      Queues: 'queues',
 +    Articles: 'articles',
-     Assets: 'assets'
++    Assets: 'assets',
+     Principals: 'principals'
  };
  
 +Selectize.define('rt_drag_drop', function(options) {
 +    this.require('drag_drop');
 +    var self = this;
 +    self.setup = (function() {
 +        var original = self.setup;
 +        return function() {
 +            original.apply(this, arguments);
 +            self.$control.sortable('option', 'connectWith', '.selectize-input');
 +            self.$control.on('sortreceive', function(e, ui) {
 +                var input = jQuery(e.target).parent().prev('input');
 +                var self = input.selectize()[0].selectize;
 +                var value = ui.item.attr('data-value');
 +                self.createItem(value, false);
 +                self.getItem(value).children('span').text(ui.item.children('span').text());
 +                self.getItem(value).insertBefore(ui.item);
 +                ui.item.remove();
 +                self.setCaret(self.items.length);
 +            });
 +            self.$control.on('sortremove', function(e, ui) {
 +                var input = jQuery(e.target).parent().prev('input');
 +                var self = input.selectize()[0].selectize;
 +                var value = ui.item.attr('data-value');
 +                self.removeItem(value, true);
 +                self.trigger('item_remove', value, ui.item);
 +            });
 +        };
 +    })();
 +});
 +
  window.RT.Autocomplete.bind = function(from) {
  
      jQuery("input[data-autocomplete]", from).each(function(){
@@@ -50,70 -19,6 +51,70 @@@
          if (!what || !window.RT.Autocomplete.Classes[what])
              return;
  
-         if (what === 'Users' && input.is('[data-autocomplete-multiple]')) {
++        if ( (what === 'Users' || what === 'Principals') && input.is('[data-autocomplete-multiple]')) {
 +            var options = input.attr('data-options');
 +            var items = input.attr('data-items');
 +            input.selectize({
 +                plugins: ['remove_button', 'rt_drag_drop'],
 +                options: options ? JSON.parse(options) : null,
 +
 +                // If input value contains multiple items, selectize only
 +                // renders the first item somehow. Here we explicitly set
 +                // items to get around this issue.
 +                items: items ? JSON.parse(items) : null,
 +                valueField: 'value',
 +                labelField: 'label',
 +                searchField: ['label', 'value', 'text'],
 +                create: true,
 +                closeAfterSelect: true,
 +                maxItems: null,
 +                allowEmptyOption: false,
 +                openOnFocus: false,
 +                selectOnTab: true,
 +                placeholder: input.attr('placeholder'),
 +                render: {
 +                    option_create: function(data, escape) {
 +                        return '<div class="create"><strong>' + escape(data.input) + '</strong></div>';
 +                    },
 +                    option: function(data, escape) {
 +                        return '<div class="option">' + (data.selectize_option || escape(data.label)) + '</div>';
 +                    },
 +                    item: function(data, escape) {
 +                        return '<div class="item"><span>' + (data.selectize_item || escape(data.label)) + '</span></div>';
 +                    }
 +                },
 +                onItemRemove: function(value) {
 +                    // We do not want dropdown to show on removing items, but there is no such option.
 +                    // Here we temporarily lock the selectize to achieve it.
 +                    var self = input[0].selectize;
 +                    self.lock();
 +                    setTimeout( function() {
 +                        self.unlock();
 +                    },100);
 +                },
 +                load: function(input, callback) {
 +                    if (!input.length) return callback();
 +                    jQuery.ajax({
-                         url: RT.Config.WebHomePath + '/Helpers/Autocomplete/Users',
++                        url: RT.Config.WebHomePath + '/Helpers/Autocomplete/' + what,
 +                        type: 'GET',
 +                        dataType: 'json',
 +                        data: {
 +                            delim: ',',
 +                            term: input,
 +                            return: wants
 +                        },
 +                        error: function() {
 +                            callback();
 +                        },
 +                        success: function(res) {
 +                            callback(res);
 +                        }
 +                    });
 +                }
 +            });
 +            return;
 +        }
 +
          // Don't re-bind the autocompleter
          if (input.data("ui-autocomplete"))
              return;
diff --cc t/web/ticket_role_input.t
index 0000000000,59bd31bf45..e4fa7fc448
mode 000000,100644..100644
--- a/t/web/ticket_role_input.t
+++ b/t/web/ticket_role_input.t
@@@ -1,0 -1,172 +1,173 @@@
+ use strict;
+ use warnings;
+ 
+ use RT::Test tests => undef;
+ 
+ my ( $baseurl, $m ) = RT::Test->started_ok;
+ ok $m->login, 'logged in as root';
+ my $root = RT::User->new( RT->SystemUser );
+ ok( $root->Load( 'root' ), 'load root user' );
+ 
+ my $alice = RT::Test->load_or_create_user( Name => 'alice', EmailAddress => 'alice at example.com' );
+ ok( $alice->id, 'created user alice' );
+ 
+ my $bob = RT::Test->load_or_create_user( Name => 'bob', EmailAddress => 'bob at example.com' );
+ ok( $bob->id, 'created user bob' );
+ 
+ my $richard = RT::Test->load_or_create_user( Name => 'richard', EmailAddress => 'richard at example.com' );
+ ok( $richard->id, 'created user richard' );
+ 
+ my $group_foo = RT::Test->load_or_create_group( 'foo' );
+ ok( $group_foo->id, 'created group foo' );
+ 
+ my $group_admin_user = RT::Test->load_or_create_group( 'admin user' );
+ ok( $group_admin_user->id, 'created group admin user' );
+ 
+ my $queue = RT::Test->load_or_create_queue( Name => 'General' );
+ ok $queue->id, 'loaded queue General';
+ 
+ diag "Test ticket create page";
+ {
+     $m->goto_create_ticket( $queue );
+     $m->submit_form_ok(
+         {
+             form_name => 'TicketCreate',
+             fields    => {
+                 Subject    => 'test inputs on create',
+                 Content    => 'test content',
+                 Requestors => 'alice, root at localhost, group:' . $group_foo->id,
+                 Cc         => 'richard at example.com, ' . $alice->id,
+                 AdminCc    => 'group:admin user, bob',
+             },
++            button => 'SubmitTicket',
+         },
+         'submit form TicketCreate'
+     );
+     $m->content_like( qr/Ticket \d+ created/, 'created ticket' );
+ 
+     my $ticket = RT::Test->last_ticket;
+     for my $member ( $root, $alice, $group_foo ) {
+         ok( $ticket->Requestor->HasMember( $member->PrincipalObj ), 'Requestor has member ' . $member->Name );
+     }
+ 
+     for my $member ( $alice, $richard ) {
+         ok( $ticket->Cc->HasMember( $member->PrincipalObj ), 'Cc has member ' . $member->Name );
+     }
+ 
+     for my $member ( $bob, $group_admin_user ) {
+         ok( $ticket->AdminCc->HasMember( $member->PrincipalObj ), 'AdminCc has member ' . $member->Name );
+     }
+ }
+ 
+ diag "Test ticket people page";
+ {
+ 
+     my $ticket = RT::Test->create_ticket(
+         Queue   => $queue,
+         Subject => 'test inputs on people',
+         Content => 'test content',
+     );
+     $m->goto_ticket( $ticket->id, 'ModifyPeople' );
+ 
+     $m->submit_form_ok(
+         {
+             form_name => 'TicketPeople',
+             fields    => {
+                 WatcherTypeEmail1    => 'Requestor',
+                 WatcherAddressEmail1 => 'alice',
+                 WatcherTypeEmail2    => 'AdminCc',
+                 WatcherAddressEmail2 => 'group: foo',
+             },
+             button => 'SubmitTicket',
+         },
+         'submit form TicketPeople'
+     );
+ 
+     $m->text_contains( 'Added alice as Requestor for this ticket' );
+     $m->text_contains( 'Added foo as AdminCc for this ticket' );
+ 
+     ok( $ticket->Requestor->HasMember( $alice->PrincipalObj ),   'Requestor has member ' . $alice->Name );
+     ok( $ticket->AdminCc->HasMember( $group_foo->PrincipalObj ), 'AdminCc has member ' . $group_foo->Name );
+ }
+ 
+ diag "Test ticket update page";
+ {
+ 
+     my $ticket = RT::Test->create_ticket(
+         Queue   => $queue,
+         Subject => 'test inputs on update',
+         Content => 'test content',
+     );
+     $m->goto_ticket( $ticket->id, 'Update' );
+ 
+     $m->submit_form_ok(
+         {
+             form_name => 'TicketUpdate',
+             fields    => {
+                 UpdateContent => 'test content',
+                 UpdateCc      => 'alice, bob at example.com',
+                 UpdateBcc     => 'richard',
+             },
+             button => 'SubmitTicket',
+         },
+         'submit form TicketUpdate'
+     );
+     $m->content_contains('Comments added');
+ 
 -    $m->follow_link_ok( { text => 'Show' }, 'get the outgoing email page' );
++    $m->follow_link_ok( { url_regex => qr/ShowEmailRecord/ }, 'get the outgoing email page' );
+     $m->content_contains( 'CC: alice at example.com, bob at example.com' );
+     $m->content_contains( 'BCC: richard at example.com' );
+ }
+ 
+ diag "Test ticket bulk update page";
+ {
+ 
+     my @tickets = RT::Test->create_tickets(
+         {
+             Queue   => $queue,
+             Subject => 'test role inputs on bulk update',
+             Content => 'test content',
+         },
+         ( {} ) x 3
+     );
+ 
+     $m->get_ok( '/Search/Bulk.html?Rows=10&Query=Subject="test role inputs on bulk update"' );
+     $m->submit_form_ok(
+         {
+             form_name => 'BulkUpdate',
+             fields    => {
+                 AddRequestor => 'alice',
+                 AddAdminCc => 'group: admin user',
+             },
+         },
+         'submit form BulkUpdate'
+     );
+ 
+     $m->text_contains( 'Added alice as Requestor for this ticket' );
+     $m->text_contains( 'Added admin user as AdminCc for this ticket' );
+ 
+     for my $ticket ( @tickets ) {
+         ok( $ticket->Requestor->HasMember( $alice->PrincipalObj ), 'Requestor has member ' . $alice->Name );
+         ok( $ticket->AdminCc->HasMember( $group_admin_user->PrincipalObj ),
+             'AdminCc has member ' . $group_admin_user->Name );
+     }
+ 
+     $m->get_ok( '/Search/Bulk.html?Rows=10&Query=Subject="test role inputs on bulk update"' );
+     $m->submit_form_ok(
+         {
+             form_name => 'BulkUpdate',
+             fields    => {
+                 DeleteRequestor => $alice->id,
+                 DeleteAdminCc => 'group: ' . $group_admin_user->id,
+             },
+         },
+         'submit form BulkUpdate'
+     );
+     $m->text_contains( 'admin user is no longer AdminCc for this ticket' );
+     for my $ticket ( @tickets ) {
+         ok( !$ticket->AdminCc->HasMember( $group_admin_user->PrincipalObj ),
+             'AdminCc has no member ' . $group_admin_user->Name );
+     }
+ }
+ 
+ done_testing;

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


More information about the rt-commit mailing list