[Rt-commit] rt branch, 5.0-trunk, updated. rt-5.0.0alpha1-397-gac2b4eb06d

? sunnavy sunnavy at bestpractical.com
Thu May 7 14:10:57 EDT 2020


The branch, 5.0-trunk has been updated
       via  ac2b4eb06d657db1282a6ca554486a1053f619b8 (commit)
       via  b10d4d3c3ba94048eebaaab67e01d2d0ac23cb3f (commit)
       via  bc3ada1d433d3da1bd226742945dba1e429ec53f (commit)
       via  9ca4b0c7f6d4ef99c31049afa89d2e5fc08f4b33 (commit)
       via  07989c4868269db8444a34651de347de2d81d425 (commit)
       via  5fcae3941ba6f0ca87012c4848cd63c98368e9e0 (commit)
       via  45fc8e9483995520784075e702d7ee401999067b (commit)
       via  ebc027161b8f1a5ae1a52ccb9125af14d5be76a0 (commit)
       via  a6718b57ffe4fad276291b663254972a91bc5766 (commit)
       via  0c25a01ff7a58e56abd3bab9d0b07ede1c19fc06 (commit)
       via  408895569c451ce812ac5a5b8a10eacc6e7b7e1e (commit)
       via  17d87aed177c19b23c165373cdd7a84f50e702dc (commit)
       via  c24bf9daa10f9bf8b2963360ba593a2da26eddf6 (commit)
       via  6f01d7466309657840def5d38b46b94ec3c8208c (commit)
      from  77cd4ea99cfee73557898611aa5d980a3861501c (commit)

Summary of changes:
 etc/RT_Config.pm.in                                | 241 +++++++++--------
 lib/RT/Config.pm                                   |  37 +++
 lib/RT/Interface/Web.pm                            | 168 ++++++++++++
 lib/RT/Interface/Web/MenuBuilder.pm                |  13 +-
 lib/RT/Record.pm                                   |  41 ++-
 share/html/Admin/Tools/EditConfig.html             |   8 +-
 share/html/Elements/EditCustomDateRanges           | 112 ++++++++
 share/html/Elements/RT__Ticket/ColumnMap           |  20 +-
 share/html/Elements/SelectCustomDateRangeField     |   2 +-
 .../SelectAndOr => Elements/ShowCustomDateRanges}  |  35 ++-
 .../Create.html => Prefs/CustomDateRanges.html}    |  82 +++---
 share/html/Search/CustomDateRanges.html            | 299 ---------------------
 .../Form/CustomDateRanges}                         |  56 ++--
 share/static/css/elevator-light/admin.css          |   1 +
 14 files changed, 610 insertions(+), 505 deletions(-)
 create mode 100644 share/html/Elements/EditCustomDateRanges
 copy share/html/{Search/Elements/SelectAndOr => Elements/ShowCustomDateRanges} (63%)
 copy share/html/{Admin/Conditions/Create.html => Prefs/CustomDateRanges.html} (56%)
 delete mode 100644 share/html/Search/CustomDateRanges.html
 copy share/html/{Elements/EditCustomFieldDateTime => Widgets/Form/CustomDateRanges} (63%)

- Log -----------------------------------------------------------------
commit 6f01d7466309657840def5d38b46b94ec3c8208c
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Wed May 6 21:38:08 2020 +0800

    Migrate CustomDateRanges page to elevator themes

diff --git a/share/html/Elements/SelectCustomDateRangeField b/share/html/Elements/SelectCustomDateRangeField
index 9a08b84885..17a47f998b 100644
--- a/share/html/Elements/SelectCustomDateRangeField
+++ b/share/html/Elements/SelectCustomDateRangeField
@@ -45,7 +45,7 @@
 %# those contributions and any derivatives thereof.
 %#
 %# END BPS TAGGED BLOCK }}}
-<select name="<% $Name %>" id="<% $Name %>" class="chosen custom-date-range-field">
+<select name="<% $Name %>" id="<% $Name %>" class="form-control selectpicker custom-date-range-field">
 <option value="" >-</option>
 % for my $field ( $ObjectType->new( $session{CurrentUser} )->CustomDateRangeFields ) {
 <option <% $field eq ($Default//'') ? 'selected="selected"' : '' |n %> value="<% $field %>" ><% $field %></option>
diff --git a/share/html/Search/CustomDateRanges.html b/share/html/Search/CustomDateRanges.html
index c2768ad5ec..90c1429eeb 100644
--- a/share/html/Search/CustomDateRanges.html
+++ b/share/html/Search/CustomDateRanges.html
@@ -50,22 +50,22 @@
 
 <& /Elements/ListActions, actions => \@results &>
 
-<&|/Widgets/TitleBox, title => loc('Custom Date Ranges In Config Files') &>
+<&|/Widgets/TitleBox, title => loc('Custom Date Ranges In Config Files'), class => 'mx-auto max-width-xl' &>
 % if ( $config && keys %{$config->{'RT::Ticket'}} ) {
 <div class="table-responsive">
-  <table class="collection-as-table">
+  <table class="table collection-as-table">
     <tr class="collection-as-table">
       <th class="collection-as-table"><&|/l&>Name</&></th>
       <th class="collection-as-table"><&|/l&>From</&></th>
       <th class="collection-as-table"><&|/l&>From Value if Unset</&></th>
       <th class="collection-as-table"><&|/l&>To</&></th>
       <th class="collection-as-table"><&|/l&>To Value if Unset</&></th>
-      <th class="collection-as-table"><&|/l&>Business<br>Hours?</&></th>
+      <th class="collection-as-table"><&|/l&>Business Hours?</&></th>
     </tr>
 % my $i = 0;
 % for my $name ( sort keys %{$config->{'RT::Ticket'}} ) {
 % $i++;
-    <tr class="<% $i % 2 ? 'oddline' : 'evenline' %>">
+    <tr class="collection-as-table">
       <td class="collection-as-table"><% $name %></td>
 %     my $spec = $config->{'RT::Ticket'}{$name};
 %     my %date_range_spec = RT::Ticket->_ParseCustomDateRangeSpec($name, $spec);
@@ -80,22 +80,24 @@
 </div>
 % }
 % else {
-  <p><&|/l&>No custom date ranges in config files</&></p>
+  <p class="mt-3 mt-1 ml-3"><&|/l&>No custom date ranges in config files</&></p>
 % }
 </&>
 
 <form name="CustomDateRanges" method="POST" method="?">
-  <&|/Widgets/TitleBox, title => loc('Custom Date Ranges') &>
-  <div class="table-responsive">
-    <table class="collection-as-table">
-      <tr class="collection-as-table">
+  <&|/Widgets/TitleBox, title => loc('Custom Date Ranges'), class => 'mx-auto max-width-xl' &>
+%# TODO selectpicker options exceeding the table are invisible in .table-responsive
+%# <div class="table-responsive">
+  <div>
+    <table class="collection-as-table table">
+      <tr class="collection-as-table text-center">
         <th class="collection-as-table"><&|/l&>Name</&></th>
         <th class="collection-as-table"><&|/l&>From</&></th>
         <th class="collection-as-table"><&|/l&>From Value if Unset</&></th>
         <th class="collection-as-table"><&|/l&>To</&></th>
         <th class="collection-as-table"><&|/l&>To Value if Unset</&></th>
-        <th class="collection-as-table"><&|/l&>Business<br>Hours?</&></th>
-        <th class="collection-as-table">
+        <th class="collection-as-table"><&|/l&>Business Hours?</&></th>
+        <th class="collection-as-table text-left">
           <input type="checkbox" name="DeleteAll" value="1" onclick="setCheckbox(this, /^\d+-Delete$/)" />
           <&|/l&>Delete</&>
         </th>
@@ -105,15 +107,15 @@
 % my $id = 0;
 %   for my $name ( sort keys %{$content->{'RT::Ticket'}} ) {
 % $i++;
-      <tr class="<% $i % 2 ? 'oddline' : 'evenline' %>">
-        <td class="collection-as-table"><input type="text" name="<% $id %>-name" value="<% $name %>" /></td>
+      <tr class="collection-as-table">
+        <td class="collection-as-table"><input type="text" name="<% $id %>-name" value="<% $name %>" class="form-control" /></td>
 %       my %date_range_spec = RT::Ticket->_ParseCustomDateRangeSpec($name, $content->{'RT::Ticket'}{$name});
         <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => "$id-from", Default => $date_range_spec{from} &></td>
         <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => "$id-from_fallback", Default => $date_range_spec{from_fallback} &></td>
         <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => "$id-to", Default => $date_range_spec{to} &></td>
         <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => "$id-to_fallback", Default => $date_range_spec{to_fallback} &></td>
         <td class="collection-as-table">
-          <select name="<% $id %>-business_time">
+          <select name="<% $id %>-business_time" class="form-control selectpicker">
             <option value="1" <% $date_range_spec{business_time} ? 'selected="selected"' : '' |n%>><&|/l&>Yes</&></option>
             <option value="0" <% $date_range_spec{business_time} ? '': 'selected="selected"' |n%>><&|/l&>No</&></option>
           </select>
@@ -126,14 +128,14 @@
 
 % for ( 1 .. 3 ) {
 % $i++;
-      <tr class="<% $i % 2 ? 'oddline' : 'evenline' %>">
-        <td class="collection-as-table"><input type="text" name="name" value="" /></td>
+      <tr class="collection-as-table">
+        <td class="collection-as-table"><input type="text" name="name" value="" class="form-control" /></td>
         <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => 'from' &></td>
         <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => 'from_fallback' &></td>
         <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => 'to' &></td>
         <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => 'to_fallback' &></td>
         <td class="collection-as-table">
-          <select name="business_time">
+          <select name="business_time" class="form-control selectpicker">
             <option value="1"><&|/l&>Yes</&></option>
             <option value="0" selected="selected"><&|/l&>No</&></option>
           </select>
@@ -147,12 +149,6 @@
   </&>
 </form>
 
-<script type="text/javascript">
-jQuery(function() {
-    jQuery('select.chosen.custom-date-range-field').chosen({ width: '12em', no_results_text: ' ', search_contains: true });
-});
-</script>
-
 <%INIT>
 
 Abort(loc("Permission Denied")) unless $session{'CurrentUser'}->HasRight( Object=> RT->System, Right => 'SuperUser');

commit c24bf9daa10f9bf8b2963360ba593a2da26eddf6
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu May 7 02:13:48 2020 +0800

    Switch from system attribute to "CustomDateRangesUI" configuration
    
    We are going to move to system config page, and it makes more sense we
    use configuration correspondingly.

diff --git a/lib/RT/Record.pm b/lib/RT/Record.pm
index 99242eac13..6e53760122 100644
--- a/lib/RT/Record.pm
+++ b/lib/RT/Record.pm
@@ -2656,11 +2656,9 @@ sub CustomDateRanges {
         %ranges = %{ $config->{$type} } if $config->{$type};
     }
 
-    if ( my $attribute = RT->System->FirstAttribute('CustomDateRanges') ) {
-        if ( my $content = $attribute->Content ) {
-            for my $name ( keys %{ $content->{$type} || {} } ) {
-                $ranges{$name} ||= $content->{$type}{$name};
-            }
+    if ( my $db_config = RT->Config->Get('CustomDateRangesUI') ) {
+        for my $name ( keys %{ $db_config->{$type} || {} } ) {
+            $ranges{$name} ||= $db_config->{$type}{$name};
         }
     }
     return %ranges;
diff --git a/share/html/Search/CustomDateRanges.html b/share/html/Search/CustomDateRanges.html
index 90c1429eeb..b56e2197b5 100644
--- a/share/html/Search/CustomDateRanges.html
+++ b/share/html/Search/CustomDateRanges.html
@@ -154,11 +154,12 @@
 Abort(loc("Permission Denied")) unless $session{'CurrentUser'}->HasRight( Object=> RT->System, Right => 'SuperUser');
 
 my $config = RT->Config->Get('CustomDateRanges');
-my $attribute = RT->System->FirstAttribute('CustomDateRanges') || RT::Attribute->new( $session{CurrentUser} );
+my $db_config = RT::Configuration->new( $session{CurrentUser} );
+$db_config->LoadByCols( Name => 'CustomDateRangesUI', Disabled => 0 );
 
 my $content;
 
-$content = $attribute->Content if $attribute->id;
+$content = $db_config->_DeserializeContent( $db_config->Content ) if $db_config->id;
 
 my @results;
 
@@ -268,12 +269,11 @@ if ($Save) {
 
     if ($need_save) {
         my ( $ret, $msg );
-        if ( $attribute->id ) {
-            ( $ret, $msg ) = $attribute->SetContent($content);
+        if ( $db_config->id ) {
+            ( $ret, $msg ) = $db_config->SetContent($content);
         }
         else {
-            ( $ret, $msg )
-              = $attribute->Create( Name => 'CustomDateRanges', Object => RT->System, Content => $content );
+            ( $ret, $msg ) = $db_config->Create(Name => 'CustomDateRangesUI', Content => $content);
         }
 
         unless ($ret) {

commit 17d87aed177c19b23c165373cdd7a84f50e702dc
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu May 7 04:56:46 2020 +0800

    Abstract (Show/Edit/Process)CustomDateRanges for future reusage
    
    We will need them on both system edit config and user pref pages.

diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index f21eaf42be..39f9017f8c 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -4779,6 +4779,150 @@ sub ListOfReports {
     return $list_of_reports;
 }
 
+=head2 ProcessCustomDateRanges ARGSRef => ARGSREF
+
+Returns an array of results messages.
+
+=cut
+
+sub ProcessCustomDateRanges {
+    my %args = (
+        ARGSRef => undef,
+        @_
+    );
+    my $args_ref = $args{ARGSRef};
+
+    my $config  = RT->Config->Get('CustomDateRanges');
+    my $db_config = RT::Configuration->new( $session{CurrentUser} );
+    $db_config->LoadByCols( Name => 'CustomDateRangesUI', Disabled => 0 );
+
+    my $content;
+    $content = $db_config->_DeserializeContent( $db_config->Content ) if $db_config->id;
+
+    my @results;
+    my %label = (
+        from          => 'From',                   # loc
+        to            => 'To',                     # loc
+        from_fallback => 'From Value if Unset',    # loc
+        to_fallback   => 'To Value if Unset',      # loc
+    );
+
+    my $need_save;
+    if ($content) {
+        my @current_names = sort keys %{ $content->{'RT::Ticket'} };
+        for my $id ( 0 .. $#current_names ) {
+            my $current_name = $current_names[$id];
+            my $spec         = $content->{'RT::Ticket'}{$current_name};
+            my $name         = $args_ref->{"$id-name"};
+
+            if ( $config && $config->{'RT::Ticket'}{$name} ) {
+                push @results, loc( "[_1] already exists", $name );
+                next;
+            }
+
+            if ( $args_ref->{"$id-Delete"} ) {
+                delete $content->{'RT::Ticket'}{$current_name};
+                push @results, loc( 'Deleted [_1]', $current_name );
+                $need_save ||= 1;
+                next;
+            }
+
+            my $updated;
+            for my $field (qw/from from_fallback to to_fallback/) {
+                next if ( $spec->{$field} // '' ) eq $args_ref->{"$id-$field"};
+                if ((   $args_ref->{"$id-$field"}
+                        && RT::Ticket->_ParseCustomDateRangeSpec( $name, join ' - ', 'now', $args_ref->{"$id-$field"} )
+                    )
+                    || ( !$args_ref->{"$id-$field"} && $field =~ /fallback/ )
+                   )
+                {
+                    $spec->{$field} = $args_ref->{"$id-$field"};
+                    $updated ||= 1;
+                }
+                else {
+                    push @results, loc( 'Invalid [_1] for [_2]', loc( $label{$field} ), $name );
+                    next;
+                }
+            }
+
+            if ( $spec->{business_time} != $args_ref->{"$id-business_time"} ) {
+                $spec->{business_time} = $args_ref->{"$id-business_time"};
+                $updated ||= 1;
+            }
+
+            $content->{'RT::Ticket'}{$name} = $spec;
+            if ( $name ne $current_name ) {
+                delete $content->{'RT::Ticket'}{$current_name};
+                $updated ||= 1;
+            }
+
+            if ($updated) {
+                push @results, loc( 'Updated [_1]', $name );
+                $need_save ||= 1;
+            }
+        }
+    }
+
+    if ( $args_ref->{name} ) {
+        for my $field (qw/from from_fallback to to_fallback business_time/) {
+            $args_ref->{$field} = [ $args_ref->{$field} ] unless ref $args_ref->{$field};
+        }
+
+        my $i = 0;
+        for my $name ( @{ $args_ref->{name} } ) {
+            if ($name) {
+                if ( $config && $config->{'RT::Ticket'}{$name} || $content && $content->{'RT::Ticket'}{$name} ) {
+                    push @results, loc( "[_1] already exists", $name );
+                    $i++;
+                    next;
+                }
+            }
+            else {
+                $i++;
+                next;
+            }
+
+            my $spec = { business_time => $args_ref->{business_time}[$i] };
+            for my $field (qw/from from_fallback to to_fallback/) {
+                if ((   $args_ref->{$field}[$i]
+                        && RT::Ticket->_ParseCustomDateRangeSpec( $name, join ' - ', 'now', $args_ref->{$field}[$i] )
+                    )
+                    || ( !$args_ref->{$field}[$i] && $field =~ /fallback/ )
+                   )
+                {
+                    $spec->{$field} = $args_ref->{$field}[$i];
+                }
+                else {
+                    push @results, loc( 'Invalid [_1] for [_2]', loc($field), $name );
+                    $i++;
+                    next;
+                }
+            }
+
+            $content->{'RT::Ticket'}{$name} = $spec;
+            push @results, loc( 'Created [_1]', $name );
+            $need_save ||= 1;
+            $i++;
+        }
+    }
+
+    if ($need_save) {
+        my ( $ret, $msg );
+        if ( $db_config->id ) {
+            ( $ret, $msg ) = $db_config->SetContent($content);
+        }
+        else {
+            ( $ret, $msg ) = $db_config->Create(Name => 'CustomDateRangesUI', Content => $content);
+        }
+
+        unless ($ret) {
+            RT->Logger->error("Couldn't save content: $msg");
+            push @results, $msg;
+        }
+    }
+    return @results;
+}
+
 package RT::Interface::Web;
 RT::Base->_ImportOverlays();
 
diff --git a/share/html/Elements/EditCustomDateRanges b/share/html/Elements/EditCustomDateRanges
new file mode 100644
index 0000000000..48dcb26bc6
--- /dev/null
+++ b/share/html/Elements/EditCustomDateRanges
@@ -0,0 +1,112 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+
+%# TODO selectpicker options exceeding the table are invisible in .table-responsive
+%# <div class="table-responsive">
+<div>
+  <table class="collection-as-table table">
+    <tr class="collection-as-table text-center">
+      <th class="collection-as-table"><&|/l&>Name</&></th>
+      <th class="collection-as-table"><&|/l&>From</&></th>
+      <th class="collection-as-table"><&|/l&>From Value if Unset</&></th>
+      <th class="collection-as-table"><&|/l&>To</&></th>
+      <th class="collection-as-table"><&|/l&>To Value if Unset</&></th>
+      <th class="collection-as-table"><&|/l&>Business Hours?</&></th>
+      <th class="collection-as-table text-left">
+        <input type="checkbox" name="DeleteAll" value="1" onclick="setCheckbox(this, /^\d+-Delete$/)" />
+        <&|/l&>Delete</&>
+      </th>
+    </tr>
+% my $i = 0;
+% if ( keys %CustomDateRanges ) {
+% my $id = 0;
+%   for my $name ( sort keys %CustomDateRanges ) {
+% $i++;
+    <tr class="collection-as-table">
+      <td class="collection-as-table"><input type="text" name="<% $id %>-name" value="<% $name %>" class="form-control" /></td>
+%       my %date_range_spec = $ObjectType->_ParseCustomDateRangeSpec($name, $CustomDateRanges{$name});
+      <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => "$id-from", Default => $date_range_spec{from} &></td>
+      <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => "$id-from_fallback", Default => $date_range_spec{from_fallback} &></td>
+      <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => "$id-to", Default => $date_range_spec{to} &></td>
+      <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => "$id-to_fallback", Default => $date_range_spec{to_fallback} &></td>
+      <td class="collection-as-table">
+        <select name="<% $id %>-business_time" class="form-control selectpicker">
+          <option value="1" <% $date_range_spec{business_time} ? 'selected="selected"' : '' |n%>><&|/l&>Yes</&></option>
+          <option value="0" <% $date_range_spec{business_time} ? '': 'selected="selected"' |n%>><&|/l&>No</&></option>
+        </select>
+      </td>
+      <td class="collection-as-table"><input type="checkbox" name="<% $id %>-Delete" value="1" /></td>
+    </tr>
+%     $id++;
+%   }
+% }
+
+% for ( 1 .. 3 ) {
+% $i++;
+    <tr class="collection-as-table">
+      <td class="collection-as-table"><input type="text" name="name" value="" class="form-control" /></td>
+      <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => 'from' &></td>
+      <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => 'from_fallback' &></td>
+      <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => 'to' &></td>
+      <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => 'to_fallback' &></td>
+      <td class="collection-as-table">
+        <select name="business_time" class="form-control selectpicker">
+          <option value="1"><&|/l&>Yes</&></option>
+          <option value="0" selected="selected"><&|/l&>No</&></option>
+        </select>
+      </td>
+      <td class="collection-as-table"></td>
+    </tr>
+% }
+  </table>
+</div>
+
+<%ARGS>
+%CustomDateRanges => ()
+$ObjectType => 'RT::Ticket'
+</%ARGS>
diff --git a/share/html/Elements/ShowCustomDateRanges b/share/html/Elements/ShowCustomDateRanges
new file mode 100644
index 0000000000..04cce6f790
--- /dev/null
+++ b/share/html/Elements/ShowCustomDateRanges
@@ -0,0 +1,78 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+
+<div class="table-responsive">
+  <table class="table collection-as-table">
+    <tr class="collection-as-table">
+      <th class="collection-as-table"><&|/l&>Name</&></th>
+      <th class="collection-as-table"><&|/l&>From</&></th>
+      <th class="collection-as-table"><&|/l&>From Value if Unset</&></th>
+      <th class="collection-as-table"><&|/l&>To</&></th>
+      <th class="collection-as-table"><&|/l&>To Value if Unset</&></th>
+      <th class="collection-as-table"><&|/l&>Business Hours?</&></th>
+    </tr>
+% my $i = 0;
+% for my $name ( sort keys %CustomDateRanges ) {
+% $i++;
+    <tr class="collection-as-table">
+      <td class="collection-as-table"><% $name %></td>
+%     my %date_range_spec = $ObjectType->_ParseCustomDateRangeSpec($name, $CustomDateRanges{$name});
+      <td class="collection-as-table"><% $date_range_spec{from} %></td>
+      <td class="collection-as-table"><% $date_range_spec{from_fallback} || '' %></td>
+      <td class="collection-as-table"><% $date_range_spec{to} %></td>
+      <td class="collection-as-table"><% $date_range_spec{to_fallback} || '' %></td>
+      <td class="collection-as-table"><% $date_range_spec{business_time} ? loc('Yes') : loc('No') %></td>
+    </tr>
+% }
+  </table>
+</div>
+
+<%ARGS>
+%CustomDateRanges => ()
+$ObjectType => 'RT::Ticket'
+</%ARGS>
diff --git a/share/html/Search/CustomDateRanges.html b/share/html/Search/CustomDateRanges.html
index b56e2197b5..cef7ec821e 100644
--- a/share/html/Search/CustomDateRanges.html
+++ b/share/html/Search/CustomDateRanges.html
@@ -52,32 +52,7 @@
 
 <&|/Widgets/TitleBox, title => loc('Custom Date Ranges In Config Files'), class => 'mx-auto max-width-xl' &>
 % if ( $config && keys %{$config->{'RT::Ticket'}} ) {
-<div class="table-responsive">
-  <table class="table collection-as-table">
-    <tr class="collection-as-table">
-      <th class="collection-as-table"><&|/l&>Name</&></th>
-      <th class="collection-as-table"><&|/l&>From</&></th>
-      <th class="collection-as-table"><&|/l&>From Value if Unset</&></th>
-      <th class="collection-as-table"><&|/l&>To</&></th>
-      <th class="collection-as-table"><&|/l&>To Value if Unset</&></th>
-      <th class="collection-as-table"><&|/l&>Business Hours?</&></th>
-    </tr>
-% my $i = 0;
-% for my $name ( sort keys %{$config->{'RT::Ticket'}} ) {
-% $i++;
-    <tr class="collection-as-table">
-      <td class="collection-as-table"><% $name %></td>
-%     my $spec = $config->{'RT::Ticket'}{$name};
-%     my %date_range_spec = RT::Ticket->_ParseCustomDateRangeSpec($name, $spec);
-      <td class="collection-as-table"><% $date_range_spec{from} %></td>
-      <td class="collection-as-table"><% $date_range_spec{from_fallback} || '' %></td>
-      <td class="collection-as-table"><% $date_range_spec{to} %></td>
-      <td class="collection-as-table"><% $date_range_spec{to_fallback} || '' %></td>
-      <td class="collection-as-table"><% $date_range_spec{business_time} ? loc('Yes') : loc('No') %></td>
-    </tr>
-% }
-  </table>
-</div>
+  <& /Elements/ShowCustomDateRanges, CustomDateRanges => $config->{'RT::Ticket'} || {}, ObjectType => 'RT::Ticket' &>
 % }
 % else {
   <p class="mt-3 mt-1 ml-3"><&|/l&>No custom date ranges in config files</&></p>
@@ -86,65 +61,7 @@
 
 <form name="CustomDateRanges" method="POST" method="?">
   <&|/Widgets/TitleBox, title => loc('Custom Date Ranges'), class => 'mx-auto max-width-xl' &>
-%# TODO selectpicker options exceeding the table are invisible in .table-responsive
-%# <div class="table-responsive">
-  <div>
-    <table class="collection-as-table table">
-      <tr class="collection-as-table text-center">
-        <th class="collection-as-table"><&|/l&>Name</&></th>
-        <th class="collection-as-table"><&|/l&>From</&></th>
-        <th class="collection-as-table"><&|/l&>From Value if Unset</&></th>
-        <th class="collection-as-table"><&|/l&>To</&></th>
-        <th class="collection-as-table"><&|/l&>To Value if Unset</&></th>
-        <th class="collection-as-table"><&|/l&>Business Hours?</&></th>
-        <th class="collection-as-table text-left">
-          <input type="checkbox" name="DeleteAll" value="1" onclick="setCheckbox(this, /^\d+-Delete$/)" />
-          <&|/l&>Delete</&>
-        </th>
-      </tr>
-% my $i = 0;
-% if ( $content ) {
-% my $id = 0;
-%   for my $name ( sort keys %{$content->{'RT::Ticket'}} ) {
-% $i++;
-      <tr class="collection-as-table">
-        <td class="collection-as-table"><input type="text" name="<% $id %>-name" value="<% $name %>" class="form-control" /></td>
-%       my %date_range_spec = RT::Ticket->_ParseCustomDateRangeSpec($name, $content->{'RT::Ticket'}{$name});
-        <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => "$id-from", Default => $date_range_spec{from} &></td>
-        <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => "$id-from_fallback", Default => $date_range_spec{from_fallback} &></td>
-        <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => "$id-to", Default => $date_range_spec{to} &></td>
-        <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => "$id-to_fallback", Default => $date_range_spec{to_fallback} &></td>
-        <td class="collection-as-table">
-          <select name="<% $id %>-business_time" class="form-control selectpicker">
-            <option value="1" <% $date_range_spec{business_time} ? 'selected="selected"' : '' |n%>><&|/l&>Yes</&></option>
-            <option value="0" <% $date_range_spec{business_time} ? '': 'selected="selected"' |n%>><&|/l&>No</&></option>
-          </select>
-        </td>
-        <td class="collection-as-table"><input type="checkbox" name="<% $id %>-Delete" value="1" /></td>
-      </tr>
-%     $id++;
-%   }
-% }
-
-% for ( 1 .. 3 ) {
-% $i++;
-      <tr class="collection-as-table">
-        <td class="collection-as-table"><input type="text" name="name" value="" class="form-control" /></td>
-        <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => 'from' &></td>
-        <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => 'from_fallback' &></td>
-        <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => 'to' &></td>
-        <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => 'to_fallback' &></td>
-        <td class="collection-as-table">
-          <select name="business_time" class="form-control selectpicker">
-            <option value="1"><&|/l&>Yes</&></option>
-            <option value="0" selected="selected"><&|/l&>No</&></option>
-          </select>
-        </td>
-        <td class="collection-as-table"></td>
-      </tr>
-% }
-    </table>
-  </div>
+    <& /Elements/EditCustomDateRanges, CustomDateRanges => $content->{'RT::Ticket'} || {}, ObjectType => 'RT::Ticket' &>
     <& /Elements/Submit, Name => 'Save', Label => loc('Save Changes') &>
   </&>
 </form>
@@ -164,123 +81,7 @@ $content = $db_config->_DeserializeContent( $db_config->Content ) if $db_config-
 my @results;
 
 if ($Save) {
-    my %label = (
-        from => 'From', # loc
-        to => 'To', # loc
-        from_fallback => 'From Value if Unset', # loc
-        to_fallback => 'To Value if Unset', # loc
-    );
-
-    my $need_save;
-    if ($content) {
-        my @current_names = sort keys %{ $content->{'RT::Ticket'} };
-        for my $id ( 0 .. $#current_names ) {
-            my $current_name = $current_names[$id];
-            my $spec         = $content->{'RT::Ticket'}{$current_name};
-            my $name         = $ARGS{"$id-name"};
-
-            if ( $config && $config->{'RT::Ticket'}{$name} ) {
-                push @results, loc( "[_1] already exists", $name );
-                next;
-            }
-
-            if ( $ARGS{"$id-Delete"} ) {
-                delete $content->{'RT::Ticket'}{$current_name};
-                push @results, loc( 'Deleted [_1]', $current_name );
-                $need_save ||= 1;
-                next;
-            }
-
-            my $updated;
-            for my $field (qw/from from_fallback to to_fallback/) {
-                next if ( $spec->{$field} // '' ) eq $ARGS{"$id-$field"};
-                if ((   $ARGS{"$id-$field"}
-                        && RT::Ticket->_ParseCustomDateRangeSpec( $name, join ' - ', 'now', $ARGS{"$id-$field"} )
-                    )
-                    || ( !$ARGS{"$id-$field"} && $field =~ /fallback/ )
-                  )
-                {
-                    $spec->{$field} = $ARGS{"$id-$field"};
-                    $updated ||= 1;
-                }
-                else {
-                    push @results, loc( 'Invalid [_1] for [_2]', loc( $label{$field} ), $name );
-                    next;
-                }
-            }
-
-            if ( $spec->{business_time} != $ARGS{"$id-business_time"} ) {
-                $spec->{business_time} = $ARGS{"$id-business_time"};
-                $updated ||= 1;
-            }
-
-            $content->{'RT::Ticket'}{$name} = $spec;
-            if ( $name ne $current_name ) {
-                delete $content->{'RT::Ticket'}{$current_name};
-                $updated   ||= 1;
-            }
-
-            if ( $updated ) {
-                push @results, loc( 'Updated [_1]', $name );
-                $need_save ||= 1;
-            }
-        }
-    }
-
-    if ( $ARGS{name} ) {
-        for my $field (qw/from from_fallback to to_fallback business_time/) {
-            $ARGS{$field} = [ $ARGS{$field} ] unless ref $ARGS{$field};
-        }
-
-        my $i = 0;
-        for my $name ( @{ $ARGS{name} } ) {
-            if ($name) {
-                if ( $config && $config->{'RT::Ticket'}{$name} || $content && $content->{'RT::Ticket'}{$name} ) {
-                    push @results, loc( "[_1] already exists", $name );
-                    $i++;
-                    next;
-                }
-            }
-            else {
-                $i++;
-                next;
-            }
-
-            my $spec = { business_time => $ARGS{business_time}[$i] };
-            for my $field ( qw/from from_fallback to to_fallback/ ) {
-                if ( ($ARGS{$field}[$i] && RT::Ticket->_ParseCustomDateRangeSpec( $name, join ' - ', 'now', $ARGS{$field}[$i] ))
-                    || ( !$ARGS{$field}[$i] && $field =~ /fallback/  )
-                ) {
-                    $spec->{$field} = $ARGS{$field}[$i];
-                }
-                else {
-                    push @results, loc( 'Invalid [_1] for [_2]', loc($field), $name );
-                    $i++;
-                    next;
-                }
-            }
-
-            $content->{'RT::Ticket'}{$name} = $spec;
-            push @results, loc( 'Created [_1]', $name );
-            $need_save ||= 1;
-            $i++;
-        }
-    }
-
-    if ($need_save) {
-        my ( $ret, $msg );
-        if ( $db_config->id ) {
-            ( $ret, $msg ) = $db_config->SetContent($content);
-        }
-        else {
-            ( $ret, $msg ) = $db_config->Create(Name => 'CustomDateRangesUI', Content => $content);
-        }
-
-        unless ($ret) {
-            RT->Logger->error("Couldn't save content: $msg");
-            push @results, $msg;
-        }
-    }
+    push @results, ProcessCustomDateRanges( ARGSRef => \%ARGS );
 }
 
 MaybeRedirectForResults(

commit 408895569c451ce812ac5a5b8a10eacc6e7b7e1e
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu May 7 05:46:07 2020 +0800

    Delete before checking other rules for conflicts
    
    Otherwise, when there is a conflicted entry, people couldn't delete it.

diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index 39f9017f8c..e3e38b3cfd 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -4815,11 +4815,6 @@ sub ProcessCustomDateRanges {
             my $spec         = $content->{'RT::Ticket'}{$current_name};
             my $name         = $args_ref->{"$id-name"};
 
-            if ( $config && $config->{'RT::Ticket'}{$name} ) {
-                push @results, loc( "[_1] already exists", $name );
-                next;
-            }
-
             if ( $args_ref->{"$id-Delete"} ) {
                 delete $content->{'RT::Ticket'}{$current_name};
                 push @results, loc( 'Deleted [_1]', $current_name );
@@ -4827,6 +4822,11 @@ sub ProcessCustomDateRanges {
                 next;
             }
 
+            if ( $config && $config->{'RT::Ticket'}{$name} ) {
+                push @results, loc( "[_1] already exists", $name );
+                next;
+            }
+
             my $updated;
             for my $field (qw/from from_fallback to to_fallback/) {
                 next if ( $spec->{$field} // '' ) eq $args_ref->{"$id-$field"};

commit 0c25a01ff7a58e56abd3bab9d0b07ede1c19fc06
Author: michel <michel at bestpractical.com>
Date:   Mon Feb 3 14:56:10 2020 +0100

    Move CustomDateRanges to the Features section of the web config editor.

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 6834f32939..15ea944c5d 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -1736,119 +1736,6 @@ 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<%CustomDateRanges>
-
-This option lets you declare additional date ranges to be calculated
-and displayed in search results. Durations between any two core fields,
-as well as custom fields, are supported. Each custom date range is
-added as an additional display column in the search builder.
-
-Set C<%CustomDateRanges> to a nested structure similar to the following:
-
-    Set(%CustomDateRanges,
-        'RT::Ticket' => {
-            'Resolution Time' => 'Resolved - Created',
-
-            'Downtime' => {
-                value => 'CF.Recovered - CF.{First Alert}',
-                business_time => 1,
-            },
-
-            'Time To Beta' => {
-                value => 'CF.Beta - now',
-
-                format => sub {
-                    my ($duration, $beta, $now, $ticket) = @_;
-                    my $days = int($duration / (24*60*60));
-                    if ($days < 0) {
-                        $ticket->loc('[quant,_1,day,days] ago', -$days);
-                    }
-                    else {
-                        $ticket->loc('in [quant,_1,day,days]', $days);
-                    }
-                },
-            },
-        },
-    );
-
-The first level keys are record types. Each record type's value must be a
-hash reference. Each pair in the second-level hash defines a new range. The
-key is the range's name (which is displayed to users in the UI), and its
-value describes the range and must be either a string or a hashref.
-
-Values that are plain strings simply describe the calculation to be made.
-
-Values that are hashrefs that could include:
-
-=over 4
-
-=item value
-
-A string that describes the calculation to be made.
-
-The calculation is expected to be of the format C<"field - field"> where each
-field may be:
-
-=over 4
-
-=item * a core field
-
-For example, L<RT::Ticket> supports: Created, Starts, Started, LastUpdated,
-Told or LastContact, Due and Resolved.
-
-=item * a custom field
-
-You may use either C<CF.Name> or C<CF.{Longer Name}> syntax.
-
-=item * the word C<now>
-
-=back
-
-Custom date range calculations are defined using typical math operators with
-a space before and after. Subtraction (-) is currently supported.
-
-If either field and its corresponding fallback field(see blow) are both unset,
-then nothing will be displayed for that record (and the C<format> code
-reference will not be called).  If you need additional control over how
-results are calculated, see L<docs/customizing/search_result_columns.pod>.
-
-=item from and to
-
-When value is not set, C<from/to> will be used to calculate instead.
-Technically, C<Resolved - Created"> is equal to:
-
-    { from => 'Created', to => 'Resolved' }
-
-=item from_fallback and to_fallback
-
-Fallback fields when the main fields are unset, e.g.
-
-    {   from        => 'CF.{First Alert}',
-        to          => 'CF.Recovered',
-        to_fallback => 'now',
-    }
-
-When C<CF.Recovered> is unset, "now" will be used in the calculation.
-
-=item business_time
-
-A boolean value to indicate if it's a business time or not.
-
-When the schedule can't be deducted from corresponding object, the
-C<Default> one defined in C<%ServiceBusinessHours> will be used instead.
-
-=item format
-
-A code reference that allows customization of how the duration is displayed
-to users.  This code reference receives four parameters: the duration (a
-number of seconds), the end time (an L<RT::Date> object), the start time
-(another L<RT::Date>), and the record itself (which corresponds to the
-first-level key; in the example config above, it would be the L<RT::Ticket>
-object). The code reference should return the string to be displayed to the
-user.
-
-=back
-
 =item C<$CanonicalizeRedirectURLs>
 
 Set C<$CanonicalizeRedirectURLs> to 1 to use C<$WebURL> when
@@ -4695,6 +4582,124 @@ Set( %ServiceBusinessHours, );
 
 =back
 
+=head2 Custom Date Ranges
+
+=over 4
+
+=item C<%CustomDateRanges>
+
+This option lets you declare additional date ranges to be calculated
+and displayed in search results. Durations between any two core fields,
+as well as custom fields, are supported. Each custom date range is
+added as an additional display column in the search builder.
+
+Set C<%CustomDateRanges> to a nested structure similar to the following:
+
+    Set(%CustomDateRanges,
+        'RT::Ticket' => {
+            'Resolution Time' => 'Resolved - Created',
+
+            'Downtime' => {
+                value => 'CF.Recovered - CF.{First Alert}',
+                business_time => 1,
+            },
+
+            'Time To Beta' => {
+                value => 'CF.Beta - now',
+
+                format => sub {
+                    my ($duration, $beta, $now, $ticket) = @_;
+                    my $days = int($duration / (24*60*60));
+                    if ($days < 0) {
+                        $ticket->loc('[quant,_1,day,days] ago', -$days);
+                    }
+                    else {
+                        $ticket->loc('in [quant,_1,day,days]', $days);
+                    }
+                },
+            },
+        },
+    );
+
+The first level keys are record types. Each record type's value must be a
+hash reference. Each pair in the second-level hash defines a new range. The
+key is the range's name (which is displayed to users in the UI), and its
+value describes the range and must be either a string or a hashref.
+
+Values that are plain strings simply describe the calculation to be made.
+
+Values that are hashrefs that could include:
+
+=over 4
+
+=item value
+
+A string that describes the calculation to be made.
+
+The calculation is expected to be of the format C<"field - field"> where each
+field may be:
+
+=over 4
+
+=item * a core field
+
+For example, L<RT::Ticket> supports: Created, Starts, Started, LastUpdated,
+Told or LastContact, Due and Resolved.
+
+=item * a custom field
+
+You may use either C<CF.Name> or C<CF.{Longer Name}> syntax.
+
+=item * the word C<now>
+
+=back
+
+Custom date range calculations are defined using typical math operators with
+a space before and after. Subtraction (-) is currently supported.
+
+If either field and its corresponding fallback field(see blow) are both unset,
+then nothing will be displayed for that record (and the C<format> code
+reference will not be called).  If you need additional control over how
+results are calculated, see L<docs/customizing/search_result_columns.pod>.
+
+=item from and to
+
+When value is not set, C<from/to> will be used to calculate instead.
+Technically, C<Resolved - Created"> is equal to:
+
+    { from => 'Created', to => 'Resolved' }
+
+=item from_fallback and to_fallback
+
+Fallback fields when the main fields are unset, e.g.
+
+    {   from        => 'CF.{First Alert}',
+        to          => 'CF.Recovered',
+        to_fallback => 'now',
+    }
+
+When C<CF.Recovered> is unset, "now" will be used in the calculation.
+
+=item business_time
+
+A boolean value to indicate if it's a business time or not.
+
+When the schedule can't be deducted from corresponding object, the
+C<Default> one defined in C<%ServiceBusinessHours> will be used instead.
+
+=item format
+
+A code reference that allows customization of how the duration is displayed
+to users.  This code reference receives four parameters: the duration (a
+number of seconds), the end time (an L<RT::Date> object), the start time
+(another L<RT::Date>), and the record itself (which corresponds to the
+first-level key; in the example config above, it would be the L<RT::Ticket>
+object). The code reference should return the string to be displayed to the
+user.
+
+=back
+
+=back
 
 =cut
 

commit a6718b57ffe4fad276291b663254972a91bc5766
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu May 7 02:37:24 2020 +0800

    Add missing EOF newline
    
    Editors usually automatically add it for us, and it's annoying we drop
    this unrelated change every time.

diff --git a/share/html/Admin/Tools/EditConfig.html b/share/html/Admin/Tools/EditConfig.html
index 788e1806f3..0d7af8c6ec 100644
--- a/share/html/Admin/Tools/EditConfig.html
+++ b/share/html/Admin/Tools/EditConfig.html
@@ -195,4 +195,4 @@ my $nav_type='tab'; # 'tab' or 'pill'
 % }
   </div><!-- content-all -->
 </div><!-- titlebox-content -->
-</div><!-- configuration -->
\ No newline at end of file
+</div><!-- configuration -->

commit ebc027161b8f1a5ae1a52ccb9125af14d5be76a0
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu May 7 03:35:01 2020 +0800

    Move CustomDateRanges to system edit config page

diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index 853e05b971..ccac869202 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -1190,6 +1190,7 @@ our %META;
     },
     CustomDateRanges => {
         Type            => 'HASH',
+        Widget          => '/Widgets/Form/CustomDateRanges',
         PostLoadCheck   => sub {
             my $config = shift;
             # use scalar context intentionally to avoid not a hash error
@@ -1219,6 +1220,10 @@ our %META;
             }
         },
     },
+    CustomDateRangesUI => {
+        Type            => 'HASH',
+        Widget          => '/Widgets/Form/CustomDateRanges',
+    },
     ExternalStorage => {
         Type            => 'HASH',
         PostLoadCheck   => sub {
diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index e3e38b3cfd..e3ea6c3cf5 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -4781,6 +4781,10 @@ sub ListOfReports {
 
 =head2 ProcessCustomDateRanges ARGSRef => ARGSREF
 
+For system database configuration, it adds corresponding arguments to the
+passed ARGSRef, and the following code on EditConfig.html page will do the
+real update job.
+
 Returns an array of results messages.
 
 =cut
@@ -4907,18 +4911,8 @@ sub ProcessCustomDateRanges {
     }
 
     if ($need_save) {
-        my ( $ret, $msg );
-        if ( $db_config->id ) {
-            ( $ret, $msg ) = $db_config->SetContent($content);
-        }
-        else {
-            ( $ret, $msg ) = $db_config->Create(Name => 'CustomDateRangesUI', Content => $content);
-        }
-
-        unless ($ret) {
-            RT->Logger->error("Couldn't save content: $msg");
-            push @results, $msg;
-        }
+        $args_ref->{'CustomDateRangesUI-Current'} = ''; # EditConfig.html needs this to update CustomDateRangesUI
+        $args_ref->{CustomDateRangesUI} = $content;
     }
     return @results;
 }
diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm
index c8e58daeed..0d472f7a7a 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -670,8 +670,6 @@ sub BuildMainNav {
         if ( $current_user->HasRight( Right => 'ShowSearchAdvanced', Object => RT->System ) ) {
             $current_search_menu->child( advanced => title => loc('Advanced'), path => "/Search/Edit.html$args" );
         }
-        $current_search_menu->child( custom_date_ranges =>
-            title => loc('Custom Date Ranges'), path => "/Search/CustomDateRanges.html" ) if $class eq 'RT::Tickets';
         if ($has_query) {
             my $result_page = $HTML::Mason::Commands::DECODED_ARGS->{ResultPage};
             if ( my $web_path = RT->Config->Get('WebPath') ) {
diff --git a/share/html/Admin/Tools/EditConfig.html b/share/html/Admin/Tools/EditConfig.html
index 0d7af8c6ec..7cd701cadd 100644
--- a/share/html/Admin/Tools/EditConfig.html
+++ b/share/html/Admin/Tools/EditConfig.html
@@ -67,10 +67,16 @@ if (delete $ARGS{Update}) {
     $RT::Handle->BeginTransaction;
     my $has_error;
 
+    if ( delete $ARGS{CustomDateRanges} ) {
+        push @results, ProcessCustomDateRanges( ARGSRef => \%ARGS );
+    }
+
     eval {
         for my $key (keys %ARGS) {
             next if $key =~ /-Current$/;
             next if $key eq 'tab' || $key eq 'section' || $key eq 'subsection';
+            # Get rid of extra arguments like in CustomDateRanges
+            next if !exists $ARGS{$key . '-Current'};
 
             my $meta = RT->Config->Meta( $key );
             my $widget = $meta->{Widget} || '/Widgets/Form/Code';
diff --git a/share/html/Search/CustomDateRanges.html b/share/html/Widgets/Form/CustomDateRanges
similarity index 76%
rename from share/html/Search/CustomDateRanges.html
rename to share/html/Widgets/Form/CustomDateRanges
index cef7ec821e..e51dd2891d 100644
--- a/share/html/Search/CustomDateRanges.html
+++ b/share/html/Widgets/Form/CustomDateRanges
@@ -45,11 +45,18 @@
 %# those contributions and any derivatives thereof.
 %#
 %# END BPS TAGGED BLOCK }}}
-<& /Elements/Header, Title => loc('Custom Date Ranges') &>
-<& /Elements/Tabs &>
+<%DOC>
+see docs/extending/using_forms_widgets.pod
 
-<& /Elements/ListActions, actions => \@results &>
+This is a special widget only for custom date ranges, it obeys the
+InputOnly/Process basic structure but no general arguments like Name,
+Default, etc.
+</%DOC>
 
+<& SELF:InputOnly, %ARGS &>
+
+<%METHOD InputOnly>
+<input type="hidden" name="CustomDateRanges" value="1" />
 <&|/Widgets/TitleBox, title => loc('Custom Date Ranges In Config Files'), class => 'mx-auto max-width-xl' &>
 % if ( $config && keys %{$config->{'RT::Ticket'}} ) {
   <& /Elements/ShowCustomDateRanges, CustomDateRanges => $config->{'RT::Ticket'} || {}, ObjectType => 'RT::Ticket' &>
@@ -59,38 +66,20 @@
 % }
 </&>
 
-<form name="CustomDateRanges" method="POST" method="?">
-  <&|/Widgets/TitleBox, title => loc('Custom Date Ranges'), class => 'mx-auto max-width-xl' &>
-    <& /Elements/EditCustomDateRanges, CustomDateRanges => $content->{'RT::Ticket'} || {}, ObjectType => 'RT::Ticket' &>
-    <& /Elements/Submit, Name => 'Save', Label => loc('Save Changes') &>
-  </&>
-</form>
+<&|/Widgets/TitleBox, title => loc('Custom Date Ranges'), class => 'mx-auto max-width-xl' &>
+  <& /Elements/EditCustomDateRanges, CustomDateRanges => $content->{'RT::Ticket'} || {}, ObjectType => 'RT::Ticket' &>
+</&>
 
 <%INIT>
 
-Abort(loc("Permission Denied")) unless $session{'CurrentUser'}->HasRight( Object=> RT->System, Right => 'SuperUser');
-
 my $config = RT->Config->Get('CustomDateRanges');
 my $db_config = RT::Configuration->new( $session{CurrentUser} );
 $db_config->LoadByCols( Name => 'CustomDateRangesUI', Disabled => 0 );
 
 my $content;
-
 $content = $db_config->_DeserializeContent( $db_config->Content ) if $db_config->id;
-
-my @results;
-
-if ($Save) {
-    push @results, ProcessCustomDateRanges( ARGSRef => \%ARGS );
-}
-
-MaybeRedirectForResults(
-    Actions => \@results,
-    Path    => '/Search/CustomDateRanges.html',
-);
-
 </%INIT>
+</%METHOD>
 
-<%ARGS>
-$Save => undef
-</%ARGS>
+<%METHOD Process>
+</%METHOD>

commit 45fc8e9483995520784075e702d7ee401999067b
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu May 7 04:46:55 2020 +0800

    Add decent space for nested widgets on system edit config page
    
    E.g. "Custom Date Ranges In Config Files" and "Custom Date Ranges"
    widgets in "Features - Custom Date Ranges"

diff --git a/share/static/css/elevator-light/admin.css b/share/static/css/elevator-light/admin.css
index 6e847a83b0..df5a5e1924 100644
--- a/share/static/css/elevator-light/admin.css
+++ b/share/static/css/elevator-light/admin.css
@@ -211,6 +211,7 @@ div.inline-row i {
     margin-top: 0;
 }
 
+.configuration .tab-content .card.titlebox .card.titlebox,
 .configuration > .titlebox-content {
     margin-top: 20px;
 }

commit 5fcae3941ba6f0ca87012c4848cd63c98368e9e0
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu May 7 06:20:09 2020 +0800

    Support users(with ModifySelf) to update custom date ranges

diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index e3ea6c3cf5..832029f3c8 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -4779,29 +4779,43 @@ sub ListOfReports {
     return $list_of_reports;
 }
 
-=head2 ProcessCustomDateRanges ARGSRef => ARGSREF
+=head2 ProcessCustomDateRanges ARGSRef => ARGSREF, UserPreference => 0|1
 
 For system database configuration, it adds corresponding arguments to the
 passed ARGSRef, and the following code on EditConfig.html page will do the
 real update job.
 
+For user preference, it updates attributes accordingly.
+
 Returns an array of results messages.
 
 =cut
 
 sub ProcessCustomDateRanges {
     my %args = (
-        ARGSRef => undef,
+        ARGSRef        => undef,
+        UserPreference => 0,
         @_
     );
     my $args_ref = $args{ARGSRef};
 
-    my $config  = RT->Config->Get('CustomDateRanges');
-    my $db_config = RT::Configuration->new( $session{CurrentUser} );
-    $db_config->LoadByCols( Name => 'CustomDateRangesUI', Disabled => 0 );
+    my ( $config, $content );
+    if ( $args{UserPreference} ) {
+        $config = { 'RT::Ticket' => { RT::Ticket->CustomDateRanges( ExcludeUser => $session{CurrentUser}->Id ) } };
+        $content = $session{CurrentUser}->Preferences('CustomDateRanges');
+
+        # SetPreferences also checks rights, we short-circuit to avoid
+        # returning misleading messages.
 
-    my $content;
-    $content = $db_config->_DeserializeContent( $db_config->Content ) if $db_config->id;
+        return ( 0, loc("No permission to set preferences") )
+            unless $session{CurrentUser}->CurrentUserCanModify('Preferences');
+    }
+    else {
+        $config = RT->Config->Get('CustomDateRanges');
+        my $db_config = RT::Configuration->new( $session{CurrentUser} );
+        $db_config->LoadByCols( Name => 'CustomDateRangesUI', Disabled => 0 );
+        $content = $db_config->_DeserializeContent( $db_config->Content ) if $db_config->id;
+    }
 
     my @results;
     my %label = (
@@ -4911,8 +4925,24 @@ sub ProcessCustomDateRanges {
     }
 
     if ($need_save) {
-        $args_ref->{'CustomDateRangesUI-Current'} = ''; # EditConfig.html needs this to update CustomDateRangesUI
-        $args_ref->{CustomDateRangesUI} = $content;
+        if ( $args{UserPreference} ) {
+            my ( $ret, $msg );
+            if ( keys %{$content->{'RT::Ticket'}} ) {
+                ( $ret, $msg ) = $session{CurrentUser}->SetPreferences( 'CustomDateRanges', $content );
+            }
+            else {
+                ( $ret, $msg ) = $session{CurrentUser}->DeletePreferences( 'CustomDateRanges' );
+            }
+
+            unless ($ret) {
+                RT->Logger->error($msg);
+                push @results, $msg;
+            }
+        }
+        else {
+            $args_ref->{'CustomDateRangesUI-Current'} = ''; # EditConfig.html needs this to update CustomDateRangesUI
+            $args_ref->{CustomDateRangesUI} = $content;
+        }
     }
     return @results;
 }
diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm
index 0d472f7a7a..7d0f04911c 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -317,6 +317,17 @@ sub BuildMainNav {
             );
 
         }
+
+        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"
+            );
+        }
     }
     my $logout_url = RT->Config->Get('LogoutURL');
     if ( $current_user->Name
diff --git a/lib/RT/Record.pm b/lib/RT/Record.pm
index 6e53760122..d66b70405c 100644
--- a/lib/RT/Record.pm
+++ b/lib/RT/Record.pm
@@ -2648,17 +2648,46 @@ Return all of the custom date ranges of current class.
 
 sub CustomDateRanges {
     my $self = shift;
-    my $type = ref $self || $self;
+    my %args = (
+        Type          => undef,
+        ExcludeSystem => undef,
+        ExcludeUsers  => undef,
+        ExcludeUser   => undef,
+        @_,
+    );
 
+    my $type = $args{Type} || ref $self || $self,;
     my %ranges;
 
-    if ( my $config = RT->Config->Get('CustomDateRanges') ) {
-        %ranges = %{ $config->{$type} } if $config->{$type};
+    if ( !$args{ExcludeSystem} ) {
+        if ( my $config = RT->Config->Get('CustomDateRanges') ) {
+            for my $name ( keys %{ $config->{$type} || {} } ) {
+                $ranges{$name} ||= $config->{$type}{$name};
+            }
+        }
+
+        if ( my $db_config = RT->Config->Get('CustomDateRangesUI') ) {
+            for my $name ( keys %{ $db_config->{$type} || {} } ) {
+                $ranges{$name} ||= $db_config->{$type}{$name};
+            }
+        }
     }
 
-    if ( my $db_config = RT->Config->Get('CustomDateRangesUI') ) {
-        for my $name ( keys %{ $db_config->{$type} || {} } ) {
-            $ranges{$name} ||= $db_config->{$type}{$name};
+    if ( !$args{ExcludeUsers} ) {
+        my $attributes = RT::Attributes->new( RT->SystemUser );
+        $attributes->Limit( FIELD => 'Name',       VALUE => 'Pref-CustomDateRanges' );
+        $attributes->Limit( FIELD => 'ObjectType', VALUE => 'RT::User' );
+        if ( $args{ExcludeUser} ) {
+            $attributes->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $args{ExcludeUser} );
+        }
+        $attributes->OrderBy( FIELD => 'id' );
+
+        while ( my $attribute = $attributes->Next ) {
+            if ( my $content = $attribute->Content ) {
+                for my $name ( keys %{ $content->{$type} || {} } ) {
+                    $ranges{$name} ||= $content->{$type}{$name};
+                }
+            }
         }
     }
     return %ranges;
diff --git a/share/html/Prefs/CustomDateRanges.html b/share/html/Prefs/CustomDateRanges.html
new file mode 100644
index 0000000000..0ed9fbbc3e
--- /dev/null
+++ b/share/html/Prefs/CustomDateRanges.html
@@ -0,0 +1,100 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/Header, Title => loc('Custom Date Ranges') &>
+<& /Elements/Tabs &>
+
+<& /Elements/ListActions, actions => \@results &>
+
+<&|/Widgets/TitleBox, title => loc('System Custom Date Ranges'), class => 'mx-auto max-width-xl' &>
+% if ( keys %{$system_config->{'RT::Ticket'}} ) {
+  <& /Elements/ShowCustomDateRanges, CustomDateRanges => $system_config->{'RT::Ticket'} || {}, ObjectType => 'RT::Ticket' &>
+% }
+% else {
+  <p class="mt-3 mt-1 ml-3"><&|/l&>No system custom date ranges</&></p>
+% }
+</&>
+
+<&|/Widgets/TitleBox, title => loc('Other Users Custom Date Ranges'), class => 'mx-auto max-width-xl' &>
+% if ( keys %{$user_config->{'RT::Ticket'}} ) {
+  <& /Elements/ShowCustomDateRanges, CustomDateRanges => $user_config->{'RT::Ticket'} || {}, ObjectType => 'RT::Ticket' &>
+% }
+% else {
+  <p class="mt-3 mt-1 ml-3"><&|/l&>No other users custom date ranges</&></p>
+% }
+</&>
+
+<form name="CustomDateRanges" method="POST">
+  <&|/Widgets/TitleBox, title => loc('My Custom Date Ranges'), class => 'mx-auto max-width-xl' &>
+    <& /Elements/EditCustomDateRanges, CustomDateRanges => $content->{'RT::Ticket'} || {}, ObjectType => 'RT::Ticket' &>
+    <& /Elements/Submit, Name => 'Save', Label => loc('Save Changes') &>
+  </&>
+</form>
+
+<%INIT>
+my $system_config = { 'RT::Ticket' => { RT::Ticket->CustomDateRanges( ExcludeUsers => 1 ) } };
+my $user_config = {
+    'RT::Ticket' => { RT::Ticket->CustomDateRanges( ExcludeSystem => 1, ExcludeUser => $session{CurrentUser}->Id ) } };
+
+my $content = $session{CurrentUser}->Preferences('CustomDateRanges');
+
+my @results;
+
+if ($Save) {
+    push @results, ProcessCustomDateRanges( ARGSRef => \%ARGS, UserPreference => 1 );
+}
+
+MaybeRedirectForResults(
+    Actions => \@results,
+    Path    => '/Prefs/CustomDateRanges.html',
+);
+
+</%INIT>
+
+<%ARGS>
+$Save => undef
+</%ARGS>

commit 07989c4868269db8444a34651de347de2d81d425
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu May 7 09:38:03 2020 +0800

    Warn custom date range name conflicts on config load

diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index ccac869202..be9828119f 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -1218,6 +1218,38 @@ our %META;
                     RT->Logger->error("Config option \%CustomDateRanges{$class} is not a HASH");
                 }
             }
+
+            my %system_config = %$ranges;
+            if ( my $db_config = $config->Get('CustomDateRangesUI') ) {
+                for my $type ( keys %$db_config ) {
+                    for my $name ( keys %{ $db_config->{$type} || {} } ) {
+                        if ( $system_config{$type}{$name} ) {
+                            RT->Logger->warning("$type custom date range $name is defined by config file and db");
+                        }
+                        else {
+                            $system_config{$name} = $db_config->{$type}{$name};
+                        }
+                    }
+                }
+            }
+
+            for my $type ( keys %system_config ) {
+                my $attributes = RT::Attributes->new( RT->SystemUser );
+                $attributes->Limit( FIELD => 'Name',       VALUE => 'Pref-CustomDateRanges' );
+                $attributes->Limit( FIELD => 'ObjectType', VALUE => 'RT::User' );
+                $attributes->OrderBy( FIELD => 'id' );
+
+                while ( my $attribute = $attributes->Next ) {
+                    if ( my $content = $attribute->Content ) {
+                        for my $name ( keys %{ $content->{$type} || {} } ) {
+                            if ( $system_config{$type}{$name} ) {
+                                RT->Logger->warning( "$type custom date range $name is defined by system and user #"
+                                        . $attribute->ObjectId );
+                            }
+                        }
+                    }
+                }
+            }
         },
     },
     CustomDateRangesUI => {

commit 9ca4b0c7f6d4ef99c31049afa89d2e5fc08f4b33
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri May 8 00:40:42 2020 +0800

    Don't cache CustomDateRanges in ColumnMap
    
    Since custom date ranges could be updated via web UI, putting it inside
    <%ONCE> is wrong.

diff --git a/share/html/Elements/RT__Ticket/ColumnMap b/share/html/Elements/RT__Ticket/ColumnMap
index e09009aa59..f4fde6c58f 100644
--- a/share/html/Elements/RT__Ticket/ColumnMap
+++ b/share/html/Elements/RT__Ticket/ColumnMap
@@ -373,16 +373,6 @@ $COLUMN_MAP = {
    }
 };
 
-my %ranges = RT::Ticket->CustomDateRanges;
-for my $name (keys %ranges) {
-    $COLUMN_MAP->{$name} = {
-        title => $name,
-        value => sub {
-            $_[0]->CustomDateRange($name, $ranges{$name});
-        },
-    };
-}
-
 </%ONCE>
 <%init>
 # if no encryption support, then KeyOwnerName and KeyRequestors fall back to the regular
@@ -432,6 +422,16 @@ if ( RT->Config->Get('EnablePriorityAsString') ) {
     }
 }
 
+my %ranges = RT::Ticket->CustomDateRanges;
+for my $name (keys %ranges) {
+    $COLUMN_MAP->{$name} = {
+        title => $name,
+        value => sub {
+            $_[0]->CustomDateRange($name, $ranges{$name});
+        },
+    };
+}
+
 $m->callback( GenericMap => $GenericMap, COLUMN_MAP => $COLUMN_MAP, CallbackName => 'Once', CallbackOnce => 1 );
 return GetColumnMapEntry( Map => $COLUMN_MAP, Name => $Name, Attribute => $Attr );
 </%init>

commit bc3ada1d433d3da1bd226742945dba1e429ec53f
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Thu May 7 13:43:17 2020 -0400

    Break longer column headings for better spacing

diff --git a/share/html/Elements/EditCustomDateRanges b/share/html/Elements/EditCustomDateRanges
index 48dcb26bc6..f5f6e4ba22 100644
--- a/share/html/Elements/EditCustomDateRanges
+++ b/share/html/Elements/EditCustomDateRanges
@@ -53,10 +53,10 @@
     <tr class="collection-as-table text-center">
       <th class="collection-as-table"><&|/l&>Name</&></th>
       <th class="collection-as-table"><&|/l&>From</&></th>
-      <th class="collection-as-table"><&|/l&>From Value if Unset</&></th>
+      <th class="collection-as-table"><&|/l&>From Value<br>if Unset</&></th>
       <th class="collection-as-table"><&|/l&>To</&></th>
-      <th class="collection-as-table"><&|/l&>To Value if Unset</&></th>
-      <th class="collection-as-table"><&|/l&>Business Hours?</&></th>
+      <th class="collection-as-table"><&|/l&>To Value<br>if Unset</&></th>
+      <th class="collection-as-table"><&|/l&>Business<br>Hours?</&></th>
       <th class="collection-as-table text-left">
         <input type="checkbox" name="DeleteAll" value="1" onclick="setCheckbox(this, /^\d+-Delete$/)" />
         <&|/l&>Delete</&>
diff --git a/share/html/Elements/ShowCustomDateRanges b/share/html/Elements/ShowCustomDateRanges
index 04cce6f790..19280b3b67 100644
--- a/share/html/Elements/ShowCustomDateRanges
+++ b/share/html/Elements/ShowCustomDateRanges
@@ -51,10 +51,10 @@
     <tr class="collection-as-table">
       <th class="collection-as-table"><&|/l&>Name</&></th>
       <th class="collection-as-table"><&|/l&>From</&></th>
-      <th class="collection-as-table"><&|/l&>From Value if Unset</&></th>
+      <th class="collection-as-table"><&|/l&>From Value<br>if Unset</&></th>
       <th class="collection-as-table"><&|/l&>To</&></th>
-      <th class="collection-as-table"><&|/l&>To Value if Unset</&></th>
-      <th class="collection-as-table"><&|/l&>Business Hours?</&></th>
+      <th class="collection-as-table"><&|/l&>To Value<br>if Unset</&></th>
+      <th class="collection-as-table"><&|/l&>Business<br>Hours?</&></th>
     </tr>
 % my $i = 0;
 % for my $name ( sort keys %CustomDateRanges ) {

commit b10d4d3c3ba94048eebaaab67e01d2d0ac23cb3f
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Thu May 7 13:44:29 2020 -0400

    Reference web UI options in CustomDateRanges docs

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 15ea944c5d..103dca37c8 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -4589,9 +4589,19 @@ Set( %ServiceBusinessHours, );
 =item C<%CustomDateRanges>
 
 This option lets you declare additional date ranges to be calculated
-and displayed in search results. Durations between any two core fields,
-as well as custom fields, are supported. Each custom date range is
-added as an additional display column in the search builder.
+and displayed in search results. Durations between any two core date
+fields, as well as date custom fields, are supported. Each custom
+date range is added as an additional display column in the query builder.
+
+You can create basic date calculations via the web UI. SuperUsers can
+create them in the main System Configuration section. Individual users
+can also create date ranges in the Search options section of user
+preferences. More complicated configurations, such as those with
+custom code, can be added in your C<RT_SiteConfig.pm> file as described
+below.
+
+Business hours are also supported in calculations if you have
+L<%ServiceBusinessHours> configured.
 
 Set C<%CustomDateRanges> to a nested structure similar to the following:
 

commit ac2b4eb06d657db1282a6ca554486a1053f619b8
Merge: 77cd4ea99c b10d4d3c3b
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri May 8 02:03:21 2020 +0800

    Merge branch '5.0/custom-date-ranges-config-ui' into 5.0-trunk


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


More information about the rt-commit mailing list