[Rt-commit] rt branch, 5.0/custom-date-ranges-config-ui, created. rt-5.0.0alpha1-245-g0355413e5a

? sunnavy sunnavy at bestpractical.com
Wed May 6 21:44:56 EDT 2020


The branch, 5.0/custom-date-ranges-config-ui has been created
        at  0355413e5af580eaa013bda475ebd41e5d441483 (commit)

- Log -----------------------------------------------------------------
commit ff4b420e17fe87183037155b895e6438d976d390
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..6eae2fb4d5 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,22 @@
 </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') &>
+  <&|/Widgets/TitleBox, title => loc('Custom Date Ranges'), class => 'mx-auto max-width-xl' &>
   <div class="table-responsive">
-    <table class="collection-as-table">
-      <tr class="collection-as-table">
+    <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 +105,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 +126,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 +147,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 b4788d39b8bba19a7b5f0f5328a8dea0a1629186
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 6eae2fb4d5..e375a30375 100644
--- a/share/html/Search/CustomDateRanges.html
+++ b/share/html/Search/CustomDateRanges.html
@@ -152,11 +152,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;
 
@@ -266,12 +267,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 b7594c45a10eca4b24cddd81f87871e55091fb65
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 e685be5b86..f611c7e9be 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -4748,6 +4748,150 @@ sub UpdateDashboard {
     }
 }
 
+=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..a2dc468249
--- /dev/null
+++ b/share/html/Elements/EditCustomDateRanges
@@ -0,0 +1,110 @@
+%# 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="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 e375a30375..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,63 +61,7 @@
 
 <form name="CustomDateRanges" method="POST" method="?">
   <&|/Widgets/TitleBox, title => loc('Custom Date Ranges'), class => 'mx-auto max-width-xl' &>
-  <div class="table-responsive">
-    <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>
@@ -162,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 65a64c328abddfcf8e148c3251f02fe3324764ca
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 f611c7e9be..ce762f7e67 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -4784,11 +4784,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 );
@@ -4796,6 +4791,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 1f5bef7ba9628ca2de49411e2053ecc8a2bb41dd
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 5f5e453651ad0fe1d04bad3d45f31a568a91d841
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 b6b91bebb970b0e4713ea205cc7a858aa51a72d5
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 ce762f7e67..785ad02c7c 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -4750,6 +4750,10 @@ sub UpdateDashboard {
 
 =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
@@ -4876,18 +4880,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 0f1020bf46..0c5e44e3a7 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -644,8 +644,6 @@ sub BuildMainNav {
             title => loc('Edit Search'), path => "/Search/Build.html$args" );
         $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) {
             $current_search_menu->child( results => title => loc('Show Results'), path => "/Search/Results.html$args" );
         }
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 1a62ec2062b88c4bd4855f061b85b35e4b840c9d
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 65c4cbd9e0e90c10f43348f0ea8574532b892e99
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 785ad02c7c..a5e8d8cbce 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -4748,29 +4748,43 @@ sub UpdateDashboard {
     }
 }
 
-=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 = (
@@ -4880,8 +4894,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 0c5e44e3a7..e58f21b903 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -299,6 +299,21 @@ sub BuildMainNav {
             );
 
         }
+
+        if ( $request_path eq '/Prefs/SearchOptions.html' ) {
+            $page->child(
+                custom_date_ranges => title => loc('Custom Date Ranges'),
+                path               => "/Prefs/CustomDateRanges.html"
+            );
+        }
+
+        if ( $request_path eq '/Prefs/CustomDateRanges.html' ) {
+            $page->child(
+                search_options => title => loc('Search Preferences'),
+                path               => "/Prefs/SearchOptions.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 0355413e5af580eaa013bda475ebd41e5d441483
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 => {

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


More information about the rt-commit mailing list