[Rt-commit] rt branch, 5.0-trunk, updated. rt-5.0.1-424-g8697ff3163

? sunnavy sunnavy at bestpractical.com
Fri May 28 18:37:19 EDT 2021


The branch, 5.0-trunk has been updated
       via  8697ff3163208bab0faad698447b28af8acb472a (commit)
       via  1768ea860cc0dd2e83b5bc2f92fc658870663550 (commit)
       via  1fea77105c4c683178daedd8ca07a78864c461d5 (commit)
       via  90d1e61cfe8f58b76f4e525c7dbffc776679e10e (commit)
       via  b719657e2e9d22a0e3bf5f4e204010bac53dc59e (commit)
       via  3d23d972b81d31790e67ac25bdb94f74ad1407d5 (commit)
       via  f1d9b9fa5ecf40f5ae0f32dc8f52472e4b05391e (commit)
       via  546384e2db0469ad0e21ce81228de0682c33ccf5 (commit)
       via  e3ff35b4d6742a55d111f59b8dee6fab46e8bb51 (commit)
       via  b53b91a923b7c12472f9955994b1f8a5e766df87 (commit)
       via  6fb9f202078eee7ce61e475e2fda10364dd4963c (commit)
       via  6d6246e30151cc0bba2cf2ded1c3842e7b74a96e (commit)
       via  05e042f2853e0aec4b185858816c9da39f5bb547 (commit)
       via  bede0d94a08ce08897d39ef362279be99e5cb392 (commit)
       via  5957dba5e163a6f1106bfc1d9b4b1e969f8655fb (commit)
       via  d0dd9772af523147b8be0bf5f7c9262f32f18bc4 (commit)
       via  4cdae3b5b51a2eee8b16801e932a114924a85673 (commit)
       via  cba74f8dc1d26561bfbb82359e4ad2d4bc1ce72c (commit)
       via  d32f3e84a9146c939d338679fe7b530ea392b1d5 (commit)
       via  7a51a9e3d131e41bc8d3b3185a3095aeee6fd72e (commit)
       via  764abd053bccb228adb608d0fcbb28adc19fcbcd (commit)
       via  ac4f68399b0251e642ec7d223c61ab65a07d277c (commit)
       via  8a6dcb2cffd5b979957570a86387f77faa4b44dd (commit)
       via  d967ff85f301ba1047a03f166eae3cf28a00ecb1 (commit)
       via  48d10b0803a8e7cf645565c0a470755d8c3f928e (commit)
       via  14550358c995ca10dffdfbb883749cb169771245 (commit)
       via  9b7bb8689a350f7822ef79749a57db84f46d1031 (commit)
       via  ee5c1df23fdfc83c27499903d1bacd2c76d88f48 (commit)
       via  994bfb3ad812c2b76cc701b94c09c43e794a85a8 (commit)
       via  6076e503228f7e07110ef8ecc61bc8fada4e07c5 (commit)
       via  b284ff9c49cde2aed99960348106f11e383aa0b2 (commit)
       via  36274009a65485b71bee1c1cbeb3e3d75f2c77e3 (commit)
       via  188eafbd222847431a6c071e67de3008690d74ce (commit)
       via  e717bceb24a35b94d5c0c477f1cb5f4a12eb2ebe (commit)
       via  24a380ea2ec641d0ebe53ac99d9cc16ade42ee8e (commit)
       via  b7c660f3c2859ecb335cb7dab227499ba35dbdde (commit)
       via  e73b179262aa8c7520c89521149dcb2d9e0b9572 (commit)
       via  b0bfcb602cb10368c2346f04ee993c14ace29b5c (commit)
       via  23270aa9679e756c18745a268b2e1ca6103dda39 (commit)
       via  6a7f1beb71dd4484ab55eea3254e29c1a5017342 (commit)
       via  4b7c5afbab153bae1faed2662eef484643907fca (commit)
       via  2ec8e3c200415b9e8fd0689162187df7e889bbb3 (commit)
       via  af341c4168078223d8bbb135ee374df216d2f889 (commit)
       via  3528ae2bf5df8e26ff1e204dd9e1a8157cfa6a88 (commit)
       via  26c2125b8e53ebe58c662c0bcd928a44ce7e9d21 (commit)
       via  6f4651be57584065f36c9cb44f4f6665fa9b01da (commit)
       via  8b87cf76d4827dd9e069ab0206e30465dbc35c44 (commit)
       via  4d0e0ae56edd2820914baace5d253dfba89815ad (commit)
       via  4cc816622a0e9fcc0273fe0efd409a057ffe21a4 (commit)
       via  6649b1d0c7e5401c17afbfb07e756bb931ca9869 (commit)
       via  1a51b9434d43925ca2286215677c2d2f70acaa76 (commit)
       via  77f69ec7f5eb08b961eae8a9a1d727a503f669c4 (commit)
       via  af7802e5440cfc15269c8893817639451ac860dc (commit)
       via  fec1593d8a3d8f37047a0e0b4706208f5f104736 (commit)
       via  0605d7a6667124bf2f86b558daad24c4639e3319 (commit)
       via  d5d34d1160e8591452aa2b67e2ca276d54d8ef98 (commit)
       via  c3563e76c6e23a6de9a168d7d756002839f8d273 (commit)
       via  7f044a8fc5d276b8c6c50e8dce3252251f6b1864 (commit)
       via  fda000abd117d24aecca112e9b550bedafb38eba (commit)
       via  c4ffebb48a57254c8f17081a29408207b78cf98f (commit)
       via  7e5f7277b23151e5a9743295e4c62db85e02de0a (commit)
       via  bed31a4038238472128bef092b7548b16be078be (commit)
       via  04abe154682d1c87e79d85668d37fe021e45a9d0 (commit)
       via  7bb5f6db4da543456d9290c4002c1f1c9a84878a (commit)
       via  251d3db92b90e27cbcf8697f1bfcfa33d33e69c9 (commit)
       via  01ca83fdc0092c783307b1c33b3c18e268ef435d (commit)
       via  db2e115a77bb596bd2a5801cd2ad941b82ebd927 (commit)
       via  cd949cc2568c5bf9df4eb8139f9d6d8311842a6a (commit)
       via  3c3727434c2b37f36d1170c64745887e02a0d220 (commit)
       via  53725266941c2aca7522ddea7439da52c1e899d1 (commit)
       via  a6e66eff7de39dbbf257916fec1cf0f775b27739 (commit)
       via  4f7a9bf91b481263a6b1337b480e4607c09cdd04 (commit)
       via  f73e5c340cc10f11fb43ab3301fca72a48ac5750 (commit)
       via  dc0ebd6ce24002aaae0524cf8cd8caf4b077f811 (commit)
       via  99c330d9afcf60589e0bb7aa85d53856fb19e17f (commit)
       via  4dad4d797da641ee84b724c81931b9eb8432b192 (commit)
       via  00aaad5733fa19d2cd268e3068b554eafd338c52 (commit)
       via  847aedb3237716ec5c774098dc8cb97a6349a4ac (commit)
       via  957c290eb21a3040beaaf83c5ece54320b944a86 (commit)
       via  b54749dc41bdc968cc1972ef3366c0b8f56bd506 (commit)
       via  9a44c12bd74b3c1a0f3eebe0b5bfdd42eb5da4dd (commit)
       via  57d04305b8b9a3c4880bb7c160eda6160148fb98 (commit)
       via  a80deb3d0487869ccabaa061bd78d7ce31fb7e72 (commit)
       via  0abfc2018cb799e7c19bd7cd595e95edd15a3580 (commit)
       via  3295013b07d80d5e5b35cb606b0355c24cab24a4 (commit)
       via  4035b3670f2bbc372016174eb27e06f6e4df950f (commit)
       via  e9202dbcbff9f0a31098a4ea65d831e2aba3c1ea (commit)
       via  0e0e89fd95b77459d637875eaf96da590dc4801a (commit)
       via  90bbff572425b084ec89fbecd93c82bbb2d3025b (commit)
      from  d2816b280948ded331d7f55f3e0e5846f30238b6 (commit)

Summary of changes:
 .github/workflows/github-action.yml                | 170 +++++++++++
 .travis.yml => .travis.yml.pause                   |   0
 Dockerfile                                         |   3 +
 bin/rt-crontool.in                                 | 165 ++++++-----
 configure.ac                                       |   7 +-
 docs/UPGRADING-4.4                                 |  11 +
 etc/RT_Config.pm.in                                |  17 +-
 etc/cpanfile                                       |   1 +
 etc/upgrade/4.4.5/content                          |  14 +
 lib/RT/Attribute.pm                                | 315 +++++++++++++++++++--
 lib/RT/Config.pm                                   |  10 +
 lib/RT/Crypt/Role.pm                               |   7 +-
 lib/RT/Crypt/SMIME.pm                              |  34 ++-
 lib/RT/CustomRole.pm                               |  20 +-
 lib/RT/CustomRoles.pm                              |  23 ++
 lib/RT/Interface/CLI.pm                            |  41 ++-
 lib/RT/Interface/Web.pm                            |  38 +++
 lib/RT/Interface/Web/MenuBuilder.pm                |  23 +-
 lib/RT/Principal.pm                                |   9 +
 lib/RT/Record.pm                                   |   2 +
 lib/RT/Record/Role/Roles.pm                        |  21 +-
 lib/RT/Test/Apache.pm                              |   2 +
 lib/RT/Ticket.pm                                   |   6 +-
 lib/RT/Tickets.pm                                  |   2 +-
 lib/RT/URI/{asset.pm => attribute.pm}              |  96 ++++---
 lib/RT/Users.pm                                    |  12 +
 sbin/rt-email-digest.in                            |  40 ++-
 sbin/rt-shredder.in                                |  39 ++-
 share/html/Admin/Elements/EditCustomRoles          | 170 +++++++++++
 share/html/Admin/Groups/Members.html               |  58 +++-
 .../Topics.html => Queues/CustomRoles.html}        |  13 +-
 share/html/Admin/Users/Modify.html                 |   1 +
 share/html/Admin/Users/index.html                  |   2 +
 share/html/Articles/Elements/IncludeArticle        |   2 +-
 share/html/Elements/EditCustomField                |  10 +
 .../Elements/{Section => EditCustomFieldDisplay}   |   7 +-
 share/html/Elements/RT__Article/ColumnMap          |  12 +-
 share/html/Elements/RT__CustomField/ColumnMap      |  12 +-
 share/html/Elements/RT__CustomRole/ColumnMap       |  52 ++++
 share/html/Elements/RT__Queue/ColumnMap            |   5 +
 share/html/Elements/RT__Template/ColumnMap         |   5 +
 share/html/Elements/ShowUser                       |   3 +-
 share/html/Search/Elements/EditSearches            |  88 +++++-
 share/html/Ticket/Display.html                     |   2 +-
 share/static/css/elevator-light/misc.css           |   5 +
 t/api/initialdata-roundtrip.t                      |   8 +
 t/customroles/existing-tickets.t                   |   1 +
 t/customroles/rights.t                             |   9 +-
 t/customroles/sort_order.t                         |  65 +++++
 ...2.4+fastcgi.conf.in => apache2.4+fcgid.conf.in} |  14 +-
 t/externalauth/ldap.t                              |  26 +-
 t/mail/smime/separate_certs.t                      | 137 +++++++++
 t/ticket/scrips_batch.t                            |  16 +-
 t/web/admin_user.t                                 |  28 ++
 t/web/search_linkdisplay.t                         |  44 +++
 55 files changed, 1670 insertions(+), 253 deletions(-)
 create mode 100644 .github/workflows/github-action.yml
 rename .travis.yml => .travis.yml.pause (100%)
 copy lib/RT/URI/{asset.pm => attribute.pm} (61%)
 create mode 100644 share/html/Admin/Elements/EditCustomRoles
 copy share/html/Admin/{Global/Topics.html => Queues/CustomRoles.html} (86%)
 copy share/html/Elements/{Section => EditCustomFieldDisplay} (94%)
 create mode 100644 t/customroles/sort_order.t
 copy t/data/configs/{apache2.4+fastcgi.conf.in => apache2.4+fcgid.conf.in} (72%)
 create mode 100644 t/mail/smime/separate_certs.t

- Log -----------------------------------------------------------------
commit 8697ff3163208bab0faad698447b28af8acb472a
Merge: d2816b2809 1768ea860c
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Sat May 29 04:13:47 2021 +0800

    Merge branch '4.4-trunk' into 5.0-trunk

diff --cc etc/RT_Config.pm.in
index b1b8b2b78a,14fb330658..5954410293
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@@ -1356,748 -1332,9 +1356,748 @@@ Apache::Session::* modules can be used 
  
  =cut
  
 -Set($UseSQLForACLChecks, 1);
 +Set($WebSessionClass, undef);
  
 -=item C<$TicketsItemMapSize>, C<$ShowSearchNavigation>
 +=item C<%WebSessionProperties>
 +
 +C<%WebSessionProperties> is the hash to configure class L</$WebSessionClass>
 +in case custom class is used. By default it's empty and values are picked
 +depending on the class. Make sure that it's empty if you're using DB as session
 +backend.
 +
 +=cut
 +
 +Set( %WebSessionProperties );
 +
 +=item C<$AutoLogoff>
 +
 +By default, RT's user sessions persist until a user closes his or her
 +browser. With the C<$AutoLogoff> option you can setup session lifetime
 +in minutes. A user will be logged out if he or she doesn't send any
 +requests to RT for the defined time.
 +
 +=cut
 +
 +Set($AutoLogoff, 0);
 +
 +=item C<$LogoutRefresh>
 +
 +The number of seconds to wait after logout before sending the user to
 +the login page. By default, 1 second, though you may want to increase
 +this if you display additional information on the logout page.
 +
 +=cut
 +
 +Set($LogoutRefresh, 1);
 +
 +=item C<$WebSecureCookies>
 +
 +By default, RT's session cookie isn't marked as "secure". Some web
 +browsers will treat secure cookies more carefully than non-secure
 +ones, being careful not to write them to disk, only sending them over
 +an SSL secured connection, and so on. To enable this behavior, set
 +C<$WebSecureCookies> to 1.  NOTE: You probably don't want to turn this
 +on I<unless> users are only connecting via SSL encrypted HTTPS
 +connections.
 +
 +=cut
 +
 +Set($WebSecureCookies, 0);
 +
 +=item C<$WebHttpOnlyCookies>
 +
 +Default RT's session cookie to not being directly accessible to
 +javascript.  The content is still sent during regular and AJAX requests,
 +and other cookies are unaffected, but the session-id is less
 +programmatically accessible to javascript.  Turning this off should only
 +be necessary in situations with odd client-side authentication
 +requirements.
 +
 +=cut
 +
 +Set($WebHttpOnlyCookies, 1);
 +
 +=item C<$MinimumPasswordLength>
 +
 +C<$MinimumPasswordLength> defines the minimum length for user
 +passwords. Setting it to 0 disables this check.
 +
 +=cut
 +
 +Set($MinimumPasswordLength, 5);
 +
 +=back
 +
 +=head3 External Authentication and Authorization
 +
 +RT has a built-in module for integrating with a directory service like
 +LDAP or Active Directory for authentication (login) and authorization
 +(enabling/disabling users and setting user attributes). The core configuration
 +settings for the service are listed here. Additional details are available
 +in the L<RT::Authen::ExternalAuth> module documentation.
 +
 +=over 4
 +
 +=item C<$ExternalSettings>
 +
 +This option, along with the following options, activate and configure authentication
 +via a resource external to RT. All of the configuration for your external authentication
 +service, like LDAP or Active Directory, are defined in a data structure in this option.
 +You can find full details on the configuration
 +options in the L<RT::Authen::ExternalAuth> documentation.
 +
 +=cut
 +
 +# No defaults are set for ExternalAuth because this is an optional feature.
 +
 +=item C<$ExternalAuthPriority>
 +
 +Sets the priority of authentication resources if you have multiple configured.
 +RT will attempt authorization with each resource, in order, until one succeeds or
 +no more remain. See L<RT::Authen::ExternalAuth> for details.
 +
 +=item C<$ExternalInfoPriority>
 +
 +Sets the order of resources for querying user information if you have multiple
 +configured. RT will query each resource, in order, until one succeeds or
 +no more remain. See L<RT::Authen::ExternalAuth> for details.
 +
 +=item C<$UserAutocreateDefaultsOnLogin>
 +
 +A hashref of options to set for users who are autocreated on login via
 +ExternalAuth. For example, you can automatically make "Privileged" users
 +who were authenticated and created from LDAP or Active Directory.
 +See L<RT::Authen::ExternalAuth> for details.
 +
 +=item C<$AutoCreateNonExternalUsers>
 +
 +Users should still be autocreated by RT as internal users if they
 +fail to exist in an external service; this is so requestors who
 +are not in LDAP can still be created when they email in.
 +See L<RT::Authen::ExternalAuth> for details.
 +
 +=item C<$DisablePasswordForAuthToken>
 +
 +If you have a mix of RT and federated authentication, RT can't directly
 +verify a user's password against the federated IdP. You can explicitly
 +disable the password prompt when creating a token by setting this option
 +to true (1).
 +
 +=back
 +
 +=cut
 +
 +Set($DisablePasswordForAuthToken, 0);
 +
 +=head2 Initialdata Formats
 +
 +RT supports pluggable data format parsers for F<initialdata> files.
 +
 +If you add format handlers, note that you can remove the perl entry if you
 +don't want it available. B<Removing the default perl entry may cause problems
 +installing plugins and RT updates>. If so, re-enable it temporarily.
 +
 +=over 4
 +
 +=item C<$InitialdataFormatHandlers>
 +
 +Set the C<$InitialdataFormatHandlers> to an arrayref containing a list of
 +format handler modules. The 'perl' entry is the system default, and handles
 +perl-style intialdata files.
 +
 +The JSON format handler is also available in RT, but it is not loaded by
 +default. Add it to your configuration as shown below to enable it.
 +
 +    Set( $InitialdataFormatHandlers,
 +         [
 +            'perl',
 +            'RT::Initialdata::JSON',
 +            'RT::Extension::Initialdata::Foo',
 +            ...
 +         [
 +       );
 +
 +=back
 +
 +=cut
 +
 +Set( $InitialdataFormatHandlers, [
 +    'perl',
 +]);
 +
 +
 +=head2 Development options
 +
 +=over 4
 +
 +=item C<$DevelMode>
 +
 +RT comes with a "Development mode" setting.  This setting, as a
 +convenience for developers, turns on several of development options
 +that you most likely don't want in production:
 +
 +=over 4
 +
 +=item *
 +
 +Disables CSS and JS minification and concatenation.  Both CSS and JS
 +will be instead be served as a number of individual smaller files,
 +unchanged from how they are stored on disk.
 +
 +=item *
 +
 +Uses L<Module::Refresh> to reload changed Perl modules on each
 +request.
 +
 +=item *
 +
 +Turns off Mason's C<static_source> directive; this causes Mason to
 +reload template files which have been modified on disk.
 +
 +=item *
 +
 +Turns on Mason's HTML C<error_format>; this renders compilation errors
 +to the browser, along with a full stack trace.  It is possible for
 +stack traces to reveal sensitive information such as passwords or
 +ticket content.
 +
 +=item *
 +
 +Turns off caching of callbacks; this enables additional callbacks to
 +be added while the server is running.
 +
 +=back
 +
 +=cut
 +
 +Set($DevelMode, 0);
 +
 +
 +=item C<$RecordBaseClass>
 +
 +What abstract base class should RT use for its records. You should
 +probably never change this.
 +
 +Valid values are C<DBIx::SearchBuilder::Record> or
 +C<DBIx::SearchBuilder::Record::Cachable>
 +
 +=cut
 +
 +Set($RecordBaseClass, "DBIx::SearchBuilder::Record::Cachable");
 +
 +
 +=item C<@MasonParameters>
 +
 +C<@MasonParameters> is the list of parameters for the constructor of
 +HTML::Mason's Apache or CGI Handler.  This is normally only useful for
 +debugging, e.g. profiling individual components with:
 +
 +    use MasonX::Profiler; # available on CPAN
 +    Set(@MasonParameters, (preamble => 'my $p = MasonX::Profiler->new($m, $r);'));
 +
 +=cut
 +
 +Set(@MasonParameters, ());
 +
 +=item C<$StatementLog>
 +
 +RT has rudimentary SQL statement logging support; simply set
 +C<$StatementLog> to be the level that you wish SQL statements to be
 +logged at.
 +
 +Enabling this option will also expose the SQL Queries page in the
 +Admin -> Tools menu for SuperUsers.
 +
 +=cut
 +
 +Set($StatementLog, undef);
 +
 +=back
 +
 +
 +=head1 Web interface
 +
 +=head2 Base configuration
 +
 +=over 4
 +
 +=item C<$WebDefaultStylesheet>
 +
 +This determines the default stylesheet the RT web interface will use.
 +RT ships with several themes by default:
 +
 +  elevator-light  The default light theme for RT 5
 +  elevator-dark   The dark theme for RT 5
 +
 +This value actually specifies a directory in F<share/static/css/>
 +from which RT will try to load the file main.css (which should @import
 +any other files the stylesheet needs).  This allows you to easily and
 +cleanly create your own stylesheets to apply to RT.  This option can
 +be overridden by users in their preferences.
 +
 +=cut
 +
 +Set($WebDefaultStylesheet, "elevator-light");
 +
 +=item C<$ShowMobileSite>
 +
 +Starting with RT 5.0, RT's web interface is fully responsive and
 +will render correctly on most mobile devices. However, RT also has
 +a mobile-optimized mode that shows a limited feature set
 +focused on ticket updates. To default to this site when RT is accessed
 +from a mobile device, enable this option (set to 1).
 +
 +=cut
 +
 +Set($ShowMobileSite, 0);
 +
 +=item C<$DefaultQueue>
 +
 +Use this to select the default queue name that will be used for
 +creating new tickets. You may use either the queue's name or its
 +ID. This only affects the queue selection boxes on the web interface.
 +
 +=cut
 +
 +# Set($DefaultQueue, "General");
 +
 +=item C<$RememberDefaultQueue>
 +
 +When a queue is selected in the new ticket dropdown, make it the new
 +default for the new ticket dropdown.
 +
 +=cut
 +
 +# Set($RememberDefaultQueue, 1);
 +
 +=item C<$EnableReminders>
 +
 +Hide all links and portlets related to Reminders by setting this to 0
 +
 +=cut
 +
 +Set($EnableReminders, 1);
 +
 +=item C<@CustomFieldValuesSources>
 +
 +Set C<@CustomFieldValuesSources> to a list of class names which extend
 +L<RT::CustomFieldValues::External>.  This can be used to pull lists of
 +custom field values from external sources at runtime.
 +
 +=cut
 +
 +Set(@CustomFieldValuesSources, ());
 +
 +=item C<@CustomFieldValuesCanonicalizers>
 +
 +Set C<@CustomFieldValuesCanonicalizers> to a list of class names which extend
 +L<RT::CustomFieldValues::Canonicalizer>. This can be used to rewrite
 +(canonicalize) values entered by users to fit some defined format.
 +
 +See the documentation in L<RT::CustomFieldValues::Canonicalizer> for adding
 +your own canonicalizers.
 +
 +=cut
 +
 +Set(@CustomFieldValuesCanonicalizers, qw(
 +    RT::CustomFieldValues::Canonicalizer::Uppercase
 +    RT::CustomFieldValues::Canonicalizer::Lowercase
 +));
 +
 +=item C<%CustomFieldGroupings>
 +
 +This option affects the display of ticket, user, and asset custom fields
 +in the web interface. It does not address the sorting of custom fields within
 +the groupings; that ordering is controlled by the Ticket Custom Fields tab in
 +Queue configuration in the Admin UI. Asset custom field ordering is
 +found in the Asset Custom Fields tab in Catalog configuration.
 +
 +A nested data structure defines how to group together custom fields
 +under a mix of built-in and arbitrary headings ("groupings").
 +
 +Set C<%CustomFieldGroupings> to a nested structure similar to the following:
 +
 +    Set(%CustomFieldGroupings,
 +        'RT::Ticket' => [
 +            'Grouping Name'     => ['CF Name', 'Another CF'],
 +            'Another Grouping'  => ['Some CF'],
 +            'Dates'             => ['Shipped date'],
 +        ],
 +        'RT::User' => [
 +            'Phones' => ['Fax number'],
 +        ],
 +        'RT::Asset' => [
 +            'Asset Details' => ['Serial Number', 'Manufacturer', 'Type', 'Tracking Number'],
 +            'Dates'         => ['Support Expiration', 'Issue Date'],
 +        ],
 +        'RT::Group' => [
 +            'Basics' => ['Department'],
 +        ],
 +    );
 +
 +The first level keys are record types for which CFs may be used, and the
 +values are either hashrefs or arrayrefs -- if arrayrefs, then the
 +ordering is preserved during display, otherwise groupings are displayed
 +alphabetically.  The second level keys are the grouping names and the
 +values are array refs containing a list of CF names.
 +
 +There are several special built-in groupings which RT displays in
 +specific places (usually the collapsible box of the same title).  The
 +ordering of these standard groupings cannot be modified.  You may also
 +only append Custom Fields to the list in these boxes, not reorder or
 +remove core fields.
 +
 +For C<RT::Ticket>, these groupings are: C<Basics>, C<Dates>, C<Links>, C<People>
 +
 +For C<RT::User>: C<Identity>, C<Access control>, C<Location>, C<Phones>
 +
 +For C<RT::Group>: C<Basics>
 +
 +For C<RT::Asset>: C<Basics>, C<Dates>, C<People>, C<Links>
 +
 +Extensions may also add their own built-in groupings, refer to the individual
 +extension documentation for those.
 +
 +=item C<$CanonicalizeRedirectURLs>
 +
 +Set C<$CanonicalizeRedirectURLs> to 1 to use C<$WebURL> when
 +redirecting rather than the one we get from C<%ENV>.
 +
 +Apache's UseCanonicalName directive changes the hostname that RT
 +finds in C<%ENV>.  You can read more about what turning it On or Off
 +means in the documentation for your version of Apache.
 +
 +If you use RT behind a reverse proxy, you almost certainly want to
 +enable this option.
 +
 +=cut
 +
 +Set($CanonicalizeRedirectURLs, 0);
 +
 +=item C<$CanonicalizeURLsInFeeds>
 +
 +Set C<$CanonicalizeURLsInFeeds> to 1 to use C<$WebURL> in feeds
 +rather than the one we get from request.
 +
 +If you use RT behind a reverse proxy, you almost certainly want to
 +enable this option.
 +
 +=cut
 +
 +Set($CanonicalizeURLsInFeeds, 0);
 +
 +=item C<@JSFiles>
 +
 +A list of additional JavaScript files to be included in head.
 +
 +=cut
 +
 +Set(@JSFiles, qw//);
 +
 +=item C<@CSSFiles>
 +
 +A list of additional CSS files to be included in head.
 +
 +If you're a plugin author, refer to RT->AddStyleSheets.
 +
 +=cut
 +
 +Set(@CSSFiles, qw//);
 +
 +=item C<$UsernameFormat>
 +
 +This determines how user info is displayed. 'concise' will show the
 +first of RealName, Name or EmailAddress that has a value. 'verbose' will
 +show EmailAddress, and the first of RealName or Name which is defined.
 +The default, 'role', uses 'verbose' for unprivileged users, and the Name
 +followed by the RealName for privileged users.
 +
 +=cut
 +
 +Set($UsernameFormat, "role");
 +
 +=item C<$UserSearchResultFormat>
 +
 +This controls the display of lists of users returned from the User
 +Summary Search. The display of users in the Admin interface is
 +controlled by C<%AdminSearchResultFormat>.
 +
 +=cut
 +
 +Set($UserSearchResultFormat,
 +         q{ '<a href="__WebPath__/User/Summary.html?id=__id__">__id__</a>/TITLE:#'}
 +        .q{,'<a href="__WebPath__/User/Summary.html?id=__id__">__Name__</a>/TITLE:Name'}
 +        .q{,__RealName__, __EmailAddress__}
 +);
 +
 +=item C<@UserSummaryPortlets>
 +
 +A list of portlets to be displayed on the User Summary page.
 +By default, we show all of the available portlets.
 +Extensions may provide their own portlets for this page.
 +
 +=cut
 +
 +Set(@UserSummaryPortlets, (qw/ExtraInfo CreateTicket ActiveTickets InactiveTickets UserAssets /));
 +
 +=item C<$UserSummaryExtraInfo>
 +
 +This controls what information is displayed on the User Summary
 +portal. By default the user's Real Name, Email Address and Username
 +are displayed. You can remove these or add more as needed. This
 +expects a Format string of user attributes. Please note that not all
 +the attributes are supported in this display because we're not
 +building a table.
 +
 +=cut
 +
 +Set($UserSummaryExtraInfo, "RealName, EmailAddress, Name");
 +
 +=item C<$UserSummaryTicketListFormat>
 +
 +Control the appearance of the Active and Inactive ticket lists in the
 +User Summary.
 +
 +=cut
 +
 +Set($UserSummaryTicketListFormat, q{
 +       '<B><A HREF="__WebPath__/Ticket/Display.html?id=__id__">__id__</a></B>/TITLE:#',
 +       '<B><A HREF="__WebPath__/Ticket/Display.html?id=__id__">__Subject__</a></B>/TITLE:Subject',
 +       Status,
 +       QueueName,
 +       Owner,
 +       Priority,
 +       '__NEWLINE__',
 +       '',
 +       '<small>__Requestors__</small>',
 +       '<small>__CreatedRelative__</small>',
 +       '<small>__ToldRelative__</small>',
 +       '<small>__LastUpdatedRelative__</small>',
 +       '<small>__TimeLeft__</small>'
 +});
 +
 +=item C<$WebBaseURL>, C<$WebURL>
 +
 +Usually you don't want to set these options. The only obvious reason
 +is if RT is accessible via https protocol on a non standard port, e.g.
 +'https://rt.example.com:9999'. In all other cases these options are
 +computed using C<$WebDomain>, C<$WebPort> and C<$WebPath>.
 +
 +C<$WebBaseURL> is the scheme, server and port
 +(e.g. 'http://rt.example.com') for constructing URLs to the web
 +UI. C<$WebBaseURL> doesn't need a trailing /.
 +
 +C<$WebURL> is the C<$WebBaseURL>, C<$WebPath> and trailing /, for
 +example: 'http://www.example.com/rt/'.
 +
 +=cut
 +
 +my $port = RT->Config->Get('WebPort');
 +Set($WebBaseURL,
 +    ($port == 443? 'https': 'http') .'://'
 +    . RT->Config->Get('WebDomain')
 +    . ($port != 80 && $port != 443? ":$port" : '')
 +);
 +
 +Set($WebURL, RT->Config->Get('WebBaseURL') . RT->Config->Get('WebPath') . "/");
 +
 +=item C<$WebImagesURL>
 +
 +C<$WebImagesURL> points to the base URL where RT can find its images.
 +Define the directory name to be used for images in RT web documents.
 +
 +=cut
 +
 +Set($WebImagesURL, RT->Config->Get('WebPath') . "/static/images/");
 +
 +=item C<$LogoURL>
 +
 +C<$LogoURL> points to the URL of the RT logo displayed in the web UI.
 +This can also be configured via the web UI.
 +
 +=cut
 +
 +Set($LogoURL, RT->Config->Get('WebImagesURL') . "request-tracker-logo.svg");
 +
 +=item C<$LogoLinkURL>
 +
 +C<$LogoLinkURL> is the URL that the RT logo hyperlinks to.
 +
 +=cut
 +
 +Set($LogoLinkURL, "http://bestpractical.com");
 +
 +=item C<$LogoAltText>
 +
 +C<$LogoAltText> is a string of text for the alt-text of the logo. It
 +will be passed through C<loc> for localization.
 +
 +=cut
 +
 +Set($LogoAltText, "Request Tracker logo");
 +
 +=item C<$WebNoAuthRegex>
 +
 +What portion of RT's URL space should not require authentication.  The
 +default is almost certainly correct, and should only be changed if you
 +are extending RT.
 +
 +=cut
 +
 +Set($WebNoAuthRegex, qr{^ (?:/+NoAuth/ | /+REST/\d+\.\d+/NoAuth/) }x );
 +
 +=item C<$WebFlushDbCacheEveryRequest>
 +
 +By default, RT clears its database cache after every page view.  This
 +ensures that you've always got the most current information when
 +working in a multi-process (mod_perl or FastCGI) Environment.  Setting
 +C<$WebFlushDbCacheEveryRequest> to 0 will turn this off, which will
 +speed RT up a bit, at the expense of a tiny bit of data accuracy.
 +
 +=cut
 +
 +Set($WebFlushDbCacheEveryRequest, 1);
 +
 +=item C<%ChartFont>
 +
 +The L<GD> module (which RT uses for graphs) ships with a built-in font
 +that doesn't have full Unicode support. You can use a given TrueType
 +font for a specific language by setting %ChartFont to (language =E<gt>
 +the absolute path of a font) pairs. Your GD library must have support
 +for TrueType fonts to use this option. If there is no entry for a
 +language in the hash then font with 'others' key is used.
 +
 +RT comes with two TrueType fonts covering most available languages.
 +
 +=cut
 +
 +Set(
 +    %ChartFont,
 +    'zh-cn'  => "$RT::FontPath/DroidSansFallback.ttf",
 +    'zh-tw'  => "$RT::FontPath/DroidSansFallback.ttf",
 +    'ja'     => "$RT::FontPath/DroidSansFallback.ttf",
 +    'others' => "$RT::FontPath/DroidSans.ttf",
 +);
 +
 +=item C<$ChartsTimezonesInDB>
 +
 +RT stores dates using the UTC timezone in the DB, so charts grouped by
 +dates and time are not representative. Set C<$ChartsTimezonesInDB> to 1
 +to enable timezone conversions using your DB's capabilities. You may
 +need to do some work on the DB side to use this feature, read more in
 +F<docs/customizing/timezones_in_charts.pod>.
 +
 +At this time, this feature only applies to MySQL and PostgreSQL.
 +
 +=cut
 +
 +Set($ChartsTimezonesInDB, 0);
 +
 +=item C<@ChartColors>
 +
 +An array of 6-digit hexadecimal RGB color values used for chart series.  By
 +default there are 12 distinct colors.
 +
 +=cut
 +
 +Set(@ChartColors, qw(
 +    66cc66 ff6666 ffcc66 663399
 +    3333cc 339933 993333 996633
 +    33cc33 cc3333 cc9933 6633cc
 +));
 +
 +=item C<$EnableJSChart>
 +
 +Set this to 0 to disable Chart in JavaScript.
 +
 +=cut
 +
 +Set($EnableJSChart, 1);
 +
 +=item C<$JSChartColorScheme>
 +
 +The color scheme to use for Chart in Javascript. By default it's
 +I<brewer.Paired12>.  The full list is:
 +L<https://nagix.github.io/chartjs-plugin-colorschemes/colorchart.html>
 +
 +=cut
 +
 +Set($JSChartColorScheme, 'brewer.Paired12');
 +
 +=back
 +
 +
 +
 +=head2 Home page
 +
 +=over 4
 +
 +=item C<$DefaultSummaryRows>
 +
 +C<$DefaultSummaryRows> is default number of rows displayed in for
 +search results on the front page.
 +
 +=cut
 +
 +Set($DefaultSummaryRows, 10);
 +
 +=item C<@RefreshIntervals>
 +
 +This setting defines the possible homepage and search result refresh
 +options. Each value is a number of seconds. You should not include a value
 +of C<0>, as that is always provided as an option.
 +
 +See also L</$HomePageRefreshInterval> and L</$SearchResultsRefreshInterval>.
 +
 +=cut
 +
 +Set(@RefreshIntervals, qw(120 300 600 1200 3600 7200));
 +
 +=item C<$HomePageRefreshInterval>
 +
 +C<$HomePageRefreshInterval> is default number of seconds to refresh
 +the RT home page. Choose from any value in L</@RefreshIntervals>,
 +or the default of C<0> for no automatic refresh.
 +
 +=cut
 +
 +Set($HomePageRefreshInterval, 0);
 +
 +=item C<$HomepageComponents>
 +
 +C<$HomepageComponents> is an arrayref of allowed components on a
 +user's customized homepage ("RT at a glance").
 +
 +=cut
 +
 +Set(
 +    $HomepageComponents,
 +    [
 +        qw(QuickCreate QueueList QueueListAllStatuses MyAdminQueues MySupportQueues MyReminders RefreshHomepage Dashboards SavedSearches FindUser MyAssets FindAsset FindGroup) # loc_qw
 +    ]
 +);
 +
 +=back
 +
 +
 +
 +
 +=head2 Ticket search
 +
 +=over 4
 +
 +=item C<$UseSQLForACLChecks>
 +
 +Historically, ACLs were checked on display, which could lead to empty
 +search pages and wrong ticket counts.  Set C<$UseSQLForACLChecks> to 0
 +to go back to this method; this will reduce the complexity of the
 +generated SQL statements, at the cost of the aforementioned bugs.
 +
 +=cut
 +
 +Set($UseSQLForACLChecks, 1);
 +
- =item C<$TicketsItemMapSize>
++=item C<$TicketsItemMapSize>, C<$ShowSearchNavigation>
  
  On the display page of a ticket from search results, RT provides links
  to the first, next, previous and last ticket from the results.  In
diff --cc etc/cpanfile
index b8e3159269,0000000000..a3c8324fe9
mode 100644,000000..100644
--- a/etc/cpanfile
+++ b/etc/cpanfile
@@@ -1,222 -1,0 +1,223 @@@
 +requires 'perl', '5.10.1';
 +
 +# Core dependencies
 +requires 'Apache::Session', '>= 1.53';
 +requires 'Business::Hours';
 +requires 'CGI', ($] >= 5.019003 ? '>= 4.00' : '>= 3.38');
 +requires 'CGI::Cookie', '>= 1.20';
 +requires 'CGI::Emulate::PSGI';
 +requires 'CGI::PSGI', '>= 0.12';
 +requires 'Class::Accessor::Fast';
 +requires 'Clone';
 +requires 'Convert::Color';
 +requires 'Crypt::Eksblowfish';
 +requires 'CSS::Minifier::XS';
 +requires 'CSS::Squish', '>= 0.06';
 +requires 'Data::GUID';
 +requires 'Data::ICal';
 +requires 'Data::Page::Pageset';
 +requires 'Date::Extract', '>= 0.02';
 +requires 'Date::Manip';
 +requires 'DateTime', '>= 0.44';
 +requires 'DateTime::Format::Natural', '>= 0.67';
 +requires 'DateTime::Locale', '>= 0.40, != 1.00, != 1.01';
 +requires 'DBI', '>= 1.37';
 +requires 'DBIx::SearchBuilder', '>= 1.68';
 +requires 'Devel::GlobalDestruction';
 +requires 'Devel::StackTrace', '>= 1.19, != 1.28, != 1.29';
 +requires 'Digest::base';
 +requires 'Digest::MD5', '>= 2.27';
 +requires 'Digest::SHA';
 +requires 'Email::Address', '>= 1.912';
 +requires 'Email::Address::List', '>= 0.06';
 +requires 'Encode', '>= 2.64';
 +requires 'Encode::Detect::Detector';
 +requires 'Encode::HanExtra';
 +requires 'Errno';
 +requires 'File::Glob';
 +requires 'File::ShareDir';
 +requires 'File::Spec', '>= 0.8';
 +requires 'File::Temp', '>= 0.19';
 +requires 'HTML::Entities';
 +requires 'HTML::FormatExternal';
 +requires 'HTML::FormatText::WithLinks', '>= 0.14';
 +requires 'HTML::FormatText::WithLinks::AndTables', '>= 0.06';
 +requires 'HTML::Gumbo';
 +requires 'HTML::Mason', '>= 1.43';
 +requires 'HTML::Mason::PSGIHandler', '>= 0.52';
 +requires 'HTML::Quoted';
 +requires 'HTML::RewriteAttributes', '>= 0.05';
 +requires 'HTML::Scrubber', '>= 0.08';
 +requires 'HTTP::Message', '>= 6.07';
 +requires 'IPC::Run3';
 +requires 'JavaScript::Minifier::XS';
 +requires 'JSON';
 +requires 'List::MoreUtils';
 +requires 'Locale::Maketext', '>= 1.06';
 +requires 'Locale::Maketext::Fuzzy', '>= 0.11';
 +requires 'Locale::Maketext::Lexicon', '>= 0.32';
 +requires 'Log::Dispatch', '>= 2.30';
 +requires 'LWP::Simple';
 +requires 'Mail::Header', '>= 2.12';
 +requires 'Mail::Mailer', '>= 1.57';
 +requires 'MIME::Entity', '>= 5.504';
 +requires 'MIME::Types';
 +requires 'Module::Refresh', '>= 0.03';
 +requires 'Module::Versions::Report', '>= 1.05';
 +requires 'Net::CIDR';
 +requires 'Net::IP';
++requires 'Parallel::ForkManager';
 +requires 'Plack', '>= 1.0002';
 +requires 'Plack::Handler::Starlet';
 +requires 'Pod::Select';
 +requires 'Regexp::Common';
 +requires 'Regexp::Common::net::CIDR';
 +requires 'Regexp::IPv6';
 +requires 'Role::Basic', '>= 0.12';
 +requires 'Scalar::Util';
 +requires 'Scope::Upper';
 +requires 'Storable', '>= 2.08';
 +requires 'Symbol::Global::Name', ($] >= 5.019003 ? '>= 0.05' : '>= 0.04');
 +requires 'Sys::Syslog', '>= 0.16';
 +requires 'Text::Password::Pronounceable';
 +requires 'Text::Quoted', '>= 2.07';
 +requires 'Text::Template', '>= 1.44';
 +requires 'Text::WikiFormat', '>= 0.76';
 +requires 'Text::WordDiff';
 +requires 'Text::Wrapper';
 +requires 'Time::HiRes';
 +requires 'Time::ParseDate';
 +requires 'Tree::Simple', '>= 1.04';
 +requires 'UNIVERSAL::require';
 +requires 'URI', '>= 1.59';
 +requires 'URI::QueryParam';
 +requires 'XML::RSS', '>= 1.05';
 +
 +# Mailgate
 +requires 'Getopt::Long';
 +requires 'LWP::Protocol::https';
 +requires 'LWP::UserAgent', '>= 6.02';
 +requires 'Mozilla::CA';
 +requires 'Pod::Usage';
 +
 +# CLI
 +requires 'Getopt::Long', '>= 2.24';
 +requires 'HTTP::Request::Common';
 +requires 'LWP', '>= 6.02';
 +requires 'Term::ReadKey';
 +requires 'Term::ReadLine';
 +requires 'Text::ParseWords';
 +
 +# REST2
 +requires 'Module::Runtime';
 +requires 'Moose';
 +requires 'MooseX::NonMoose';
 +requires 'MooseX::Role::Parameterized';
 +requires 'namespace::autoclean';
 +requires 'Sub::Exporter';
 +requires 'Web::Machine' => '>= 0.12';
 +requires 'Module::Path';
 +requires 'Path::Dispatcher' => '>= 1.07';
 +
 +on 'develop' => sub {
 +    requires 'Email::Abstract';
 +    requires 'File::Find';
 +    requires 'File::Which';
 +    requires 'HTML::Entities';
 +    requires 'Locale::PO';
 +    requires 'Log::Dispatch::Perl';
 +    requires 'Mojolicious', '!= 8.54';
 +    requires 'Plack::Middleware::Test::StashWarnings', '>= 0.08';
 +    requires 'Pod::Simple', '>= 3.24';
 +    requires 'Set::Tiny';
 +    requires 'String::ShellQuote';
 +    requires 'Test::Builder', '>= 0.90';
 +    requires 'Test::Deep';
 +    requires 'Test::Email';
 +    requires 'Test::Expect', '>= 0.31';
 +    requires 'Test::LongString';
 +    requires 'Test::MockTime';
 +    requires 'Test::NoWarnings';
 +    requires 'Test::Pod';
 +    requires 'Test::Warn';
 +    requires 'Test::WWW::Mechanize', '>= 1.30';
 +    requires 'Test::WWW::Mechanize::PSGI';
 +    requires 'Try::Tiny';
 +    requires 'WWW::Mechanize', '>= 1.80';
 +    requires 'XML::Simple';
 +};
 +
 +
 +# Deployment options
 +feature 'standalone' => sub {};
 +
 +feature 'fastcgi' => sub {
 +    requires 'FCGI', '>= 0.74';
 +};
 +
 +feature 'modperl1' => sub {
 +    requires 'Apache::Request';
 +};
 +
 +feature 'modperl2' => sub {};
 +
 +
 +# Database options
 +feature 'mysql' => sub {
 +    requires 'DBD::mysql', '>= 2.1018, != 4.042';
 +};
 +
 +feature 'oracle' => sub {
 +    requires 'DBD::Oracle', '!= 1.23';
 +};
 +
 +feature 'pg' => sub {
 +    requires 'DBD::Pg', '>= 3.008';
 +};
 +
 +feature 'sqlite' => sub {
 +    requires 'DBD::SQLite', '>= 1.00';
 +};
 +
 +
 +# Optional features
 +feature 'gpg' => sub {
 +    requires 'File::Which';
 +    requires 'GnuPG::Interface', '>= 1.02';
 +    requires 'PerlIO::eol';
 +};
 +
 +feature 'smime' => sub {
 +    requires 'Crypt::X509';
 +    requires 'File::Which';
 +    requires 'String::ShellQuote';
 +};
 +
 +feature 'graphviz' => sub {
 +    requires 'GraphViz';
 +    requires 'IPC::Run', '>= 0.90';
 +};
 +
 +feature 'gd' => sub {
 +    requires 'GD';
 +    requires 'GD::Graph', '>= 1.47';
 +    requires 'GD::Text';
 +};
 +
 +feature 'externalauth' => sub {
 +    requires 'Net::SSLeay';
 +    requires 'Net::LDAP';
 +    on 'develop' => sub {
 +        requires 'Net::LDAP::Server::Test';
 +    };
 +};
 +
 +
 +# External attachment storage
 +feature 's3' => sub {
 +    requires 'Amazon::S3';
 +};
 +
 +feature 'dropbox' => sub {
 +    requires 'File::Dropbox', '>= 0.7';
 +};
diff --cc lib/RT/Config.pm
index 0e9152aaaf,48be38ebb2..6372e60527
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@@ -579,48 -568,16 +579,58 @@@ our %META
              Description => 'Hide unset fields?' # loc
          }
      },
 +    InlineEdit => {
 +        Section => 'Ticket display',
 +        Overridable => 1,
 +        SortOrder => 12,
 +        Widget => '/Widgets/Form/Boolean',
 +        WidgetArguments => {
 +            Description => 'Enable inline edit?' # loc
 +        }
 +    },
 +
 +    InlineEditPanelBehavior => {
 +        Type            => 'HASH',
 +        PostLoadCheck   => sub {
 +            my $config = shift;
 +            # use scalar context intentionally to avoid not a hash error
 +            my $behavior = $config->Get('InlineEditPanelBehavior') || {};
 +
 +            unless (ref($behavior) eq 'HASH') {
 +                RT->Logger->error("Config option \%InlineEditPanelBehavior is a @{[ref $behavior]} not a HASH; ignoring");
 +                $behavior = {};
 +            }
 +
 +            my %valid = map { $_ => 1 } qw/link click always hide/;
 +            for my $class (keys %$behavior) {
 +                if (ref($behavior->{$class}) eq 'HASH') {
 +                    for my $panel (keys %{ $behavior->{$class} }) {
 +                        my $value = $behavior->{$class}{$panel};
 +                        if (!$valid{$value}) {
 +                            RT->Logger->error("Config option \%InlineEditPanelBehavior{$class}{$panel}, which is '$value', must be one of: " . (join ', ', map { "'$_'" } sort keys %valid) . "; ignoring");
 +                            delete $behavior->{$class}{$panel};
 +                        }
 +                    }
 +                } else {
 +                    RT->Logger->error("Config option \%InlineEditPanelBehavior{$class} is not a HASH; ignoring");
 +                    delete $behavior->{$class};
 +                    next;
 +                }
 +            }
 +
 +            $config->Set( InlineEditPanelBehavior => %$behavior );
 +        },
 +    },
+     ShowSearchNavigation => {
+         Section     => 'Ticket display',
+         Overridable => 1,
 -        SortOrder   => 12,
++        SortOrder   => 13,
+         Widget      => '/Widgets/Form/Boolean',
+         WidgetArguments => {
+             Description => 'Show search navigation', # loc
+             Hints       => 'Show search navigation links of "First", "Last", "Prev" and "Next"', # loc
+         }
+     },
  
      # User overridable locale options
      DateTimeFormat => {
diff --cc lib/RT/Crypt/SMIME.pm
index 22cd66e293,bf921d0cc6..1574e3f27b
--- a/lib/RT/Crypt/SMIME.pm
+++ b/lib/RT/Crypt/SMIME.pm
@@@ -80,13 -80,17 +80,17 @@@ You should start from reading L<RT::Cry
      Set( %SMIME,
          Enable => 1,
          OpenSSL => '/usr/bin/openssl',
 -        Keyring => '/opt/rt4/var/data/smime',
 -        CAPath  => '/opt/rt4/var/data/smime/signing-ca.pem',
 +        Keyring => '/opt/rt5/var/data/smime',
 +        CAPath  => '/opt/rt5/var/data/smime/signing-ca.pem',
          Passphrase => {
              'queue.address at example.com' => 'passphrase',
+             'another.queue.address at example.com' => {
+                 Encryption => 'passphrase for encryption certificate',
+                 Signing    => 'passphrase for signing certificate',
+             },
              '' => 'fallback',
          },
--        OtherCertificatesToSend => '/opt/rt4/var/data/smime/other-certs.pem',
++        OtherCertificatesToSend => '/opt/rt5/var/data/smime/other-certs.pem',
          CheckCRL => 0,
          CheckOCSP => 0,
          CheckRevocationDownloadTimeout => 30,
diff --cc lib/RT/Interface/Web/MenuBuilder.pm
index 7c72d3ea07,0000000000..3b3b67c554
mode 100644,000000..100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@@ -1,1710 -1,0 +1,1711 @@@
 +# 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"} ) {
++                if ( RT::Config->Get( 'ShowSearchNavigation', $current_user )
++                    && 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 ( defined ( my $rows = $HTML::Mason::Commands::DECODED_ARGS->{'RowsPerPage'} // $current_search->{'RowsPerPage'} ) ) {
++            $fallback_query_args{RowsPerPage} = $rows;
++        }
++
 +        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 ( defined ( my $rows = $HTML::Mason::Commands::DECODED_ARGS->{'RowsPerPage'} // $current_search->{'RowsPerPage'} ) ) {
++            $fallback_query_args{RowsPerPage} = $rows;
++        }
++
 +        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( 'custom-roles' => title => loc('Custom Roles'), path => "/Admin/Queues/CustomRoles.html?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 share/html/Admin/Groups/Members.html
index b3b5720f6a,53e3358f30..e6f2b7f2c1
--- a/share/html/Admin/Groups/Members.html
+++ b/share/html/Admin/Groups/Members.html
@@@ -59,66 -61,67 +61,75 @@@
  
  <&| /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]}
-             sort { lc($a->[0]) cmp lc($b->[0]) }
-             map { [$_->Format, $_] }
-             @{ $Users->ItemsArrayRef };
- </%perl>
+ % $Users->RowsPerPage($Rows);
+ % $Users->GotoPage($Page - 1);
 -<ul>
 +      <ul class="list-group list-group-compact">
- % for my $user (@users) {
+ % while ( my $user = $Users->Next ) {
  % $UsersSeen{ $user->id } = 1 if $SkipSeenUsers;
- % my $id= 'DeleteMember-' . $user->PrincipalObj->Id;
 -<li><input type="checkbox" class="checkbox" name="DeleteMember-<% $user->PrincipalObj->Id %>" value="1" />
 -<& /Elements/ShowUser, User => $user &></li>
++% my $id = 'DeleteMember-' . $user->PrincipalObj->Id;
 +        <li class="list-group-item">
 +          <div class="custom-control custom-checkbox">
 +            <input type="checkbox" class="checkbox custom-control-input" id="<% $id %>" name="<% $id %>" value="1" />
 +            <label class="custom-control-label" for="<% $id %>"><& /Elements/ShowUser, User => $user &></label>
 +          </div>
 +        </li>
  % }
 -</ul>
 -<&|/l&>Groups</&>
 -<ul>
 +      </ul>
 +      <h4><&|/l&>Groups</&></h4>
 +      <ul class="list-group list-group-compact">
- % my $GroupMembers = $Group->MembersObj;
- % $GroupMembers->LimitToGroups();
- % while ( my $member = $GroupMembers->Next ) {
- % $GroupsSeen{ $member->MemberId } = 1 if $SkipSeenGroups;
- % my $id= 'DeleteMember-' .  $member->MemberId;
+ % my $Groups = $Group->GroupMembersObj( Recursively => 0 );
+ % $Groups->RowsPerPage($Rows);
+ % $Groups->GotoPage($Page - 1);
+ % while ( my $group = $Groups->Next ) {
+ % $GroupsSeen{ $group->id } = 1 if $SkipSeenGroups;
 -<li><input type="checkbox" class="checkbox" name="DeleteMember-<% $group->id %>" value="1" />
 -<a href="<% RT->Config->Get('WebPath') %>/Admin/Groups/Modify.html?id=<% $group->id %>"><% $group->Name %></a>
++% my $id= 'DeleteMember-' .  $group->id;
 +      <li class="list-group-item">
 +        <div class="custom-control custom-checkbox">
 +          <input type="checkbox" class="checkbox custom-control-input" id="<% $id%>" name="<% $id %>" value="1" />
 +          <label class="custom-control-label" for="<% $id %>">
-             <a href="<% RT->Config->Get('WebPath') %>/Admin/Groups/Modify.html?id=<% $member->MemberObj->Object->id %>"><% $member->MemberObj->Object->Name %></a>
++            <a href="<% RT->Config->Get('WebPath') %>/Admin/Groups/Modify.html?id=<% $group->id %>"><% $group->Name %></a>
 +          </label>
 +        </div>
 +      </li>
  % }
 -</ul>
 +      </ul>
 +      <em><&|/l&>(Check box to delete)</&></em>
  % }
 -</td>
 -<td valign="top">
 -<& /Admin/Elements/SelectNewGroupMembers, Name => "AddMembers", Group => $Group,
 -    SkipUsers => \%UsersSeen, SkipGroups => \%GroupsSeen &>
 -</td>
 -</tr>
 -</table>
 +
 +  </div>
 +  <div class="col-6">
 +    <h3><&|/l&>Add members</&></h3>
 +    <& /Admin/Elements/SelectNewGroupMembers, Name => "AddMembers", Group => $Group,
 +        SkipUsers => \%UsersSeen, SkipGroups => \%GroupsSeen &>
 +  </div>
 +</div>
+ 
+ % if ( $Pages ) {
+     <& /Elements/CollectionListPaging,
+         BaseURL     => '?',
+         Rows        => $Rows,
+         TotalFound  => $TotalFound,
+         CurrentPage => $Page,
+         Pages       => $Pages,
+         URLParams   => { map { $_ => $ARGS{$_} } qw/id Rows Page/ },
+     &>
+ % }
+ 
  </&>
 -<em><&|/l&>(Check box to delete)</&></em>
 -<& /Elements/Submit, Label => loc('Modify Members'), Reset => 1 &>
 +  <div class="form-row">
 +    <div class="col-12">
 +      <& /Elements/Submit, Label => loc('Modify Members'), Reset => 1 &>
 +    </div>
 +  </div>
  </form>
  
  
diff --cc share/html/Search/Elements/EditSearches
index 51d0350040,cc661772e4..a196333127
--- a/share/html/Search/Elements/EditSearches
+++ b/share/html/Search/Elements/EditSearches
@@@ -47,53 -47,72 +47,112 @@@
  %# END BPS TAGGED BLOCK }}}
  <div class="edit-saved-searches">
  <&| /Widgets/TitleBox, title => loc($Title)&>
 -
  %# Hide all the save functionality if the user shouldn't see it.
  % if ( $can_modify ) {
 -<span class="label"><&|/l&>Privacy</&>:</span>
 -<& SelectSearchObject, Name => 'SavedSearchOwner', Objects => \@CreateObjects, Object => ( $Object && $Object->id ) ? $Object->Object : '' &>
 -<br />
 -<span class="label"><&|/l&>Description</&>:</span>
 -<input size="25" name="SavedSearchDescription" value="<% $Description || '' %>" />
  
 +<div class="form-row">
 +  <div class="label col-4"><&|/l&>Privacy</&>:</div>
 +  <div class="col-8">
 +<& SelectSearchObject, Name => 'SavedSearchOwner', Objects => \@CreateObjects, Object => ( $Object && $Object->id ) ? $Object->Object : '' &>
 +  </div>
 +</div>
 +  <div class="form-row">
 +    <div class="label col-4"><&|/l&>Description</&>:</div>
 +    <div class="col-8">
 +      <input type="text" size="25" name="SavedSearchDescription" value="<% $Description || '' %>" class="form-control" />
 +    </div>
 +  </div>
 +  <div class="form-row justify-content-end">
 +    <div class="col-auto">
  % if ($Id ne 'new') {
 -<nobr>
  % if ( $Dirty ) {
 -<input type="submit" class="button" name="SavedSearchRevert" value="<%loc('Revert')%>" />
 +<input type="submit" class="button btn btn-primary mr-1" name="SavedSearchRevert" value="<%loc('Revert')%>" />
 +% }
- <input type="submit" class="button btn btn-primary mr-1" name="SavedSearchDelete" value="<%loc('Delete')%>" />
++<input class="button btn btn-primary mr-1"
++% if ( $Object && $Object->Id && $Object->DependedOnBy->Count ) {
++  type="button" data-toggle="modal" data-target="#delete-saved-search-confirm-modal"
++% } else {
++  type="submit"
+ % }
 -<input type="submit" class="button <% $Object && $Object->Id && $Object->DependedOnBy->Count ? 'confirm' : '' %>" name="SavedSearchDelete" value="<%loc('Delete')%>" />
++  name="SavedSearchDelete" value="<%loc('Delete')%>" />
  % if ( $AllowCopy ) {
 -<input type="submit" class="button" name="SavedSearchCopy"   value="<%loc('Save as New')%>" />
 +<input type="submit" class="button btn btn-primary mr-1" name="SavedSearchCopy"   value="<%loc('Save as New')%>" />
  % }
 -</nobr>
  % }
 +
  % if ( $Object && $Object->Id && $Object->CurrentUserHasRight('update') ) {
 -<input type="submit" class="button" id="SavedSearchSave" name="SavedSearchSave"   value="<%loc('Update')%>" />
 +<input type="submit" class="button btn btn-primary mr-1" id="SavedSearchSave" name="SavedSearchSave"   value="<%loc('Update')%>" />
  % } elsif ( !$Object ) {
 -<input type="submit" class="button" id="SavedSearchSave" name="SavedSearchSave"   value="<%loc('Save')%>" />
 +<input type="submit" class="button btn btn-primary mr-1" id="SavedSearchSave" name="SavedSearchSave"   value="<%loc('Save')%>" />
  %}
 +    </div>
 +  </div>
  % }
 -<br />
  
+ % if ( $Object && $Object->Id && $Object->DependedOnBy->Count ) {
 -<span class="label"><&|/l&>Depended on by</&>:</span>
 -<a href="#" class="view-saved-search-depended-on-by-list"><% loc('View') %></a>
 -<br />
++  <div class="form-row">
++    <div class="label col-4"><&|/l&>Depended on by</&>:</div>
++    <div class="col-8">
++      <span class="form-control current-value">
++        <a href="#" data-toggle="modal" data-target="#saved-search-depended-on-by-list-modal"><% loc('View') %></a>
++      </span>
++    </div>
++  </div>
+ % }
+ 
 -<hr />
 -<span class="label"><&|/l&>Load saved search</&>:</span>
 +  <hr />
 +  <div class="form-row">
 +    <div class="label col-4"><&|/l&>Load saved search</&>:</div>
 +    <div class="col-8 input-group">
  <& SelectSearchesForObjects, Name => 'SavedSearchLoad', Objects => \@LoadObjects, SearchType => $Type &>
 -<input type="submit" value="<% loc('Load') %>" id="SavedSearchLoadSubmit" name="SavedSearchLoadSubmit" class="button" />
 +<input type="submit" class="button btn btn-primary" value="<% loc('Load') %>" id="SavedSearchLoadSubmit" name="SavedSearchLoadSubmit" />
 +    </div>
 +  </div>
  
  </&>
  </div>
+ 
+ % if ( $Object && $Object->Id && $Object->DependedOnBy->Count ) {
 -    <div class="delete-saved-search-confirm hidden">
 -      <p>
 -        <&|/l&>This search is used in these dashboards/homepages, really delete?</&>
 -      </p>
 -      <ul class="saved-search-depended-on-by-list">
 -%     my $links = $Object->DependedOnBy;
 -%     while ( my $link = $links->Next ) {
 -        <li>
 -%       if ( $link->BaseObj->Name eq 'Dashboard' ) {
 -          <a href="<% $link->BaseURI->Resolver->HREF %>" target="_blank"><% $link->BaseURI->AsString %></a>
 -%       } elsif ( $link->BaseObj->ObjectType eq 'RT::System' ) {
 -          <% loc('Global') %>
 -%       } elsif ( $link->BaseObj->ObjectType eq 'RT::User' ) {
 -          <% loc('User') %>: <& /Elements/ShowUser, User => $link->BaseObj->Object, LinkTarget => '_blank' &>
 -%       } else {
 -          <% $link->BaseObj->ObjectType %>: #<% $link->BaseObj->ObjectId %>
 -%       }
 -        </li>
 -%     }
 -      </ul>
 -      <& /Elements/Submit, Name => 'SavedSearchDelete', Label => loc('Delete') &>
++<div class="modal" id="delete-saved-search-confirm-modal">
++  <div class="modal-dialog modal-dialog-centered" role="document">
++    <div class="modal-content">
++      <div class="modal-header">
++        <h5 class="modal-title"><&|/l&>Really Delete?</&></h5>
++        <a href="javascript:void(0)" class="close" data-dismiss="modal" aria-label="Close">
++          <span aria-hidden="true">×</span>
++        </a>
++      </div>
++      <div class="modal-body">
++        <& SELF:GetDependedOnByList, Object => $Object &>
++      </div>
++      <div class="modal-footer">
++        <div class="form-row justify-content-end">
++          <div class="col-auto">
++            <input type="submit" class="button btn btn-primary" name="SavedSearchDelete" value="<% loc('Delete') %>" />
++          </div>
++        </div>
++      </div>
+     </div>
++  </div>
++</div>
++
++<div class="modal" id="saved-search-depended-on-by-list-modal">
++  <div class="modal-dialog modal-dialog-centered" role="document">
++    <div class="modal-content">
++      <div class="modal-header">
++        <h5 class="modal-title"><&|/l&>Depended On By List</&></h5>
++        <a href="javascript:void(0)" class="close" data-dismiss="modal" aria-label="Close">
++          <span aria-hidden="true">×</span>
++        </a>
++      </div>
++      <div class="modal-body">
++        <& SELF:GetDependedOnByList, Object => $Object &>
++      </div>
++    </div>
++  </div>
++</div>
+ % }
++
  <%INIT>
  return unless $session{'CurrentUser'}->HasRight(
      Right  => 'LoadSavedSearch',
@@@ -369,3 -364,3 +428,30 @@@ return @results
  
  </%INIT>
  </%METHOD>
++
++<%METHOD GetDependedOnByList>
++
++  <p>
++    <&|/l&>This search is used in these dashboards/homepages</&>:
++  </p>
++  <ul class="saved-search-depended-on-by-list list-group-compact">
++% my $links = $Object->DependedOnBy;
++% while ( my $link = $links->Next ) {
++    <li class="list-group-item">
++%   if ( $link->BaseObj->Name eq 'Dashboard' ) {
++      <a href="<% $link->BaseURI->Resolver->HREF %>" target="_blank"><% $link->BaseURI->AsString %></a>
++%   } elsif ( $link->BaseObj->ObjectType eq 'RT::System' ) {
++      <% loc('Global') %>
++%   } elsif ( $link->BaseObj->ObjectType eq 'RT::User' ) {
++      <% loc('User') %>: <& /Elements/ShowUser, User => $link->BaseObj->Object, LinkTarget => '_blank' &>
++%   } else {
++      <% $link->BaseObj->ObjectType %>: #<% $link->BaseObj->ObjectId %>
++%   }
++    </li>
++% }
++  </ul>
++
++<%ARGS>
++$Object
++</%ARGS>
++</%METHOD>
diff --cc share/html/Ticket/Display.html
index 7460ef1545,bf64200bf0..87883d5419
--- a/share/html/Ticket/Display.html
+++ b/share/html/Ticket/Display.html
@@@ -286,8 -240,8 +286,8 @@@ my $attachments = $TicketObj->Attachmen
  my $attachment_content = $TicketObj->TextAttachments;
  
  my %link_rel;
- if (defined $session{'collection-RT::Tickets'} and ($ARGS{'Query'} or $session{'CurrentSearchHash'}->{'Query'})) {
 -if (RT::Config->Get( 'ShowSearchNavigation', $session{'CurrentUser'} ) && defined $session{'tickets'} and ($ARGS{'Query'} or $session{'CurrentSearchHash'}->{'Query'})) {
 -    my $item_map = $session{'tickets'}->ItemMap;
++if (RT::Config->Get( 'ShowSearchNavigation', $session{'CurrentUser'} ) && defined $session{'collection-RT::Tickets'} and ($ARGS{'Query'} or $session{'CurrentSearchHash'}->{'Query'})) {
 +    my $item_map = $session{'collection-RT::Tickets'}->ItemMap;
      $link_rel{first} = "/Ticket/Display.html?id=" . $item_map->{first}                if $item_map->{$TicketObj->Id}{prev};
      $link_rel{prev}  = "/Ticket/Display.html?id=" . $item_map->{$TicketObj->Id}{prev} if $item_map->{$TicketObj->Id}{prev};
      $link_rel{next}  = "/Ticket/Display.html?id=" . $item_map->{$TicketObj->Id}{next} if $item_map->{$TicketObj->Id}{next};
diff --cc share/static/css/elevator-light/misc.css
index e8095bdf9b,0000000000..4f754ef5f5
mode 100644,000000..100644
--- a/share/static/css/elevator-light/misc.css
+++ b/share/static/css/elevator-light/misc.css
@@@ -1,143 -1,0 +1,148 @@@
 +.hide, .hidden { display: none !important; }
 +
 +.clear { clear: both; }
 +
 +* html .clearfix {
 +    height: 1%; /* IE5-6 */
 +}
 +.clearfix {
 +    display: inline-block; /* IE7xhtml*/
 +}
 +html[xmlns] .clearfix { /* O */
 +    display: block;
 +}
 +.clearfix:after { /* FF, O, etc. */
 +    content: ".";
 +    display: block;
 +    height: 0;
 +    clear: both;
 +    visibility: hidden;
 +}
 +
 +hr.clear {
 +    visibility: hidden;
 +    height: 0;
 +    margin: 0;
 +    padding: 0;
 +    border: none;
 +    font-size: 1px;
 +}
 +
 +.query-stacktrace-toggle {
 +    float: right;
 +}
 +
 +/* jQuery UI overrides */
 +.ui-widget {
 +    font-family: arial,helvetica,sans-serif !important;
 +}
 +
 +textarea.messagebox, #cke_Content, #cke_UpdateContent {
 +  -moz-box-sizing: border-box;
 +  box-sizing: border-box;
 +}
 +
 +.selection-box {
 +    min-width: 300px;
 +}
 +
 +.datepicker {
 +    max-width: 17em;
 +    min-width: 10em;
 +}
 +
 +.selectowner {
 +    max-width: 15.8em;
 +    min-width: 10em;
 +}
 +
 +.dashboard-subscription tr.frequency .value input {
 +    margin-bottom: 0.75em;
 +}
 +
 +/* infinite history error message */
 +
 +.error-load-history {
 +    background-color: #b32;
 +    padding: 10px;
 +    border-radius: 5px;
 +    color: white;
 +}
 +
 +.error-load-history a {
 +    text-decoration: underline !important;
 +    color: white !important;
 +}
 +
 +/* */
 +
 +.comment {
 +    padding-left: 0.5em;
 +    color: #999;
 +}
 +
 +#comp-Ticket-ShowEmailRecord #header {
 +    top: 0em;
 +}
 +
 +#comp-Ticket-ShowEmailRecord #body {
 +    margin-left: 1em;
 +    margin-top: 1em;
 +    overflow: auto;
 +}
 +
 +.modal {
 +  background: rgb(0, 0, 0, .70); 
 +}
 +
 +/* manipulate the svg image for selected bookmarks */
 +svg.bookmark-selected path {
 +    stroke: black;
 +    stroke-width: 50;
 +    fill: #46B346;
 +}
 +
 +/* borders for cog icons */
 +svg.icon-bordered {
 +    width: 1em !important;
 +    height: 1em !important;
 +    border: solid 0.05em #eee;
 +    border-radius: 0.1em;
 +    padding: 0.2em 0.25em 0.15em;
 +}
 +
 +/* styling for helper text svg images */
 +svg.icon-helper {
 +    padding-left: 0.2em;
 +    color: #666;
 +}
 +
 +/* row colouring */
 +.oddline {
 +    background-color: rgba(242, 242, 242);
 +}
 +
 +.cke_toolgroup a.cke_button {
 +    padding-left: 3px;
 +    padding-right: 3px;
 +}
 +
 +.cke_toolbar .cke_combo_button,
 +.cke_toolbar .cke_toolgroup {
 +    margin-right: 5px;
 +}
 +
 +legend {
 +  font-size: 1rem;
 +}
 +
 +/* transaction display page */
 +h1#transaction-extra-info {
 +    font-size: 1.4rem;
 +    padding-top: 0.4rem;
 +}
++
++/* Prevent page links from running off the side of the page for Firefox */
++span.pagenum {
++    display: inline-block;
++}
diff --cc t/api/initialdata-roundtrip.t
index fbd349507d,0000000000..a5b2b68329
mode 100644,000000..100644
--- a/t/api/initialdata-roundtrip.t
+++ b/t/api/initialdata-roundtrip.t
@@@ -1,1171 -1,0 +1,1179 @@@
 +use utf8;
 +use strict;
 +use warnings;
 +use JSON;
 +
 +use RT::Test tests => undef, config => << 'CONFIG';
 +Set($InitialdataFormatHandlers, [ 'perl', 'RT::Initialdata::JSON' ]);
 +CONFIG
 +
 +my $general = RT::Queue->new(RT->SystemUser);
 +$general->Load('General');
 +
 +my @tests = (
 +    {
 +        name => 'Simple user-defined group',
 +        create => sub {
 +            my $group = RT::Group->new(RT->SystemUser);
 +            my ($ok, $msg) = $group->CreateUserDefinedGroup(Name => 'Staff');
 +            ok($ok, $msg);
 +        },
 +        absent => sub {
 +            my $group = RT::Group->new(RT->SystemUser);
 +            $group->LoadUserDefinedGroup('Staff');
 +            ok(!$group->Id, 'No such group');
 +        },
 +        present => sub {
 +            my $group = RT::Group->new(RT->SystemUser);
 +            $group->LoadUserDefinedGroup('Staff');
 +            ok($group->Id, 'Loaded group');
 +            is($group->Name, 'Staff', 'Group name');
 +            is($group->Domain, 'UserDefined', 'Domain');
 +        },
 +    },
 +    {
 +        name => 'Group membership and ACLs',
 +        create => sub {
 +            my $outer = RT::Group->new(RT->SystemUser);
 +            my ($ok, $msg) = $outer->CreateUserDefinedGroup(Name => 'Outer');
 +            ok($ok, $msg);
 +
 +            my $inner = RT::Group->new(RT->SystemUser);
 +            ($ok, $msg) = $inner->CreateUserDefinedGroup(Name => 'Inner');
 +            ok($ok, $msg);
 +
 +            my $unrelated = RT::Group->new(RT->SystemUser);
 +            ($ok, $msg) = $unrelated->CreateUserDefinedGroup(Name => 'Unrelated');
 +            ok($ok, $msg);
 +
 +            my $user = RT::User->new(RT->SystemUser);
 +            ($ok, $msg) = $user->Create(Name => 'User');
 +            ok($ok, $msg);
 +
 +            my $unprivileged = RT::User->new(RT->SystemUser);
 +            ($ok, $msg) = $unprivileged->Create(Name => 'Unprivileged');
 +            ok($ok, $msg);
 +
 +            ($ok, $msg) = $outer->AddMember($inner->PrincipalId);
 +            ok($ok, $msg);
 +
 +            ($ok, $msg) = $inner->AddMember($user->PrincipalId);
 +            ok($ok, $msg);
 +
 +            ($ok, $msg) = $general->AddWatcher(Type => 'AdminCc', PrincipalId => $outer->PrincipalId);
 +            ok($ok, $msg);
 +
 +            ($ok, $msg) = $general->AdminCc->PrincipalObj->GrantRight(Object => $general, Right => 'ShowTicket');
 +            ok($ok, $msg);
 +
 +            ($ok, $msg) = $inner->PrincipalObj->GrantRight(Object => $general, Right => 'ModifyTicket');
 +            ok($ok, $msg);
 +
 +            ($ok, $msg) = $user->PrincipalObj->GrantRight(Object => $general, Right => 'OwnTicket');
 +            ok($ok, $msg);
 +
 +            ($ok, $msg) = $unprivileged->PrincipalObj->GrantRight(Object => RT->System, Right => 'ModifyTicket');
 +            ok($ok, $msg);
 +
 +            ($ok, $msg) = $inner->PrincipalObj->GrantRight(Object => $inner, Right => 'SeeGroup');
 +            ok($ok, $msg);
 +
 +        },
 +        present => sub {
 +            my $outer = RT::Group->new(RT->SystemUser);
 +            $outer->LoadUserDefinedGroup('Outer');
 +            ok($outer->Id, 'Loaded group');
 +            is($outer->Name, 'Outer', 'Group name');
 +
 +            my $inner = RT::Group->new(RT->SystemUser);
 +            $inner->LoadUserDefinedGroup('Inner');
 +            ok($inner->Id, 'Loaded group');
 +            is($inner->Name, 'Inner', 'Group name');
 +
 +            my $unrelated = RT::Group->new(RT->SystemUser);
 +            $unrelated->LoadUserDefinedGroup('Unrelated');
 +            ok($unrelated->Id, 'Loaded group');
 +            is($unrelated->Name, 'Unrelated', 'Group name');
 +
 +            my $user = RT::User->new(RT->SystemUser);
 +            $user->Load('User');
 +            ok($user->Id, 'Loaded user');
 +            is($user->Name, 'User', 'User name');
 +
 +            my $unprivileged = RT::User->new(RT->SystemUser);
 +            $unprivileged->Load('Unprivileged');
 +            ok($unprivileged->Id, 'Loaded Unprivileged');
 +            is($unprivileged->Name, 'Unprivileged', 'Unprivileged name');
 +
 +            ok($outer->HasMember($inner->PrincipalId), 'outer hasmember inner');
 +            ok($inner->HasMember($user->PrincipalId), 'inner hasmember user');
 +            ok($outer->HasMemberRecursively($user->PrincipalId), 'outer hasmember user recursively');
 +            ok(!$outer->HasMember($user->PrincipalId), 'outer does not have member user directly');
 +            ok(!$inner->HasMember($outer->PrincipalId), 'inner does not have member outer');
 +
 +            ok($general->AdminCc->HasMember($outer->PrincipalId), 'queue AdminCc');
 +            ok($general->AdminCc->HasMemberRecursively($inner->PrincipalId), 'queue AdminCc');
 +            ok($general->AdminCc->HasMemberRecursively($user->PrincipalId), 'queue AdminCc');
 +
 +            ok(!$outer->HasMemberRecursively($unrelated->PrincipalId), 'unrelated group membership');
 +            ok(!$inner->HasMemberRecursively($unrelated->PrincipalId), 'unrelated group membership');
 +            ok(!$general->AdminCc->HasMemberRecursively($unrelated->PrincipalId), 'unrelated group membership');
 +
 +            ok($general->AdminCc->PrincipalObj->HasRight(Object => $general, Right => 'ShowTicket'), 'AdminCc ShowTicket right');
 +            ok($outer->PrincipalObj->HasRight(Object => $general, Right => 'ShowTicket'), 'outer ShowTicket right');
 +            ok($inner->PrincipalObj->HasRight(Object => $general, Right => 'ShowTicket'), 'inner ShowTicket right');
 +            ok($user->PrincipalObj->HasRight(Object => $general, Right => 'ShowTicket'), 'user ShowTicket right');
 +            ok(!$unrelated->PrincipalObj->HasRight(Object => $general, Right => 'ShowTicket'), 'unrelated ShowTicket right');
 +
 +            ok(!$general->AdminCc->PrincipalObj->HasRight(Object => $general, Right => 'ModifyTicket'), 'AdminCc ModifyTicket right');
 +            ok(!$outer->PrincipalObj->HasRight(Object => $general, Right => 'ModifyTicket'), 'outer ModifyTicket right');
 +            ok($inner->PrincipalObj->HasRight(Object => $general, Right => 'ModifyTicket'), 'inner ModifyTicket right');
 +            ok($user->PrincipalObj->HasRight(Object => $general, Right => 'ModifyTicket'), 'user ModifyTicket right');
 +            ok(!$unrelated->PrincipalObj->HasRight(Object => $general, Right => 'ModifyTicket'), 'unrelated ModifyTicket right');
 +
 +            ok(!$general->AdminCc->PrincipalObj->HasRight(Object => $general, Right => 'OwnTicket'), 'AdminCc OwnTicket right');
 +            ok(!$outer->PrincipalObj->HasRight(Object => $general, Right => 'OwnTicket'), 'outer OwnTicket right');
 +            ok(!$inner->PrincipalObj->HasRight(Object => $general, Right => 'OwnTicket'), 'inner OwnTicket right');
 +            ok($user->PrincipalObj->HasRight(Object => $general, Right => 'OwnTicket'), 'inner OwnTicket right');
 +            ok(!$unrelated->PrincipalObj->HasRight(Object => $general, Right => 'OwnTicket'), 'unrelated OwnTicket right');
 +
 +            ok($unprivileged->PrincipalObj->HasRight(Object => RT->System, Right => 'ModifyTicket'), 'unprivileged ModifyTicket right');
 +
 +            ok(!$general->AdminCc->PrincipalObj->HasRight(Object => $inner, Right => 'SeeGroup'), 'AdminCc SeeGroup right');
 +            ok(!$outer->PrincipalObj->HasRight(Object => $inner, Right => 'SeeGroup'), 'outer SeeGroup right');
 +            ok($inner->PrincipalObj->HasRight(Object => $inner, Right => 'SeeGroup'), 'inner SeeGroup right');
 +            ok($user->PrincipalObj->HasRight(Object => $inner, Right => 'SeeGroup'), 'user SeeGroup right');
 +            ok(!$unrelated->PrincipalObj->HasRight(Object => $inner, Right => 'SeeGroup'), 'unrelated SeeGroup right');
 +        },
 +    },
 +
 +    {
 +        name => 'Custom field on two queues',
 +        create => sub {
 +            my $bugs = RT::Queue->new(RT->SystemUser);
 +            my ($ok, $msg) = $bugs->Create(Name => 'Bugs');
 +            ok($ok, $msg);
 +
 +            my $features = RT::Queue->new(RT->SystemUser);
 +            ($ok, $msg) = $features->Create(Name => 'Features');
 +            ok($ok, $msg);
 +
 +            my $cf = RT::CustomField->new(RT->SystemUser);
 +            ($ok, $msg) = $cf->Create(
 +                Name => 'Fixed In',
 +                Type => 'SelectSingle',
 +                LookupType => RT::Ticket->CustomFieldLookupType,
 +            );
 +            ok($ok, $msg);
 +
 +            ($ok, $msg) = $cf->AddToObject($bugs);
 +            ok($ok, $msg);
 +
 +            ($ok, $msg) = $cf->AddToObject($features);
 +            ok($ok, $msg);
 +
 +            ($ok, $msg) = $cf->AddValue(Name => '0.1', Description => 'Prototype', SortOrder => '1');
 +            ok($ok, $msg);
 +
 +            ($ok, $msg) = $cf->AddValue(Name => '1.0', Description => 'Gold', SortOrder => '10');
 +            ok($ok, $msg);
 +
 +            # these next two are intentionally added in an order different from their SortOrder
 +            ($ok, $msg) = $cf->AddValue(Name => '2.0', Description => 'Remaster', SortOrder => '20');
 +            ok($ok, $msg);
 +
 +            ($ok, $msg) = $cf->AddValue(Name => '1.1', Description => 'Gold Bugfix', SortOrder => '11');
 +            ok($ok, $msg);
 +
 +        },
 +        present => sub {
 +            my $bugs = RT::Queue->new(RT->SystemUser);
 +            $bugs->Load('Bugs');
 +            ok($bugs->Id, 'Bugs queue loaded');
 +            is($bugs->Name, 'Bugs');
 +
 +            my $features = RT::Queue->new(RT->SystemUser);
 +            $features->Load('Features');
 +            ok($features->Id, 'Features queue loaded');
 +            is($features->Name, 'Features');
 +
 +            my $cf = RT::CustomField->new(RT->SystemUser);
 +            $cf->Load('Fixed In');
 +            ok($cf->Id, 'Fixed In CF loaded');
 +            is($cf->Name, 'Fixed In');
 +            is($cf->Type, 'Select', 'Type');
 +            is($cf->MaxValues, 1, 'MaxValues');
 +            is($cf->LookupType, RT::Ticket->CustomFieldLookupType, 'LookupType');
 +
 +            ok($cf->IsAdded($bugs->Id), 'CF is on Bugs queue');
 +            ok($cf->IsAdded($features->Id), 'CF is on Features queue');
 +            ok(!$cf->IsAdded(0), 'CF is not global');
 +            ok(!$cf->IsAdded($general->Id), 'CF is not on General queue');
 +
 +            my @values = map { {
 +                Name => $_->Name,
 +                Description => $_->Description,
 +                SortOrder => $_->SortOrder,
 +            } } @{ $cf->Values->ItemsArrayRef };
 +
 +            is_deeply(\@values, [
 +                { Name => '0.1', Description => 'Prototype', SortOrder => '1' },
 +                { Name => '1.0', Description => 'Gold', SortOrder => '10' },
 +                { Name => '1.1', Description => 'Gold Bugfix', SortOrder => '11' },
 +                { Name => '2.0', Description => 'Remaster', SortOrder => '20' },
 +            ], 'CF values');
 +        },
 +    },
 +
 +    {
 +        name => 'Custom field lookup types',
 +        create => sub {
 +            my %extra = (
 +                Group => { method => 'CreateUserDefinedGroup' },
 +                Asset => undef,
 +                Article => { Class => 'General' },
 +                Ticket => undef,
 +                Transaction => undef,
 +                User => undef,
 +            );
 +
 +            for my $type (qw/Asset Article Group Queue Ticket Transaction User/) {
 +                my $class = "RT::$type";
 +                my $cf = RT::CustomField->new(RT->SystemUser);
 +                my ($ok, $msg) = $cf->Create(
 +                    Name => "$type CF",
 +                    Type => "FreeformSingle",
 +                    LookupType => $class->CustomFieldLookupType,
 +                );
 +                ok($ok, $msg);
 +
 +                # apply globally
 +                ($ok, $msg) = $cf->AddToObject($cf->RecordClassFromLookupType->new(RT->SystemUser));
 +                ok($ok, $msg);
 +
 +                next if exists($extra{$type}) && !defined($extra{$type});
 +
 +                my $obj = $class->new(RT->SystemUser);
 +                my $method = delete($extra{$type}{method}) || 'Create';
 +                ($ok, $msg) = $obj->$method(
 +                    Name => $type,
 +                    %{ $extra{$type} || {} },
 +                );
 +                ok($ok, "created $type: $msg");
 +                ok($obj->Id, "loaded $type");
 +
 +                ($ok, $msg) = $obj->AddCustomFieldValue(
 +                    Field => $cf->Id,
 +                    Value => "$type Value",
 +                );
 +                ok($ok, $msg);
 +            }
 +        },
 +        present => sub {
 +            my %load = (
 +                Transaction => undef,
 +                Ticket => undef,
 +                User => undef,
 +                Asset => undef,
 +            );
 +
 +            for my $type (qw/Asset Article Group Queue Ticket Transaction User/) {
 +                my $class = "RT::$type";
 +                my $cf = RT::CustomField->new(RT->SystemUser);
 +                $cf->Load("$type CF");
 +                ok($cf->Id, "loaded $type CF");
 +                is($cf->Name, "$type CF", 'Name');
 +                is($cf->Type, 'Freeform', 'Type');
 +                is($cf->MaxValues, 1, 'MaxValues');
 +                is($cf->LookupType, $class->CustomFieldLookupType, 'LookupType');
 +
 +                next if exists($load{$type}) && !defined($load{$type});
 +
 +                my $obj = $class->new(RT->SystemUser);
 +                $obj->LoadByCols(
 +                    %{ $load{$type} || { Name => $type } },
 +                );
 +                ok($obj->Id, "loaded $type");
 +
 +                is($obj->FirstCustomFieldValue($cf->Id), "$type Value", "CF value for $type");
 +            }
 +        },
 +    },
 +
 +    {
 +        name => 'Custom field LargeContent',
 +        create => sub {
 +            my $cf = RT::CustomField->new(RT->SystemUser);
 +            my ($ok, $msg) = $cf->Create(
 +                Name => "Group CF",
 +                Type => "FreeformSingle",
 +                LookupType => RT::Group->CustomFieldLookupType,
 +            );
 +            ok($ok, $msg);
 +
 +            ($ok, $msg) = $cf->AddToObject(RT::Group->new(RT->SystemUser));
 +            ok($ok, $msg);
 +
 +            my $group = RT::Group->new(RT->SystemUser);
 +            ($ok, $msg) = $group->CreateUserDefinedGroup(Name => 'Group');
 +            ok($ok, $msg);
 +
 +            ($ok, $msg) = $group->AddCustomFieldValue(
 +                Field => $cf->Id,
 +                Value => scalar("abc" x 256),
 +            );
 +            ok($ok, $msg);
 +        },
 +        present => sub {
 +            my $group = RT::Group->new(RT->SystemUser);
 +            $group->LoadUserDefinedGroup('Group');
 +            ok($group->Id, 'loaded Group');
 +            is($group->FirstCustomFieldValue('Group CF'), scalar("abc" x 256), "CF LargeContent");
 +        },
 +        # the following test peers into the initialdata only to make sure that
 +        # we are roundtripping LargeContent as expected; if this starts
 +        # failing it's not necessarily a problem, but it's worthy of
 +        # investigating whether the "present" tests are still testing
 +        # what they were meant to test
 +        raw => sub {
 +            my $json = shift;
 +            my ($group) = grep { $_->{Name} eq 'Group' } @{ $json->{Groups} };
 +            ok($group, 'found the group');
 +            my ($ocfv) = @{ $group->{CustomFields} };
 +            ok($ocfv, 'found the OCFV');
 +
 +            is($ocfv->{CustomField}, 'Group CF', 'CustomField');
 +            is($ocfv->{Content}, undef, 'no Content');
 +            is($ocfv->{LargeContent}, scalar("abc" x 256), 'LargeContent');
 +            is($ocfv->{ContentType}, "text/plain", 'ContentType');
 +        }
 +    },
 +
 +    {
 +        name => 'Scrips including Disabled',
 +        export_args => { FollowDisabled => 1 },
 +        create => sub {
 +            my $bugs = RT::Queue->new(RT->SystemUser);
 +            my ($ok, $msg) = $bugs->Create(Name => 'Bugs');
 +            ok($ok, $msg);
 +
 +            my $features = RT::Queue->new(RT->SystemUser);
 +            ($ok, $msg) = $features->Create(Name => 'Features');
 +            ok($ok, $msg);
 +
 +            my $disabled = RT::Scrip->new(RT->SystemUser);
 +            ($ok, $msg) = $disabled->Create(
 +                Queue => 0,
 +                Description => 'Disabled Scrip',
 +                Template => 'Blank',
 +                ScripCondition => 'User Defined',
 +                ScripAction => 'User Defined',
 +                CustomIsApplicableCode => 'return "condition"',
 +                CustomPrepareCode => 'return "prepare"',
 +                CustomCommitCode => 'return "commit"',
 +            );
 +            ok($ok, $msg);
 +            ($ok, $msg) = $disabled->SetDisabled(1);
 +            ok($ok, $msg);
 +
 +            my $stages = RT::Scrip->new(RT->SystemUser);
 +            ($ok, $msg) = $stages->Create(
 +                Description => 'Staged Scrip',
 +                Template => 'Transaction',
 +                ScripCondition => 'On Create',
 +                ScripAction => 'Notify Owner',
 +            );
 +            ok($ok, $msg);
 +
 +            ($ok, $msg) = $stages->RemoveFromObject(0);
 +            ok($ok, $msg);
 +
 +            ($ok, $msg) = $stages->AddToObject(
 +                ObjectId  => $bugs->Id,
 +                Stage     => 'TransactionBatch',
 +                SortOrder => 42,
 +            );
 +            ok($ok, $msg);
 +
 +            ($ok, $msg) = $stages->AddToObject(
 +                ObjectId  => $features->Id,
 +                Stage     => 'TransactionCreate',
 +                SortOrder => 99,
 +            );
 +            ok($ok, $msg);
 +        },
 +        present => sub {
 +            my $bugs = RT::Queue->new(RT->SystemUser);
 +            $bugs->Load('Bugs');
 +            ok($bugs->Id, 'Bugs queue loaded');
 +            is($bugs->Name, 'Bugs');
 +
 +            my $features = RT::Queue->new(RT->SystemUser);
 +            $features->Load('Features');
 +            ok($features->Id, 'Features queue loaded');
 +            is($features->Name, 'Features');
 +
 +            my $disabled = RT::Scrip->new(RT->SystemUser);
 +            $disabled->LoadByCols(Description => 'Disabled Scrip');
 +            ok($disabled->Id, 'Disabled scrip loaded');
 +            is($disabled->Description, 'Disabled Scrip', 'Description');
 +            is($disabled->Template, 'Blank', 'Template');
 +            is($disabled->ConditionObj->Name, 'User Defined', 'Condition');
 +            is($disabled->ActionObj->Name, 'User Defined', 'Action');
 +            is($disabled->CustomIsApplicableCode, 'return "condition"', 'Condition code');
 +            is($disabled->CustomPrepareCode, 'return "prepare"', 'Prepare code');
 +            is($disabled->CustomCommitCode, 'return "commit"', 'Commit code');
 +            ok($disabled->Disabled, 'Disabled');
 +            ok($disabled->IsGlobal, 'IsGlobal');
 +
 +            my $stages = RT::Scrip->new(RT->SystemUser);
 +            $stages->LoadByCols(Description => 'Staged Scrip');
 +            ok($stages->Id, 'Staged scrip loaded');
 +            is($stages->Description, 'Staged Scrip');
 +            ok(!$stages->Disabled, 'not Disabled');
 +            ok(!$stages->IsGlobal, 'not Global');
 +
 +            my $bug_objectscrip = $stages->IsAdded($bugs->Id);
 +            ok($bug_objectscrip, 'added to Bugs');
 +            is($bug_objectscrip->Stage, 'TransactionBatch', 'Stage');
 +            is($bug_objectscrip->SortOrder, 42, 'SortOrder');
 +
 +            my $features_objectscrip = $stages->IsAdded($features->Id);
 +            ok($features_objectscrip, 'added to Features');
 +            is($features_objectscrip->Stage, 'TransactionCreate', 'Stage');
 +            is($features_objectscrip->SortOrder, 99, 'SortOrder');
 +
 +            ok(!$stages->IsAdded($general->Id), 'not added to General');
 +        },
 +    },
 +
 +    {
 +        name => 'No disabled scrips',
 +        create => sub {
 +            my $disabled = RT::Scrip->new(RT->SystemUser);
 +            my ($ok, $msg) = $disabled->Create(
 +                Description => 'Disabled Scrip',
 +                Template => 'Transaction',
 +                ScripCondition => 'On Create',
 +                ScripAction => 'Notify Owner',
 +            );
 +            ok($ok, $msg);
 +            ($ok, $msg) = $disabled->SetDisabled(1);
 +            ok($ok, $msg);
 +
 +            my $enabled = RT::Scrip->new(RT->SystemUser);
 +            ($ok, $msg) = $enabled->Create(
 +                Description => 'Enabled Scrip',
 +                Template => 'Transaction',
 +                ScripCondition => 'On Create',
 +                ScripAction => 'Notify Owner',
 +            );
 +            ok($ok, $msg);
 +        },
 +        present => sub {
 +            my $from_initialdata = shift;
 +
 +            my $disabled = RT::Scrip->new(RT->SystemUser);
 +            $disabled->LoadByCols(Description => 'Disabled Scrip');
 +
 +            if ($from_initialdata) {
 +                ok(!$disabled->Id, 'Disabled scrip absent in initialdata');
 +            }
 +            else {
 +                ok($disabled->Id, 'Disabled scrip present because of the original creation');
 +                ok($disabled->Disabled, 'Disabled scrip disabled');
 +            }
 +
 +            my $enabled = RT::Scrip->new(RT->SystemUser);
 +            $enabled->LoadByCols(Description => 'Enabled Scrip');
 +            ok($enabled->Id, 'Enabled scrip present');
 +        },
 +    },
 +
 +    {
 +        name => 'Disabled many-to-many relationships',
 +        create => sub {
 +            my $enabled_queue = RT::Queue->new(RT->SystemUser);
 +            my ($ok, $msg) = $enabled_queue->Create(
 +                Name => 'Enabled Queue',
 +            );
 +            ok($ok, $msg);
 +
 +            my $disabled_queue = RT::Queue->new(RT->SystemUser);
 +            ($ok, $msg) = $disabled_queue->Create(
 +                Name => 'Disabled Queue',
 +            );
 +            ok($ok, $msg);
 +
 +            my $enabled_cf = RT::CustomField->new(RT->SystemUser);
 +            ($ok, $msg) = $enabled_cf->Create(
 +                Name => 'Enabled CF',
 +                Type => 'FreeformSingle',
 +                LookupType => RT::Queue->CustomFieldLookupType,
 +            );
 +            ok($ok, $msg);
 +
 +            my $disabled_cf = RT::CustomField->new(RT->SystemUser);
 +            ($ok, $msg) = $disabled_cf->Create(
 +                Name => 'Disabled CF',
 +                Type => 'FreeformSingle',
 +                LookupType => RT::Queue->CustomFieldLookupType,
 +            );
 +            ok($ok, $msg);
 +
 +            my $enabled_scrip = RT::Scrip->new(RT->SystemUser);
 +            ($ok, $msg) = $enabled_scrip->Create(
 +                Queue => 0,
 +                Description => 'Enabled Scrip',
 +                Template => 'Blank',
 +                ScripCondition => 'On Create',
 +                ScripAction => 'Notify Owner',
 +            );
 +            ok($ok, $msg);
 +            $enabled_scrip->RemoveFromObject(0);
 +
 +            my $disabled_scrip = RT::Scrip->new(RT->SystemUser);
 +            ($ok, $msg) = $disabled_scrip->Create(
 +                Queue => 0,
 +                Description => 'Disabled Scrip',
 +                Template => 'Blank',
 +                ScripCondition => 'On Create',
 +                ScripAction => 'Notify Owner',
 +            );
 +            ok($ok, $msg);
 +            $disabled_scrip->RemoveFromObject(0);
 +
 +            my $enabled_class = RT::Class->new(RT->SystemUser);
 +            ($ok, $msg) = $enabled_class->Create(
 +                Name => 'Enabled Class',
 +            );
 +            ok($ok, $msg);
 +
 +            my $disabled_class = RT::Class->new(RT->SystemUser);
 +            ($ok, $msg) = $disabled_class->Create(
 +                Name => 'Disabled Class',
 +            );
 +            ok($ok, $msg);
 +
 +            my $enabled_role = RT::CustomRole->new(RT->SystemUser);
 +            ($ok, $msg) = $enabled_role->Create(
 +                Name => 'Enabled Role',
 +            );
 +            ok($ok, $msg);
 +
 +            my $disabled_role = RT::CustomRole->new(RT->SystemUser);
 +            ($ok, $msg) = $disabled_role->Create(
 +                Name => 'Disabled Role',
 +            );
 +            ok($ok, $msg);
 +
 +            my $enabled_group = RT::Group->new(RT->SystemUser);
 +            ($ok, $msg) = $enabled_group->CreateUserDefinedGroup(
 +                Name => 'Enabled Group',
 +            );
 +            ok($ok, $msg);
 +
 +            my $disabled_group = RT::Group->new(RT->SystemUser);
 +            ($ok, $msg) = $disabled_group->CreateUserDefinedGroup(
 +                Name => 'Disabled Group',
 +            );
 +            ok($ok, $msg);
 +
 +            my $enabled_user = RT::User->new(RT->SystemUser);
 +            ($ok, $msg) = $enabled_user->Create(
 +                Name => 'Enabled User',
 +            );
 +            ok($ok, $msg);
 +
 +            my $disabled_user = RT::User->new(RT->SystemUser);
 +            ($ok, $msg) = $disabled_user->Create(
 +                Name => 'Disabled User',
 +            );
 +            ok($ok, $msg);
 +
 +            for my $object ($enabled_cf, $disabled_cf,
 +                            $enabled_scrip, $disabled_scrip,
 +                            $enabled_class, $disabled_class,
 +                            $enabled_role, $disabled_role) {
 +
 +                # slightly inconsistent API
 +                my ($queue_a, $queue_b) = ($disabled_queue, $enabled_queue);
 +                ($queue_a, $queue_b) = ($queue_a->Id, $queue_b->Id)
 +                    if $object->isa('RT::Scrip')
 +                    || $object->isa('RT::CustomRole');
 +
 +                ($ok, $msg) = $object->AddToObject($queue_a);
 +                ok($ok, $msg);
 +
 +                ($ok, $msg) = $object->AddToObject($queue_b);
 +                ok($ok, $msg);
 +            }
 +
 +            for my $principal ($enabled_group, $disabled_group,
 +                               $enabled_user, $disabled_user) {
 +                ($ok, $msg) = $principal->PrincipalObj->GrantRight(Object => RT->System, Right => 'SeeQueue');
 +                ok($ok, $msg);
 +
 +                for my $queue ($enabled_queue, $disabled_queue) {
 +                    ($ok, $msg) = $principal->PrincipalObj->GrantRight(Object => $queue, Right => 'ShowTicket');
 +                    ok($ok, $msg);
 +
 +                    ($ok, $msg) = $queue->AddWatcher(Type => 'AdminCc', PrincipalId => $principal->PrincipalId);
 +                    ok($ok, $msg);
 +                }
 +            }
 +
 +            for my $cf ($enabled_cf, $disabled_cf) {
 +                for my $queue ($enabled_queue, $disabled_queue) {
 +                    ($ok, $msg) = $queue->AddCustomFieldValue(Field => $cf->Id, Value => $cf->Name);
 +                    ok($ok, $msg);
 +                }
 +            }
 +
 +            for my $object ($disabled_queue, $disabled_cf,
 +                            $disabled_scrip, $disabled_class,
 +                            $disabled_role, $disabled_group,
 +                            $disabled_user) {
 +                ($ok, $msg) = $object->SetDisabled(1);
 +                ok($ok, $msg);
 +            }
 +        },
 +        present => sub {
 +            my $from_initialdata = shift;
 +
 +            my $enabled_queue = RT::Queue->new(RT->SystemUser);
 +            $enabled_queue->Load('Enabled Queue');
 +            ok($enabled_queue->Id, 'loaded Enabled queue');
 +            is($enabled_queue->Name, 'Enabled Queue', 'Enabled Queue Name');
 +
 +            my $disabled_queue = RT::Queue->new(RT->SystemUser);
 +            $disabled_queue->Load('Disabled Queue');
 +
 +            my $enabled_cf = RT::CustomField->new(RT->SystemUser);
 +            $enabled_cf->Load('Enabled CF');
 +            ok($enabled_cf->Id, 'loaded Enabled CF');
 +            is($enabled_cf->Name, 'Enabled CF', 'Enabled CF Name');
 +            ok($enabled_cf->IsAdded($enabled_queue->Id), 'Enabled CF added to General');
 +
 +            is($enabled_queue->FirstCustomFieldValue('Enabled CF'), 'Enabled CF', 'OCFV');
 +
 +            my $disabled_cf = RT::CustomField->new(RT->SystemUser);
 +            $disabled_cf->Load('Disabled CF');
 +
 +            my $enabled_scrip = RT::Scrip->new(RT->SystemUser);
 +            $enabled_scrip->LoadByCols(Description => 'Enabled Scrip');
 +            ok($enabled_scrip->Id, 'loaded Enabled Scrip');
 +            is($enabled_scrip->Description, 'Enabled Scrip', 'Enabled Scrip Name');
 +            ok($enabled_scrip->IsAdded($enabled_queue->Id), 'Enabled Scrip added to General');
 +            my $disabled_scrip = RT::Scrip->new(RT->SystemUser);
 +            $disabled_scrip->LoadByCols(Description => 'Disabled Scrip');
 +
 +            my $enabled_class = RT::Class->new(RT->SystemUser);
 +            $enabled_class->Load('Enabled Class');
 +            ok($enabled_class->Id, 'loaded Enabled Class');
 +            is($enabled_class->Name, 'Enabled Class', 'Enabled Class Name');
 +            ok($enabled_class->IsApplied($enabled_queue->Id), 'Enabled Class added to General');
 +
 +            my $disabled_class = RT::Class->new(RT->SystemUser);
 +            $disabled_class->Load('Disabled Class');
 +
 +            my $enabled_role = RT::CustomRole->new(RT->SystemUser);
 +            $enabled_role->Load('Enabled Role');
 +            ok($enabled_role->Id, 'loaded Enabled Role');
 +            is($enabled_role->Name, 'Enabled Role', 'Enabled Role Name');
 +            ok($enabled_role->IsAdded($enabled_queue->Id), 'Enabled Role added to General');
 +
 +            my $disabled_role = RT::CustomRole->new(RT->SystemUser);
 +            $disabled_role->Load('Disabled Role');
 +
 +            my $enabled_group = RT::Group->new(RT->SystemUser);
 +            $enabled_group->LoadUserDefinedGroup('Enabled Group');
 +            ok($enabled_group->Id, 'loaded Enabled Group');
 +            is($enabled_group->Name, 'Enabled Group', 'Enabled Group Name');
 +            ok($enabled_group->PrincipalObj->HasRight(Object => $enabled_queue, Right => 'ShowTicket'), 'Enabled Group has queue right');
 +            ok($enabled_group->PrincipalObj->HasRight(Object => RT->System, Right => 'SeeQueue'), 'Enabled Group has global right');
 +            ok($enabled_queue->AdminCc->HasMember($enabled_group->PrincipalObj), 'Enabled Group still queue watcher');
 +
 +            my $disabled_group = RT::Group->new(RT->SystemUser);
 +            $disabled_group->LoadUserDefinedGroup('Disabled Group');
 +
 +            my $enabled_user = RT::User->new(RT->SystemUser);
 +            $enabled_user->Load('Enabled User');
 +            ok($enabled_user->Id, 'loaded Enabled User');
 +            is($enabled_user->Name, 'Enabled User', 'Enabled User Name');
 +            ok($enabled_user->PrincipalObj->HasRight(Object => $enabled_queue, Right => 'ShowTicket'), 'Enabled User has queue right');
 +            ok($enabled_user->PrincipalObj->HasRight(Object => RT->System, Right => 'SeeQueue'), 'Enabled User has global right');
 +            ok($enabled_queue->AdminCc->HasMember($enabled_user->PrincipalObj), 'Enabled User still queue watcher');
 +
 +            my $disabled_user = RT::User->new(RT->SystemUser);
 +            $disabled_user->Load('Disabled User');
 +
 +            for my $object ($disabled_queue, $disabled_cf,
 +                            $disabled_scrip, $disabled_class,
 +                            $disabled_role, $disabled_group,
 +                            $disabled_user) {
 +                if ($from_initialdata) {
 +                    ok(!$object->Id, "disabled " . ref($object) . " excluded");
 +                }
 +                else {
 +                    ok($object->Disabled, "disabled " . ref($object));
 +                }
 +            }
 +        },
 +    },
 +
 +    {
 +        name => 'Unapplied Objects',
 +        create => sub {
 +            my $scrip = RT::Scrip->new(RT->SystemUser);
 +            my ($ok, $msg) = $scrip->Create(
 +                Queue => 0,
 +                Description => 'Unapplied Scrip',
 +                Template => 'Blank',
 +                ScripCondition => 'On Create',
 +                ScripAction => 'Notify Owner',
 +            );
 +            ok($ok, $msg);
 +            ($ok, $msg) = $scrip->RemoveFromObject(0);
 +            ok($ok, $msg);
 +
 +            my $cf = RT::CustomField->new(RT->SystemUser);
 +            ($ok, $msg) = $cf->Create(
 +                Name        => 'Unapplied CF',
 +                Type        => 'FreeformSingle',
 +                LookupType  => RT::Ticket->CustomFieldLookupType,
 +            );
 +            ok($ok, $msg);
 +
 +            my $class = RT::Class->new(RT->SystemUser);
 +            ($ok, $msg) = $class->Create(
 +                Name => 'Unapplied Class',
 +            );
 +            ok($ok, $msg);
 +
 +            my $role = RT::CustomRole->new(RT->SystemUser);
 +            ($ok, $msg) = $role->Create(
 +                Name => 'Unapplied Custom Role',
 +            );
 +            ok($ok, $msg);
 +        },
 +        present => sub {
 +            my $scrip = RT::Scrip->new(RT->SystemUser);
 +            $scrip->LoadByCols(Description => 'Unapplied Scrip');
 +            ok($scrip->Id, 'Unapplied scrip loaded');
 +            is($scrip->Description, 'Unapplied Scrip');
 +            ok(!$scrip->Disabled, 'not Disabled');
 +            ok(!$scrip->IsGlobal, 'not Global');
 +            ok(!$scrip->IsAdded($general->Id), 'not applied to General queue');
 +
 +            my $cf = RT::CustomField->new(RT->SystemUser);
 +            $cf->Load('Unapplied CF');
 +            ok($cf->Id, 'Unapplied CF loaded');
 +            is($cf->Name, 'Unapplied CF');
 +            ok(!$cf->Disabled, 'not Disabled');
 +            ok(!$cf->IsGlobal, 'not Global');
 +            ok(!$cf->IsAdded($general->Id), 'not applied to General queue');
 +
 +            my $class = RT::Class->new(RT->SystemUser);
 +            $class->Load('Unapplied Class');
 +            ok($class->Id, 'Unapplied Class loaded');
 +            is($class->Name, 'Unapplied Class');
 +            ok(!$class->Disabled, 'not Disabled');
 +            ok(!$class->IsApplied(0), 'not Global');
 +            ok(!$class->IsApplied($general->Id), 'not applied to General queue');
 +
 +            my $role = RT::CustomRole->new(RT->SystemUser);
 +            $role->Load('Unapplied Custom Role');
 +            ok($role->Id, 'Unapplied Custom Role loaded');
 +            is($role->Name, 'Unapplied Custom Role');
 +            ok(!$role->Disabled, 'not Disabled');
 +            ok(!$role->IsAdded(0), 'not Global');
 +            ok(!$role->IsAdded($general->Id), 'not applied to General queue');
 +        },
 +    },
 +
 +    {
 +        name => 'Global Objects',
 +        create => sub {
 +            my $scrip = RT::Scrip->new(RT->SystemUser);
 +            my ($ok, $msg) = $scrip->Create(
 +                Queue => 0,
 +                Description => 'Global Scrip',
 +                Template => 'Blank',
 +                ScripCondition => 'On Create',
 +                ScripAction => 'Notify Owner',
 +            );
 +            ok($ok, $msg);
 +
 +            my $cf = RT::CustomField->new(RT->SystemUser);
 +            ($ok, $msg) = $cf->Create(
 +                Name        => 'Global CF',
 +                Type        => 'FreeformSingle',
 +                LookupType  => RT::Ticket->CustomFieldLookupType,
 +            );
 +            ok($ok, $msg);
 +            ($ok, $msg) = $cf->AddToObject(RT::Queue->new(RT->SystemUser));
 +            ok($ok, $msg);
 +
 +            my $class = RT::Class->new(RT->SystemUser);
 +            ($ok, $msg) = $class->Create(
 +                Name => 'Global Class',
 +            );
 +            ok($ok, $msg);
 +            ($ok, $msg) = $class->AddToObject(RT::Queue->new(RT->SystemUser));
 +            ok($ok, $msg);
 +        },
 +        present => sub {
 +            my $scrip = RT::Scrip->new(RT->SystemUser);
 +            $scrip->LoadByCols(Description => 'Global Scrip');
 +            ok($scrip->Id, 'Global scrip loaded');
 +            is($scrip->Description, 'Global Scrip');
 +            ok(!$scrip->Disabled, 'not Disabled');
 +            ok($scrip->IsGlobal, 'Global');
 +            ok(!$scrip->IsAdded($general->Id), 'not applied to General queue');
 +
 +            my $cf = RT::CustomField->new(RT->SystemUser);
 +            $cf->Load('Global CF');
 +            ok($cf->Id, 'Global CF loaded');
 +            is($cf->Name, 'Global CF');
 +            ok(!$cf->Disabled, 'not Disabled');
 +            ok($cf->IsGlobal, 'Global');
 +            ok(!$cf->IsAdded($general->Id), 'not applied to General queue');
 +
 +            my $class = RT::Class->new(RT->SystemUser);
 +            $class->Load('Global Class');
 +            ok($class->Id, 'Global Class loaded');
 +            is($class->Name, 'Global Class');
 +            ok(!$class->Disabled, 'not Disabled');
 +            ok($class->IsApplied(0), 'Global');
 +            ok(!$class->IsApplied($general->Id), 'not applied to General queue');
 +        },
 +    },
 +    {
 +        name => 'Templates',
 +        create => sub {
 +            my $global = RT::Template->new(RT->SystemUser);
 +            my ($ok, $msg) = $global->Create(
 +                Name => 'Initialdata test',
 +                Queue => 0,
 +                Description => 'foo',
 +                Content => "Hello こんにちは",
 +                Type => "Simple",
 +            );
 +            ok($ok, $msg);
 +
 +            my $queue = RT::Template->new(RT->SystemUser);
 +            ($ok, $msg) = $queue->Create(
 +                Name => 'Initialdata test',
 +                Queue => $general->Id,
 +                Description => 'override for Swedes',
 +                Content => "Hello Hallå",
 +                Type => "Simple",
 +            );
 +            ok($ok, $msg);
 +
 +            my $standalone = RT::Template->new(RT->SystemUser);
 +            ($ok, $msg) = $standalone->Create(
 +                Name => 'Standalone test',
 +                Queue => $general->Id,
 +                Description => 'no global version',
 +                Content => "this was broken!",
 +                Type => "Perl",
 +            );
 +            ok($ok, $msg);
 +        },
 +        present => sub {
 +            my $global = RT::Template->new(RT->SystemUser);
 +            $global->LoadGlobalTemplate('Initialdata test');
 +            ok($global->Id, 'loaded template');
 +            is($global->Name, 'Initialdata test', 'Name');
 +            is($global->Queue, 0, 'Queue');
 +            is($global->Description, 'foo', 'Description');
 +            is($global->Content, 'Hello こんにちは', 'Content');
 +            is($global->Type, 'Simple', 'Type');
 +
 +            my $queue = RT::Template->new(RT->SystemUser);
 +            $queue->LoadQueueTemplate(Name => 'Initialdata test', Queue => $general->Id);
 +            ok($queue->Id, 'loaded template');
 +            is($queue->Name, 'Initialdata test', 'Name');
 +            is($queue->Queue, $general->Id, 'Queue');
 +            is($queue->Description, 'override for Swedes', 'Description');
 +            is($queue->Content, 'Hello Hallå', 'Content');
 +            is($queue->Type, 'Simple', 'Type');
 +
 +            my $standalone = RT::Template->new(RT->SystemUser);
 +            $standalone->LoadQueueTemplate(Name => 'Standalone test', Queue => $general->Id);
 +            ok($standalone->Id, 'loaded template');
 +            is($standalone->Name, 'Standalone test', 'Name');
 +            is($standalone->Queue, $general->Id, 'Queue');
 +            is($standalone->Description, 'no global version', 'Description');
 +            is($standalone->Content, 'this was broken!', 'Content');
 +            is($standalone->Type, 'Perl', 'Type');
 +        },
 +    },
 +    {
 +        name => 'Articles',
 +        create => sub {
 +            my $class = RT::Class->new(RT->SystemUser);
 +            my ($ok, $msg) = $class->Create(
 +                Name => 'Test',
 +            );
 +            ok($ok, $msg);
 +
 +            my $content = RT::CustomField->new(RT->SystemUser);
 +            $content->LoadByCols(
 +                Name => "Content",
 +                Type => "Text",
 +                LookupType => RT::Article->CustomFieldLookupType,
 +            );
 +            ok($content->Id, "loaded builtin Content CF");
 +
 +            my $tags = RT::CustomField->new(RT->SystemUser);
 +            ($ok, $msg) = $tags->Create(
 +                Name => "Tags",
 +                Type => "FreeformMultiple",
 +                LookupType => RT::Article->CustomFieldLookupType,
 +            );
 +            ok($ok, $msg);
 +            ($ok, $msg) = $tags->AddToObject($class);
 +            ok($ok, $msg);
 +
 +            my $clearance = RT::CustomField->new(RT->SystemUser);
 +            ($ok, $msg) = $clearance->Create(
 +                Name => "Clearance",
 +                Type => "SelectSingle",
 +                LookupType => RT::Article->CustomFieldLookupType,
 +            );
 +            ok($ok, $msg);
 +            ($ok, $msg) = $clearance->AddToObject($class);
 +            ok($ok, $msg);
 +
 +            ($ok, $msg) = $clearance->AddValue(Name => 'Unclassified');
 +            ok($ok, $msg);
 +            ($ok, $msg) = $clearance->AddValue(Name => 'Classified');
 +            ok($ok, $msg);
 +            ($ok, $msg) = $clearance->AddValue(Name => 'Top Secret');
 +            ok($ok, $msg);
 +
 +            my $coffee = RT::Article->new(RT->SystemUser);
 +            ($ok, $msg) = $coffee->Create(
 +                Class => 'Test',
 +                Name  => 'Coffee time',
 +                "CustomField-" . $content->Id => 'Always',
 +                "CustomField-" . $clearance->Id => 'Unclassified',
 +                "CustomField-" . $tags->Id => ['drink', 'coffee', 'how the humans live'],
 +            );
 +            ok($ok, $msg);
 +
 +            my $twd = RT::Article->new(RT->SystemUser);
 +            ($ok, $msg) = $twd->Create(
 +                Class => 'Test',
 +                Name  => 'Total world domination plans',
 +                "CustomField-" . $content->Id => 'REDACTED',
 +                "CustomField-" . $clearance->Id => 'Top Secret',
 +                "CustomField-" . $tags->Id => ['snakes', 'clowns'],
 +            );
 +            ok($ok, $msg);
 +        },
 +        present => sub {
 +            my $class = RT::Class->new(RT->SystemUser);
 +            $class->Load('Test');
 +            ok($class->Id, 'loaded class');
 +            is($class->Name, 'Test', 'Name');
 +
 +            my $coffee = RT::Article->new(RT->SystemUser);
 +            $coffee->LoadByCols(Name => 'Coffee time');
 +            ok($coffee->Id, 'loaded article');
 +            is($coffee->Name, 'Coffee time', 'Name');
 +            is($coffee->Class, $class->Id, 'Class');
 +            is($coffee->FirstCustomFieldValue('Content'), 'Always', 'Content CF');
 +            is($coffee->FirstCustomFieldValue('Clearance'), 'Unclassified', 'Clearance CF');
 +            is($coffee->CustomFieldValuesAsString('Tags', Separator => '.'), 'drink.coffee.how the humans live', 'Tags CF');
 +
 +            my $twd = RT::Article->new(RT->SystemUser);
 +            $twd->LoadByCols(Name => 'Total world domination plans');
 +            ok($twd->Id, 'loaded article');
 +            is($twd->Name, 'Total world domination plans', 'Name');
 +            is($twd->Class, $class->Id, 'Class');
 +            is($twd->FirstCustomFieldValue('Content'), 'REDACTED', 'Content CF');
 +            is($twd->FirstCustomFieldValue('Clearance'), 'Top Secret', 'Clearance CF');
 +            is($twd->CustomFieldValuesAsString('Tags', Separator => '.'), 'snakes.clowns', 'Tags CF');
 +        },
 +    },
 +    {
 +        name => 'Attributes',
 +        create => sub {
 +            my $root = RT::User->new(RT->SystemUser);
 +            my ($ok, $msg) = $root->Load('root');
 +            ok($ok, $msg);
 +
 +            my $dashboard = RT::Dashboard->new($root);
 +            ($ok, $msg) = $dashboard->Save(
 +                Name => 'My Dashboard',
 +                Privacy => 'RT::User-' . $root->Id,
 +            );
 +            ok($ok, $msg);
 +
 +            my $subscription = RT::Attribute->new($root);
 +            ($ok, $msg) = $subscription->Create(
 +                Name        => 'Subscription',
 +                Description => 'Subscription to dashboard ' . $dashboard->Id,
 +                ContentType => 'storable',
 +                Object      => $root,
 +                Content     => { 'Tuesday' => '1', 'DashboardId' => $dashboard->Id },
 +            );
 +        },
 +        present => sub {
 +            # Provided in core initialdata
 +            my $homepage = RT::Attribute->new(RT->SystemUser);
 +            $homepage->LoadByNameAndObject(Name => 'HomepageSettings', Object => RT->System);
 +            ok($homepage->Id, 'Loaded homepage attribute');
 +            is($homepage->Name, 'HomepageSettings', 'Name is HomepageSettings');
 +            is($homepage->Description, 'HomepageSettings', 'Description is HomepageSettings');
 +            is($homepage->ContentType, 'storable', 'ContentType is storable');
 +
 +            my $root = RT::User->new(RT->SystemUser);
 +            my ($ok, $msg) = $root->Load('root');
 +            ok($ok, $msg);
 +
 +            my $dashboard = RT::Attribute->new($root);
 +            $dashboard->LoadByNameAndObject(Name => 'Dashboard', Object => $root);
 +            ok($dashboard->Id, 'Loaded dashboard attribute with id ' . $dashboard->Id);
 +
 +            my $subscription = RT::Attribute->new($root);
 +            $subscription->LoadByNameAndObject(Name => 'Subscription', Object => $root);
 +            ok($subscription->Id, 'Loaded subscription attribute with id ' . $subscription->Id);
 +            is($subscription->ContentType, 'storable', 'ContentType is storable');
 +            is($subscription->Content->{DashboardId}, $dashboard->Id, 'Dashboard Id is ' . $dashboard->Id);
 +            is( $subscription->Description,
 +                'Subscription to dashboard ' . $dashboard->Id,
 +                'Description is "Subscription to dashboard ' . $dashboard->Id . '"'
 +              );
 +        },
 +    },
 +);
 +
 +my $id = 0;
 +for my $test (@tests) {
 +    $id++;
 +    my $directory = File::Spec->catdir(RT::Test->temp_directory, "export-$id");
 +
 +    # we get a lot of warnings about already-existing objects; suppress them
 +    # for now until we clean it up
 +    my $warn = $SIG{__WARN__};
 +    local $SIG{__WARN__} = sub {
 +        return if $_[0] =~ join '|', (
 +            qr/^Name in use$/,
 +            qr/^A Template with that name already exists$/,
 +            qr/^.* already has the right .* on .*$/,
 +            qr/^Invalid value for Name$/,
 +            qr/^Queue already exists$/,
 +            qr/^Invalid Name \(names must be unique and may not be all digits\)$/,
 +        );
 +
 +        # Avoid reporting this anonymous call frame as the source of the warning
 +        goto &$warn;
 +    };
 +
 +    my $name        = delete $test->{name};
 +    my $create      = delete $test->{create};
 +    my $absent      = delete $test->{absent};
 +    my $present     = delete $test->{present};
 +    my $raw         = delete $test->{raw};
 +    my $export_args = delete $test->{export_args};
 +    fail("Unexpected keys for test #$id ($name): " . join(', ', sort keys %$test)) if keys %$test;
 +
 +    subtest "$name (ordinary creation)" => sub {
 +        autorollback(sub {
 +            $absent->(0) if $absent;
 +            $create->();
 +            $present->(0) if $present;
 +            export_initialdata($directory, %{ $export_args || {} });
 +        });
 +    };
 +
 +    if ($raw) {
 +        subtest "$name (testing initialdata)" => sub {
 +            my $file = File::Spec->catfile($directory, "initialdata.json");
 +            my $content = slurp($file);
 +            my $json = JSON->new->decode($content);
 +            $raw->($json, $content);
 +        };
 +    }
 +
 +    subtest "$name (from export-$id/initialdata.json)" => sub {
 +        autorollback(sub {
 +            $absent->(1) if $absent;
 +            import_initialdata($directory);
 +            $present->(1) if $present;
 +        });
 +    };
 +}
 +
++TODO: {
++    # Failed to load object RT::Attribute-Dashboard
++    # Failed to load object RT::Attribute-Subscription
++    local $TODO = "attribute's attributes are not supported yet.";
++    Test::NoWarnings::had_no_warnings();
++    Test::NoWarnings::clear_warnings();
++}
++
 +RT::Test::done_testing();
 +
 +sub autorollback {
 +    my $code = shift;
 +
 +    $RT::Handle->BeginTransaction;
 +    {
 +        # avoid "Rollback and commit are mixed while escaping nested transaction" warnings
 +        # due to (begin; (begin; commit); rollback)
 +        no warnings 'redefine';
 +        local *DBIx::SearchBuilder::Handle::BeginTransaction = sub {};
 +        local *DBIx::SearchBuilder::Handle::Commit = sub {};
 +        local *DBIx::SearchBuilder::Handle::Rollback = sub {};
 +
 +        $code->();
 +    }
 +    $RT::Handle->Rollback;
 +}
 +
 +sub export_initialdata {
 +    my $directory = shift;
 +    my %args      = @_;
 +    local @RT::Record::ISA = qw( DBIx::SearchBuilder::Record RT::Base );
 +
 +    use RT::Migrate::Serializer::JSON;
 +    my $migrator = RT::Migrate::Serializer::JSON->new(
 +        Directory          => $directory,
 +        Verbose            => 0,
 +        AllUsers           => 0,
 +        FollowACL          => 1,
 +        FollowScrips       => 1,
 +        FollowTransactions => 0,
 +        FollowTickets      => 0,
 +        FollowAssets       => 0,
 +        FollowDisabled     => 0,
 +        %args,
 +    );
 +
 +    $migrator->Export;
 +}
 +
 +sub import_initialdata {
 +    my $directory = shift;
 +    my $initialdata = File::Spec->catfile($directory, "initialdata.json");
 +
 +    ok(-e $initialdata, "File $initialdata exists");
 +
 +    my ($rv, $msg) = RT->DatabaseHandle->InsertData( $initialdata, undef, disconnect_after => 0 );
 +    ok($rv, "Inserted test data from $initialdata")
 +        or diag "Error: $msg";
 +}
 +
 +sub slurp {
 +    my $file = shift;
 +    local $/;
 +    open (my $f, '<:encoding(UTF-8)', $file)
 +        or die "Cannot open initialdata file '$file' for read: $@";
 +    return scalar <$f>;
 +}
diff --cc t/customroles/sort_order.t
index 0000000000,e9ea6c94cf..0947690ab6
mode 000000,100644..100644
--- a/t/customroles/sort_order.t
+++ b/t/customroles/sort_order.t
@@@ -1,0 -1,69 +1,65 @@@
+ use strict;
+ use warnings;
+ 
+ use RT::Test tests => undef;
+ 
+ my $queue_name = "CRSortQueue-$$";
+ my $queue = RT::Test->load_or_create_queue( Name => $queue_name );
+ 
+ diag "create multiple CRs: B, A and C";
+ 
+ for my $name (qw/B A C/) {
+     my $cr = RT::CustomRole->new( RT->SystemUser );
+     my ( $ret, $msg ) = $cr->Create( Name => "CR $name", );
+     ok( $ret, "Custom Role $name created" );
+     ( $ret, $msg ) = $cr->AddToObject( $queue->id );
+     ok( $ret, "Added $name to $queue_name: $msg" );
+ }
+ 
+ my ( $baseurl, $m ) = RT::Test->started_ok;
+ ok( $m->login(), 'Logged in' );
+ 
+ diag "reorder CRs: C, A and B";
+ {
+     $m->get_ok('/Admin/Queues/');
+     $m->follow_link_ok( { text => $queue->id } );
+     $m->follow_link_ok( { id   => 'page-custom-roles' } );
+     my @tmp = ( $m->content =~ /(CR [ABC])/g );
+     is_deeply( \@tmp, [ 'CR B', 'CR A', 'CR C' ], 'Order on admin page' );
+ 
+     $m->follow_link_ok( { text => '[Up]', n => 3 } );
+     $m->follow_link_ok( { text => '[Up]', n => 2 } );
+     $m->follow_link_ok( { text => '[Up]', n => 3 } );
+ 
+     @tmp = ( $m->content =~ /(CR [ABC])/g );
+     is_deeply( \@tmp, [ 'CR C', 'CR A', 'CR B' ], 'Order on updated admin page' );
+ }
+ 
+ diag "check ticket create, display and edit pages";
+ {
 -    $m->submit_form_ok(
 -        {   form_name => "CreateTicketInQueue",
 -            fields    => { Queue => $queue->Name },
 -        },
 -        'Get ticket create page'
 -    );
++    $m->submit_form_ok( { form_name => "CreateTicketInQueue" }, 'Get ticket create page' );
+ 
+     my @tmp = ( $m->content =~ /(CR [ABC])/g );
+     is_deeply( \@tmp, [ 'CR C', 'CR A', 'CR B' ], 'Order on ticket create page' );
+ 
+     $m->submit_form_ok(
+         {   form_name => "TicketCreate",
 -            fields    => { Subject => 'test' },
++            fields    => { Queue => $queue->Id, Subject => 'test' },
++            button    => 'SubmitTicket',
+         },
+         'Submit ticket create form'
+     );
+     my ($tid) = ( $m->content =~ /Ticket (\d+) created/i );
+     ok $tid, "Created a ticket succesfully";
+ 
+     @tmp = ( $m->content =~ /(CR [ABC])/g );
 -    is_deeply( \@tmp, [ 'CR C', 'CR A', 'CR B' ], 'Order on ticket display page' );
++    is_deeply( \@tmp, [ ( 'CR C', 'CR A', 'CR B' ) x 4 ], 'Order on ticket display page' );
+     $m->follow_link_ok( { text => 'People' } );
+ 
+     @tmp = ( $m->content =~ /(CR [ABC])/g );
+ 
+     # 3 "WatcherTypeEmail1" select boxes and 1 "Current watchers"
+     is_deeply( \@tmp, [ ( 'CR C', 'CR A', 'CR B' ) x 4 ], 'Order on ticket people page' );
+ }
+ 
+ done_testing;
diff --cc t/externalauth/ldap.t
index 671d3fcd95,b6d696abcb..6316491d0d
--- a/t/externalauth/ldap.t
+++ b/t/externalauth/ldap.t
@@@ -139,6 -150,8 +150,8 @@@ diag "test admin user create"
      $m->logout;
      ok( $m->login );
      $m->get_ok( $baseurl . '/Admin/Users/Modify.html?Create=1', 'user create page' );
 -    $m->text_contains( 'Employee Type:Select one value Set from external source' );
 -    $m->text_contains( 'Employee ID:Enter one value Set from external source' );
++    $m->text_contains( 'Employee Type:  Set from external source' );
++    $m->text_contains( 'Employee ID:  Set from external source' );
  
      my $username = 'testuser2';
      $m->submit_form(
diff --cc t/web/search_linkdisplay.t
index 794369e38d,4a12b0871a..9f5c817415
--- a/t/web/search_linkdisplay.t
+++ b/t/web/search_linkdisplay.t
@@@ -59,4 -59,48 +59,48 @@@ $ref = $m->find_link( url_regex => qr!/
  ok( $ref, "found article link" );
  is( $ref->text, $article->URIObj->Resolver->AsString, $article->URIObj->Resolver->AsString . " is displayed" );
  
+ 
+ # Get a search that returns multiple tickets
+ $m->get_ok("/Search/Results.html?Format=id,RefersTo;Query=id>0");
+ 
+ ok $m->goto_ticket( $ticket->Id ), 'opened diplay page of ticket # ' . $ticket->Id;
 -my $t_link = $m->find_link( id => "search-tickets-next" )->url;
++my $t_link = $m->dom->at("#li-page-next a")->attr('href');
+ is( $t_link, "/Ticket/Display.html?id=" . $ticket2->Id, 'link to the next ticket in current search found' );
+ 
+ diag "Set ShowSearchNavigation to false and confirm we do not load navigation links.";
+ {
+     RT::Test->stop_server;
+     RT->Config->Set( 'ShowSearchNavigation' => 0 );
+     ( $baseurl, $m ) = RT::Test->started_ok;
+ 
+     # Get a search that returns multiple tickets
+     $m->get_ok("/Search/Results.html?Format=id,RefersTo;Query=id>0");
+ 
+     ok $m->goto_ticket( $ticket->Id ), 'opened diplay page of ticket # ' . $ticket->Id;
 -    $t_link = $m->find_link( id => "search-tickets-next" );
++    $t_link = $m->dom->at("#li-page-next a");
+     is( $t_link, undef, "Search navigation results are not rendered" );
+ }
+ 
+ diag "Override ShowSearchNavigation at user pref level.";
+ {
+     ok( $m->login( 'root', 'password' ), 'logged in as root' );
+ 
+     my $root = RT::User->new( RT->SystemUser );
+     $root->Load('root');
+     ok( $root->Id, "Loaded root user" );
+ 
+     $root->SetPreferences( $RT::System => { %{ $root->Preferences($RT::System) || {} }, ShowSearchNavigation => 1 } );
+ 
+     is( RT::Config->Get( 'ShowSearchNavigation', $root ), 1, "User pref for ShowSearchNavigation successfully set." );
+ 
+     $m->get_ok("/Search/Results.html?Format=id,RefersTo;Query=id>0");
+ 
+     ok $m->goto_ticket( $ticket->Id ), 'opened diplay page of ticket # ' . $ticket->Id;
 -    my $t_link = $m->find_link( id => "search-tickets-next" )->url;
++    my $t_link = $m->dom->at("#li-page-next a")->attr('href');
+     is( $t_link, "/Ticket/Display.html?id=" . $ticket2->Id, 'link to the next ticket in current search found' );
+ 
+     $root->SetPreferences( $RT::System => { %{ $root->Preferences($RT::System) || {} }, ShowSearchNavigation => 0 } );
+     is( RT::Config->Get( 'ShowSearchNavigation', $root ), 0, "User pref for ShowSearchNavigation successfully set." );
+ }
+ 
  done_testing;

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


More information about the rt-commit mailing list