[Rt-commit] rt branch, 4.6/lifecycle-ui-dev, updated. rt-4.4.4-636-g22fa7153f8

Craig Kaiser craig at bestpractical.com
Mon Feb 3 17:32:18 EST 2020


The branch, 4.6/lifecycle-ui-dev has been updated
       via  22fa7153f8db11e1c836012f9ceeef302928ffda (commit)
       via  7a8e1c4783d443ffebfea3611fa43d091c92652b (commit)
       via  7c07622c6fe81c7dc846c50e26b3bd6797a8bfff (commit)
       via  d84b4532033869fe438fcd628220c9bedd472723 (commit)
       via  5b812b1724a405a9f34dd9e3530b4092cfac8a48 (commit)
       via  475957a273b99325da2b16b9169b9f4a71973bc7 (commit)
      from  527bd331a431716a004e9628bdce40c7ffc74e42 (commit)

Summary of changes:
 lib/RT/Interface/Web.pm                            |   6 +
 lib/RT/Interface/Web/MenuBuilder.pm                |   4 +
 lib/RT/Lifecycle.pm                                | 175 +++++++++++++++++++--
 share/html/Admin/Lifecycles/Actions.html           | 175 +++++++++++++++++++++
 .../Lifecycles/{Modify.html => Advanced.html}      |  79 ++++++----
 share/html/Admin/Lifecycles/Modify.html            |   2 +-
 share/html/Admin/Lifecycles/Rights.html            | 161 +++++++++++++++++++
 share/html/Elements/Lifecycle/Graph                |  16 +-
 share/static/js/lifecycleui-editor.js              |  34 ++++
 9 files changed, 598 insertions(+), 54 deletions(-)
 create mode 100644 share/html/Admin/Lifecycles/Actions.html
 copy share/html/Admin/Lifecycles/{Modify.html => Advanced.html} (56%)
 create mode 100644 share/html/Admin/Lifecycles/Rights.html

- Log -----------------------------------------------------------------
commit 475957a273b99325da2b16b9169b9f4a71973bc7
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Fri Jan 31 15:27:10 2020 -0500

    Ensure lifecycle cache is updated across threads when flagged

diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index 688e459ea1..b230628593 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -275,6 +275,12 @@ sub HandleRequest {
         Module::Refresh->refresh;
     }
 
+    my $lifecycle = RT::Lifecycle->new( RT->SystemUser );
+    if ( RT->System->LifecycleCacheNeedsUpdate > $lifecycle->{'lifecycle_cache_time'} ) {
+        $lifecycle->FillCache();
+    }
+
+
     RT->Config->RefreshConfigFromDatabase();
 
     $HTML::Mason::Commands::r->content_type("text/html; charset=utf-8");
diff --git a/lib/RT/Lifecycle.pm b/lib/RT/Lifecycle.pm
index 4a33fe8909..88c8c3a54e 100644
--- a/lib/RT/Lifecycle.pm
+++ b/lib/RT/Lifecycle.pm
@@ -56,8 +56,6 @@ our %LIFECYCLES;
 our %LIFECYCLES_CACHE;
 our %LIFECYCLES_TYPES;
 
-my $lifecycle_cache_time = 0;
-
 # cache structure:
 #    {
 #        lifecycle_x => {
@@ -109,6 +107,7 @@ Simple constructor, takes no arguments.
 sub new {
     my $proto = shift;
     my $self = bless {}, ref($proto) || $proto;
+    $self->_Init(@_);
 
     return $self;
 }
@@ -144,12 +143,6 @@ sub Load {
     );
     $args{'Type'} = $args{'Type'} // 'ticket';
 
-    my $needs_update = RT->System->LifecycleCacheNeedsUpdate;
-    if ( $needs_update > $lifecycle_cache_time) {
-        $self->FillCache();
-        $lifecycle_cache_time = $needs_update;
-    }
-
     my $load_class = sub {
         if (defined $args{Name} and exists $LIFECYCLES_CACHE{ $args{Name} }) {
             $self->{'name'} = $args{Name};
@@ -212,12 +205,6 @@ sub ListAll {
     my $self = shift;
     my $for = shift || 'ticket';
 
-    my $needs_update = RT->System->LifecycleCacheNeedsUpdate;
-    if ( $needs_update > $lifecycle_cache_time) {
-        $self->FillCache();
-        $lifecycle_cache_time = $needs_update;
-    }
-
     return sort grep {$LIFECYCLES_CACHE{$_}{type} eq $for}
         grep $_ ne '__maps__', keys %LIFECYCLES_CACHE;
 }
@@ -802,8 +789,9 @@ sub FillCache {
         $class->RegisterRights if $class->require
             and $class->can("RegisterRights");
     }
-
-    $lifecycle_cache_time = time;
+    if ( ref $self ) {
+        $self->{'lifecycle_cache_time'} = time;
+    }
 
     return;
 }
@@ -1017,4 +1005,9 @@ sub UpdateMaps {
     return (1, $CurrentUser->loc("Lifecycle mappings updated"));
 }
 
+sub _Init {
+    my $self = shift;
+    $self->{'lifecycle_cache_time'} = 0;
+}
+
 1;

commit 5b812b1724a405a9f34dd9e3530b4092cfac8a48
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Mon Feb 3 14:07:52 2020 -0500

    Add advanced page for lifecycle UI configurations

diff --git a/lib/RT/Lifecycle.pm b/lib/RT/Lifecycle.pm
index 88c8c3a54e..0bbdb3e566 100644
--- a/lib/RT/Lifecycle.pm
+++ b/lib/RT/Lifecycle.pm
@@ -1005,6 +1005,137 @@ sub UpdateMaps {
     return (1, $CurrentUser->loc("Lifecycle mappings updated"));
 }
 
+sub ValidateLifecycle {
+    my $self      = shift;
+    my $lifecycle = shift;
+
+    my @warnings;
+
+    my $type = $lifecycle->{type} ||= 'ticket';
+    $LIFECYCLES_TYPES{$type} ||= {
+        '' => [],
+        initial => [],
+        active => [],
+        inactive => [],
+        actions => [],
+    };
+
+    my @statuses;
+    $lifecycle->{canonical_case} = {};
+    foreach my $category ( qw(initial active inactive) ) {
+        for my $status (@{ $lifecycle->{ $category } || [] }) {
+            if (exists $lifecycle->{canonical_case}{lc $status}) {
+                push @warnings, "Duplicate status @{[lc $status]} in lifecycle ".$self->Name;
+            } else {
+                $lifecycle->{canonical_case}{lc $status} = $status;
+            }
+            push @{ $LIFECYCLES_TYPES{$type}{$category} }, $status;
+            push @statuses, $status;
+        }
+    }
+
+    # Lower-case for consistency
+    # ->{actions} are handled below
+    for my $state (keys %{ $lifecycle->{defaults} || {} }) {
+        my $status = $lifecycle->{defaults}{$state};
+        push @warnings, "Nonexistant status @{[lc $status]} in default states in ".$self->Name." lifecycle"
+            unless $lifecycle->{canonical_case}{lc $status};
+        # $lifecycle->{defaults}{$state} =
+        #     $lifecycle->{canonical_case}{lc $status} || lc $status;
+    }
+    for my $from (keys %{ $lifecycle->{transitions} || {} }) {
+        push @warnings, "Nonexistant status @{[lc $from]} in transitions in ".$self->Name." lifecycle"
+            unless $from eq '' or $lifecycle->{canonical_case}{lc $from};
+        my @statuses = @{$lifecycle->{transitions}{$from}};
+
+        for my $status ( @statuses ) {
+            push @warnings, "Nonexistant status @{[lc $status]} in transitions in ".$self->Name." lifecycle"
+                unless $lifecycle->{canonical_case}{lc $status};
+            push @{ $lifecycle->{transitions}{lc $from} },
+        }
+    }
+    my $rights = $lifecycle->{rights} || {};
+
+    for my $schema (keys %{$rights}) {
+        my ($from, $to) = split /\s*->\s*/, $schema, 2;
+        unless ($from and $to) {
+            push @warnings, "Invalid right transition $schema in ".$self->Name." lifecycle";
+            next;
+        }
+        push @warnings, "Nonexistant status @{[lc $from]} in right transition in ".$self->Name." lifecycle"
+            unless $from eq '*' or $lifecycle->{canonical_case}{lc $from};
+        push @warnings, "Nonexistant status @{[lc $to]} in right transition in ".$self->Name." lifecycle"
+            unless $to eq '*' or $lifecycle->{canonical_case}{lc $to};
+
+        push @warnings, "Invalid right name ($lifecycle->{rights}{$schema}) in ".$self->Name." lifecycle; right names must be ASCII"
+            if $lifecycle->{rights}{$schema} =~ /\P{ASCII}/;
+
+        push @warnings, "Invalid right name ($lifecycle->{rights}{$schema}) in ".$self->Name." lifecycle; right names must be <= 25 characters"
+            if length($lifecycle->{rights}{$schema}) > 25;
+
+        $lifecycle->{rights}{lc($from) . " -> " .lc($to)}
+            = $lifecycle->{rights}{$schema};
+    }
+
+    my %seen;
+    @statuses = grep !$seen{ lc $_ }++, @statuses;
+    $lifecycle->{''} = \@statuses;
+
+    unless ( $lifecycle->{'transitions'}{''} ) {
+        $lifecycle->{'transitions'}{''} = [ grep lc $_ ne 'deleted', @statuses ];
+    }
+
+    my @actions;
+    if ( ref $lifecycle->{'actions'} eq 'HASH' ) {
+        foreach my $k ( sort keys %{ $lifecycle->{'actions'} } ) {
+            push @actions, $k, $lifecycle->{'actions'}{ $k };
+        }
+    } elsif ( ref $lifecycle->{'actions'} eq 'ARRAY' ) {
+        @actions = @{ $lifecycle->{'actions'} };
+    }
+
+    $lifecycle->{'actions'} = [];
+    while ( my ($transition, $info) = splice @actions, 0, 2 ) {
+        my ($from, $to) = split /\s*->\s*/, $transition, 2;
+        unless ($from and $to) {
+            push @warnings, "Invalid action status change $transition in ".$self->Name." lifecycle";
+            next;
+        }
+        push @warnings, "Nonexistant status @{[lc $from]} in action in ".$self->Name." lifecycle"
+            unless $from eq '*' or $lifecycle->{canonical_case}{lc $from};
+        push @warnings, "Nonexistant status @{[lc $to]} in action in ".$self->Name." lifecycle"
+            unless $to eq '*' or $lifecycle->{canonical_case}{lc $to};
+        push @{ $lifecycle->{'actions'} },
+            { %$info,
+                from => ($lifecycle->{canonical_case}{lc $from} || lc $from),
+                to   => ($lifecycle->{canonical_case}{lc $to}   || lc $to),   };
+    }
+
+    # Lower-case the transition maps
+    for my $mapname (keys %{ $LIFECYCLES_CACHE{'__maps__'} || {} }) {
+        my ($from, $to) = split /\s*->\s*/, $mapname, 2;
+        unless ($from and $to) {
+            push @warnings, "Invalid lifecycle mapping $mapname";
+            next;
+        }
+        push @warnings, "Nonexistant lifecycle $from in $mapname lifecycle map"
+            unless $LIFECYCLES_CACHE{$from};
+        push @warnings, "Nonexistant lifecycle $to in $mapname lifecycle map"
+            unless $LIFECYCLES_CACHE{$to};
+        my $map = $LIFECYCLES_CACHE{'__maps__'}{$mapname};
+        for my $status (keys %{ $map }) {
+            push @warnings, "Nonexistant status @{[lc $status]} in $from in $mapname lifecycle map"
+                if $LIFECYCLES_CACHE{$from}
+                    and not $LIFECYCLES_CACHE{$from}{canonical_case}{lc $status};
+            push @warnings, "Nonexistant status @{[lc $map->{$status}]} in $to in $mapname lifecycle map"
+                if $LIFECYCLES_CACHE{$to}
+                    and not $LIFECYCLES_CACHE{$to}{canonical_case}{lc $map->{$status}};
+        }
+    }
+
+    return @warnings;
+}
+
 sub _Init {
     my $self = shift;
     $self->{'lifecycle_cache_time'} = 0;
diff --git a/share/html/Admin/Lifecycles/Modify.html b/share/html/Admin/Lifecycles/Actions.html
similarity index 78%
copy from share/html/Admin/Lifecycles/Modify.html
copy to share/html/Admin/Lifecycles/Actions.html
index 5483bcc511..f587b115ec 100644
--- a/share/html/Admin/Lifecycles/Modify.html
+++ b/share/html/Admin/Lifecycles/Actions.html
@@ -49,25 +49,15 @@
 <& /Elements/Tabs &>
 <& /Elements/ListActions, actions => \@results &>
 
-<script type="text/javascript" src="<%RT->Config->Get('WebPath')%>/static/js/farbtastic.js"></script>
-
-<form action="<%RT->Config->Get('WebPath')%>/Admin/Lifecycles/Modify.html" name="ModifyLifecycle" method="post" enctype="multipart/form-data">
-  <input type="hidden" class="hidden" name="Name" value="<% $LifecycleObj->Name %>" />
-  <input type="hidden" class="hidden" name="Type" value="<% $LifecycleObj->Type %>" />
-  <& /Elements/Lifecycle/Graph, Lifecycle => $LifecycleObj->Name &>
-  <div class="col-md-12">
-    <& /Elements/Submit, Label => loc('Save Changes') &>
-  </div>
-</form>
 <%INIT>
 my ($title, @results);
 my $LifecycleObj = RT::Lifecycle->new();
 $LifecycleObj->Load(Name => $Name, Type => $Type);
 
 Abort("Invalid lifecycle") unless $LifecycleObj->Name
-                               && $LifecycleObj->{data}{type} eq $Type;
+                                && $LifecycleObj->{data}{type} eq $Type;
 
-$title = loc("Modify lifecycle [_1]", $LifecycleObj->Name);
+$title = loc("Rights for lifecycle [_1]", $LifecycleObj->Name);
 
 if ($Config) {
     my $LifecycleConfiguration = JSON::from_json($LifecycleConfiguration);
@@ -78,10 +68,10 @@ if ($Config) {
         Configuration  => $LifecycleConfiguration,
     );
     if ( $ok ) {
-      push @results, "Lifecycle updated";
+        push @results, "Lifecycle updated";
     }
     else {
-      push @results, "An error occured when attempting to update lifecycle, see RT log for more info.";
+        push @results, "An error occured when attempting to update lifecycle, see RT log for more info.";
     }
 }
 
@@ -97,3 +87,4 @@ $Type                   => undef
 $Config                 => undef
 $LifecycleConfiguration => undef
 </%ARGS>
+    
\ No newline at end of file
diff --git a/share/html/Admin/Lifecycles/Modify.html b/share/html/Admin/Lifecycles/Advanced.html
similarity index 56%
copy from share/html/Admin/Lifecycles/Modify.html
copy to share/html/Admin/Lifecycles/Advanced.html
index 5483bcc511..08e8d37fa0 100644
--- a/share/html/Admin/Lifecycles/Modify.html
+++ b/share/html/Admin/Lifecycles/Advanced.html
@@ -49,51 +49,76 @@
 <& /Elements/Tabs &>
 <& /Elements/ListActions, actions => \@results &>
 
-<script type="text/javascript" src="<%RT->Config->Get('WebPath')%>/static/js/farbtastic.js"></script>
-
-<form action="<%RT->Config->Get('WebPath')%>/Admin/Lifecycles/Modify.html" name="ModifyLifecycle" method="post" enctype="multipart/form-data">
+<form action="<%RT->Config->Get('WebPath')%>/Admin/Lifecycles/Advanced.html" name="ModifyLifecycleAdvanced" method="post" enctype="multipart/form-data">
   <input type="hidden" class="hidden" name="Name" value="<% $LifecycleObj->Name %>" />
   <input type="hidden" class="hidden" name="Type" value="<% $LifecycleObj->Type %>" />
-  <& /Elements/Lifecycle/Graph, Lifecycle => $LifecycleObj->Name &>
-  <div class="col-md-12">
-    <& /Elements/Submit, Label => loc('Save Changes') &>
+
+  <div class="form-row">
+    <span class="col-md-12">
+      <textarea class="form-control" cols="170" rows="20" name="Config" id="Config"><% JSON::to_json($config, { canonical => 1, pretty => 1 }) |n %></textarea>
+    </span>
+  </div>
+
+  <div class="form-row">
+    <div class="col-md-6 d-flex justify-content-between w-100">
+      <& /Elements/Submit, Label => loc('Validate lifecycle'), Name => 'ValidateLifecycle', id => 'ValidateLifecycle' &>
+    </div>
+    <div class="col-md-6">
+      <& /Elements/Submit, Label => loc('Update lifecycle'), Name => 'UpdateLifecycle', id => 'UpdateLifecycle' &>
+    </div>
+  </div>
+  <div class="form-row">
+    <div class="col-md-auto">
+      <div class="alert alert-danger" style="display:none">JSON structure invalid</div>
+    </div>
+  </div>
+
   </div>
 </form>
+
 <%INIT>
 my ($title, @results);
 my $LifecycleObj = RT::Lifecycle->new();
 $LifecycleObj->Load(Name => $Name, Type => $Type);
 
 Abort("Invalid lifecycle") unless $LifecycleObj->Name
-                               && $LifecycleObj->{data}{type} eq $Type;
+                                && $LifecycleObj->{data}{type} eq $Type;
 
 $title = loc("Modify lifecycle [_1]", $LifecycleObj->Name);
 
-if ($Config) {
-    my $LifecycleConfiguration = JSON::from_json($LifecycleConfiguration);
-    my ($ok, $msg) = RT::Lifecycle->UpdateLifecycle(
-        CurrentUser    => $session{CurrentUser},
-        LifecycleObj   => $LifecycleObj,
-        NewConfig      => JSON::from_json($Config),
-        Configuration  => $LifecycleConfiguration,
-    );
-    if ( $ok ) {
-      push @results, "Lifecycle updated";
-    }
-    else {
-      push @results, "An error occured when attempting to update lifecycle, see RT log for more info.";
+my $config = RT->Config->Get('Lifecycles')->{$LifecycleObj->Name};
+
+if ( $UpdateLifecycle or $ValidateLifecycle ) {
+    my @warnings = $LifecycleObj->ValidateLifecycle( JSON::from_json($Config) );
+    push @results, @warnings;
+    push @results, loc("Lifecycle is valid") if scalar @warnings eq 0;
+
+    $config = JSON::from_json($Config);
+
+    if ( $UpdateLifecycle && $Config ) {
+        my ($ok, $msg) = RT::Lifecycle->UpdateLifecycle(
+            CurrentUser    => $session{CurrentUser},
+            LifecycleObj   => $LifecycleObj,
+            NewConfig      => JSON::from_json($Config),
+        );
+        if ( $ok ) {
+            push @results, "Lifecycle updated";
+        }
+        else {
+            push @results, "An error occured when attempting to update lifecycle, see RT log for more info.";
+        }
+        # This code does automatic redirection if any updates happen.
+        MaybeRedirectForResults(
+            Actions   => \@results,
+            Arguments => { Name => $LifecycleObj->Name, Type => $LifecycleObj->Type },
+        );
     }
 }
-
-# This code does automatic redirection if any updates happen.
-MaybeRedirectForResults(
-    Actions   => \@results,
-    Arguments => { Name => $LifecycleObj->Name, Type => $LifecycleObj->Type },
-);
 </%INIT>
 <%ARGS>
 $Name                   => undef
 $Type                   => undef
 $Config                 => undef
-$LifecycleConfiguration => undef
+$ValidateLifecycle      => undef
+$UpdateLifecycle        => undef
 </%ARGS>
diff --git a/share/html/Admin/Lifecycles/Modify.html b/share/html/Admin/Lifecycles/Modify.html
index 5483bcc511..32b68fe66d 100644
--- a/share/html/Admin/Lifecycles/Modify.html
+++ b/share/html/Admin/Lifecycles/Modify.html
@@ -54,7 +54,7 @@
 <form action="<%RT->Config->Get('WebPath')%>/Admin/Lifecycles/Modify.html" name="ModifyLifecycle" method="post" enctype="multipart/form-data">
   <input type="hidden" class="hidden" name="Name" value="<% $LifecycleObj->Name %>" />
   <input type="hidden" class="hidden" name="Type" value="<% $LifecycleObj->Type %>" />
-  <& /Elements/Lifecycle/Graph, Lifecycle => $LifecycleObj->Name &>
+  <& /Elements/Lifecycle/Graph, LifecycleName => $LifecycleObj->Name &>
   <div class="col-md-12">
     <& /Elements/Submit, Label => loc('Save Changes') &>
   </div>
diff --git a/share/html/Admin/Lifecycles/Modify.html b/share/html/Admin/Lifecycles/Rights.html
similarity index 78%
copy from share/html/Admin/Lifecycles/Modify.html
copy to share/html/Admin/Lifecycles/Rights.html
index 5483bcc511..f587b115ec 100644
--- a/share/html/Admin/Lifecycles/Modify.html
+++ b/share/html/Admin/Lifecycles/Rights.html
@@ -49,25 +49,15 @@
 <& /Elements/Tabs &>
 <& /Elements/ListActions, actions => \@results &>
 
-<script type="text/javascript" src="<%RT->Config->Get('WebPath')%>/static/js/farbtastic.js"></script>
-
-<form action="<%RT->Config->Get('WebPath')%>/Admin/Lifecycles/Modify.html" name="ModifyLifecycle" method="post" enctype="multipart/form-data">
-  <input type="hidden" class="hidden" name="Name" value="<% $LifecycleObj->Name %>" />
-  <input type="hidden" class="hidden" name="Type" value="<% $LifecycleObj->Type %>" />
-  <& /Elements/Lifecycle/Graph, Lifecycle => $LifecycleObj->Name &>
-  <div class="col-md-12">
-    <& /Elements/Submit, Label => loc('Save Changes') &>
-  </div>
-</form>
 <%INIT>
 my ($title, @results);
 my $LifecycleObj = RT::Lifecycle->new();
 $LifecycleObj->Load(Name => $Name, Type => $Type);
 
 Abort("Invalid lifecycle") unless $LifecycleObj->Name
-                               && $LifecycleObj->{data}{type} eq $Type;
+                                && $LifecycleObj->{data}{type} eq $Type;
 
-$title = loc("Modify lifecycle [_1]", $LifecycleObj->Name);
+$title = loc("Rights for lifecycle [_1]", $LifecycleObj->Name);
 
 if ($Config) {
     my $LifecycleConfiguration = JSON::from_json($LifecycleConfiguration);
@@ -78,10 +68,10 @@ if ($Config) {
         Configuration  => $LifecycleConfiguration,
     );
     if ( $ok ) {
-      push @results, "Lifecycle updated";
+        push @results, "Lifecycle updated";
     }
     else {
-      push @results, "An error occured when attempting to update lifecycle, see RT log for more info.";
+        push @results, "An error occured when attempting to update lifecycle, see RT log for more info.";
     }
 }
 
@@ -97,3 +87,4 @@ $Type                   => undef
 $Config                 => undef
 $LifecycleConfiguration => undef
 </%ARGS>
+    
\ No newline at end of file
diff --git a/share/html/Elements/Lifecycle/Graph b/share/html/Elements/Lifecycle/Graph
index 25f5e8d863..7bbce0e622 100644
--- a/share/html/Elements/Lifecycle/Graph
+++ b/share/html/Elements/Lifecycle/Graph
@@ -84,7 +84,7 @@
     jQuery(function () {
       var container = document.getElementById('lifecycle-<% $id %>'),
         config         = <% JSON($config) |n %>,
-        name           = <% $Lifecycle | j%>,
+        name           = <% $LifecycleName | j%>,
         configuration  = <% $configuration |n %>;
 
         var editor = new RT.NewEditor( container, config, configuration );
@@ -122,21 +122,17 @@
 </div>
 
 <%INIT>
-$Lifecycle ||= $Ticket->Lifecycle
-    if $Ticket;
-
-my $config = RT->Config->Get('Lifecycles')->{$Lifecycle};
-Abort("Invalid Lifecycle") if !$Lifecycle || !$config;
+my $config = RT->Config->Get('Lifecycles')->{$LifecycleName};
+Abort("Invalid Lifecycle") if !$LifecycleName || !$config;
 
 my $configurations = RT::Configurations->new( RT->SystemUser );
-$configurations->Limit( FIELD => 'Name', VALUE => "LifecycleConfiguration-$Lifecycle" );
+$configurations->Limit( FIELD => 'Name', VALUE => "LifecycleConfiguration-$LifecycleName" );
 my $configuration = $configurations->First;
 $configuration = $configuration ? JSON($configuration->_DeserializeContent($configuration->Content)) : "{}";
 
-my $id = $Lifecycle . '-' . int(rand(2**31));
+my $id = $LifecycleName . '-' . int(rand(2**31));
 </%INIT>
 
 <%ARGS>
-$Lifecycle => undef
-$Ticket    => undef
+$LifecycleName => undef
 </%ARGS>
diff --git a/share/static/js/lifecycleui-editor.js b/share/static/js/lifecycleui-editor.js
index 7504cb1964..ecd72f8c09 100644
--- a/share/static/js/lifecycleui-editor.js
+++ b/share/static/js/lifecycleui-editor.js
@@ -596,3 +596,37 @@ jQuery( document ).ready(function () {
         }
     }
 });
+
+if ( window.location.href.indexOf('/Admin/Lifecycles/Advanced') ) {
+    jQuery( document ).ready(function () {
+        function IsValidJSONString(str) {
+            try {
+                JSON.parse(str);
+            } catch (e) {
+                return false;
+            }
+            return true;
+        }
+
+        jQuery('#Config').bind('input propertychange', function() {
+            if ( IsValidJSONString(jQuery('#Config').val()) ) {
+                jQuery('#UpdateLifecycle').find(".button").each(function() {
+                    this.disabled = '';
+                });
+                jQuery('#ValidateLifecycle').find(".button").each(function() {
+                    this.disabled = '';
+                });
+                jQuery('.alert').hide();
+            }
+            else {
+                jQuery('#UpdateLifecycle').find(".button").each(function() {
+                    this.disabled = 'disabled';
+                });
+                jQuery('#ValidateLifecycle').find(".button").each(function() {
+                    this.disabled = 'disabled';
+                });
+                jQuery('.alert').show();
+            }
+        });
+    });
+}
\ No newline at end of file

commit d84b4532033869fe438fcd628220c9bedd472723
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Mon Feb 3 17:30:17 2020 -0500

    Add Rights page for lifecycle UI

diff --git a/share/html/Admin/Lifecycles/Rights.html b/share/html/Admin/Lifecycles/Rights.html
index f587b115ec..b45671e4b4 100644
--- a/share/html/Admin/Lifecycles/Rights.html
+++ b/share/html/Admin/Lifecycles/Rights.html
@@ -49,6 +49,48 @@
 <& /Elements/Tabs &>
 <& /Elements/ListActions, actions => \@results &>
 
+<form action="<%RT->Config->Get('WebPath')%>/Admin/Lifecycles/Rights.html" name="ModifyLifecycleRights" method="post" enctype="multipart/form-data">
+  <input type="hidden" class="hidden" name="Name" value="<% $LifecycleObj->Name %>" />
+  <input type="hidden" class="hidden" name="Type" value="<% $LifecycleObj->Type %>" />
+  <h1><&|/l&>Lifecycles</&></h1>
+
+  <table class="table table-sm collection collection-as-table" cellspacing="0">
+    <tbody>
+      <tr class="collection-as-table">
+        <th class="collection-as-table">From Status</th>
+        <th class="collection-as-table">To Status</th>
+        <th class="collection-as-table">Right</th>
+      </tr>
+    </tbody>
+    <tbody class="list-item">
+% my $i = 1;
+% foreach my $right ( sort @rights ) {
+      <tr>
+        <td class="collection-as-table">
+          <& /Elements/SelectStatus, Statuses => \@statuses, Default => $right->{'To'}, Name => "Right-To-$i" &>
+        </td>
+        <td class="collection-as-table">
+          <& /Elements/SelectStatus, Statuses => \@statuses, Default => $right->{'From'}, Name => "Right-From-$i" &>
+        </td>
+        <td class="collection-as-table">
+          <input type="text" value="<% $right->{'Name'} %>" class="form-control" Name="Right-Name-<%$i%>" />
+        </td>
+      </tr>
+% ++$i;
+% }
+      <tr>
+        <td class="collection-as-table"><& /Elements/SelectStatus, Statuses => \@statuses, Name => 'Right-To-0' &></td>
+        <td class="collection-as-table"><& /Elements/SelectStatus, Statuses => \@statuses, Name => 'Right-From-0' &></td>
+        <td class="collection-as-table"><input type="text" class="form-control" name='Right-Name-0' /></td>
+      </tr>
+    </tbody>
+  </table>
+
+  <div class="col-md-12">
+    <& /Elements/Submit, Label => loc('Update lifecycle rights'), Name => 'UpdateLifecycleRights', id => 'UpdateLifecycleRights' &>
+  </div>
+</form>
+
 <%INIT>
 my ($title, @results);
 my $LifecycleObj = RT::Lifecycle->new();
@@ -59,13 +101,43 @@ Abort("Invalid lifecycle") unless $LifecycleObj->Name
 
 $title = loc("Rights for lifecycle [_1]", $LifecycleObj->Name);
 
-if ($Config) {
-    my $LifecycleConfiguration = JSON::from_json($LifecycleConfiguration);
+my @statuses = $LifecycleObj->Statuses;
+push @statuses, '*';
+
+my $rights = $LifecycleObj->Rights || ();
+
+my @rights;
+foreach my $key ( keys %{$rights} ) {
+    my ($to, $from) = $key =~ /(.+) \-\> (.+)/;
+    push @rights, { To => $to, From => $from, Name => $rights->{$key} };
+}
+
+if ($UpdateLifecycleRights) {
+    my %new_rights;
+    foreach my $arg ( keys %ARGS ) {
+        my ($field, $count) = $arg =~ /Right-(\w+)-(\d+)/;
+        next unless $field and defined $count && $ARGS{$arg};
+
+        if ( $new_rights{$count} ) {
+            $new_rights{$count}->{$field} = $ARGS{$arg};
+        }
+        else {
+            $new_rights{$count} = {};
+            $new_rights{$count}->{$field} = $ARGS{$arg};
+        }
+    }
+    my $new_rights;
+    # Convert to RT internal format
+    foreach my $right ( keys %new_rights ) {
+        $new_rights->{"$new_rights{$right}->{To} -> $new_rights{$right}->{From}"} = $new_rights{$right}->{Name};
+    }
+    my $config = RT->Config->Get('Lifecycles')->{$LifecycleObj->Name};
+    $config->{'rights'} = $new_rights;
+
     my ($ok, $msg) = RT::Lifecycle->UpdateLifecycle(
         CurrentUser    => $session{CurrentUser},
         LifecycleObj   => $LifecycleObj,
-        NewConfig      => JSON::from_json($Config),
-        Configuration  => $LifecycleConfiguration,
+        NewConfig      => $config,
     );
     if ( $ok ) {
         push @results, "Lifecycle updated";
@@ -84,7 +156,6 @@ MaybeRedirectForResults(
 <%ARGS>
 $Name                   => undef
 $Type                   => undef
-$Config                 => undef
-$LifecycleConfiguration => undef
+$UpdateLifecycleRights  => undef
 </%ARGS>
     
\ No newline at end of file

commit 7c07622c6fe81c7dc846c50e26b3bd6797a8bfff
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Mon Feb 3 17:30:31 2020 -0500

    Add Actions page for lifecycle UI

diff --git a/share/html/Admin/Lifecycles/Actions.html b/share/html/Admin/Lifecycles/Actions.html
index f587b115ec..d1bec15702 100644
--- a/share/html/Admin/Lifecycles/Actions.html
+++ b/share/html/Admin/Lifecycles/Actions.html
@@ -49,6 +49,63 @@
 <& /Elements/Tabs &>
 <& /Elements/ListActions, actions => \@results &>
 
+<form action="<%RT->Config->Get('WebPath')%>/Admin/Lifecycles/Actions.html" name="ModifyLifecycleRights" method="post" enctype="multipart/form-data">
+    <input type="hidden" class="hidden" name="Name" value="<% $LifecycleObj->Name %>" />
+    <input type="hidden" class="hidden" name="Type" value="<% $LifecycleObj->Type %>" />
+    <h1><&|/l&>Lifecycles</&></h1>
+
+    <table class="table table-sm collection collection-as-table" cellspacing="0">
+    <tbody>
+        <tr class="collection-as-table">
+        <th class="collection-as-table">From Status</th>
+        <th class="collection-as-table">To Status</th>
+        <th class="collection-as-table">Label</th>
+        <th class="collection-as-table">Update</th>
+        </tr>
+    </tbody>
+    <tbody class="list-item">
+% my $i = 1;
+% foreach my $action ( @{$actions} ) {
+        <tr>
+          <td class="collection-as-table">
+            <& /Elements/SelectStatus, Statuses => \@statuses, Default => $action->{'to'}, Name => "Action-To-$i" &>
+          </td>
+          <td class="collection-as-table">
+            <& /Elements/SelectStatus, Statuses => \@statuses, Default => $action->{'from'}, Name => "Action-From-$i" &>
+          </td>
+          <td class="collection-as-table">
+            <input type="text" value="<% $action->{'label'} %>" class="form-control" Name="Action-Label-<%$i%>" />
+          </td>
+          <td>
+            <select name="Action-Update-<% $i %>" class="form-control selectpicker">
+              <option <% $action->{'update'} && $action->{'update'} eq 'Respond' ? qq[selected="selected"] : '' %> value="Respond">Reply</option>
+              <option <% $action->{'update'} && $action->{'update'} eq 'Comment' ? qq[selected="selected"] : ''%> value="Comment">Comment</option>
+              <option <% !$action->{'update'} ? qq[selected='selected'] : '' %> value="">None</option>
+            </select>
+          </td>
+        </tr>
+% ++$i;
+% }
+        <tr>
+          <td class="collection-as-table"><& /Elements/SelectStatus, Statuses => \@statuses, Name => 'Action-To-0' &></td>
+          <td class="collection-as-table"><& /Elements/SelectStatus, Statuses => \@statuses, Name => 'Action-From-0' &></td>
+          <td class="collection-as-table"><input type="text" class="form-control" name='Actoin-Label-0' /></td>
+          <td>
+            <select name="Action-Update-0" class="selectpicker form-control">
+              <option value="Respond">Reply</option>
+              <option value="Comment">Comment</option>
+              <option value="None">None</option>
+            </select>
+          </td>
+        </tr>
+    </tbody>
+  </table>
+
+  <div class="col-md-12">
+    <& /Elements/Submit, Label => loc('Update lifecycle actions'), Name => 'UpdateLifecycleActions', id => 'UpdateLifecycleActions' &>
+  </div>
+</form>
+
 <%INIT>
 my ($title, @results);
 my $LifecycleObj = RT::Lifecycle->new();
@@ -57,15 +114,44 @@ $LifecycleObj->Load(Name => $Name, Type => $Type);
 Abort("Invalid lifecycle") unless $LifecycleObj->Name
                                 && $LifecycleObj->{data}{type} eq $Type;
 
-$title = loc("Rights for lifecycle [_1]", $LifecycleObj->Name);
+$title = loc("Actions for lifecycle [_1]", $LifecycleObj->Name);
+
+my @statuses = $LifecycleObj->Statuses;
+push @statuses, '*';
+my $actions = $LifecycleObj->{'data'}->{'actions'} || ();
+
+if ($UpdateLifecycleActions) {
+    my %new_actions;
+    foreach my $arg ( keys %ARGS ) {
+        my ($field, $count) = $arg =~ /Action-(\w+)-(\d+)/;
+        next unless $field and defined $count && $ARGS{$arg};
+
+        if ( $new_actions{$count} ) {
+            $new_actions{$count}->{lc $field} = $ARGS{$arg};
+        }
+        else {
+            $new_actions{$count} = {};
+            $new_actions{$count}->{lc $field} = $ARGS{$arg};
+        }
+    }
+    my @new_actions;
+    foreach my $key ( keys %new_actions ) {
+        next unless $new_actions{$_}->{from} && $new_actions{$_}->{to};
+
+        push @new_actions, "$new_actions{$key}->{from} -> $new_actions{$key}->{to}";
+        push @new_actions, {
+            label  => $new_actions{$key}->{label},
+            update => $new_actions{$key}->{update}
+        };
+    }
+
+    my $config = RT->Config->Get('Lifecycles')->{$LifecycleObj->Name};
+    $config->{'actions'} = \@new_actions;
 
-if ($Config) {
-    my $LifecycleConfiguration = JSON::from_json($LifecycleConfiguration);
     my ($ok, $msg) = RT::Lifecycle->UpdateLifecycle(
         CurrentUser    => $session{CurrentUser},
         LifecycleObj   => $LifecycleObj,
-        NewConfig      => JSON::from_json($Config),
-        Configuration  => $LifecycleConfiguration,
+        NewConfig      => $config,
     );
     if ( $ok ) {
         push @results, "Lifecycle updated";
@@ -84,7 +170,6 @@ MaybeRedirectForResults(
 <%ARGS>
 $Name                   => undef
 $Type                   => undef
-$Config                 => undef
-$LifecycleConfiguration => undef
+$UpdateLifecycleActions  => undef
 </%ARGS>
     
\ No newline at end of file

commit 7a8e1c4783d443ffebfea3611fa43d091c92652b
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Mon Feb 3 17:31:46 2020 -0500

    Add lifecycle Rights/Actions/Advanced pages to menu

diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm
index 98659d9b50..70e3017a0a 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -883,6 +883,10 @@ sub _BuildAdminMenu {
 
                 $page->child( basics => title => loc('Modify'),  path => "/Admin/Lifecycles/Modify.html?Type=" . $Type_uri . "&Name=" . $Name_uri );
                 $page->child( mappings => title => loc('Mappings'),  path => "/Admin/Lifecycles/Mappings.html?Type=" . $Type_uri . "&Name=" . $Name_uri );
+
+                $page->child( rights => title => loc('Rights'),  path => "/Admin/Lifecycles/Rights.html?Type=" . $Type_uri . "&Name=" . $Name_uri );
+                $page->child( actions => title => loc('Actions'),  path => "/Admin/Lifecycles/Actions.html?Type=" . $Type_uri . "&Name=" . $Name_uri );
+                $page->child( advanced => title => loc('Advanced'),  path => "/Admin/Lifecycles/Advanced.html?Type=" . $Type_uri . "&Name=" . $Name_uri );
             }
         }
         else {

commit 22fa7153f8db11e1c836012f9ceeef302928ffda
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Mon Feb 3 17:32:14 2020 -0500

    Add helper methods to RT::Lifecycle

diff --git a/lib/RT/Lifecycle.pm b/lib/RT/Lifecycle.pm
index 0bbdb3e566..e0bdc620dd 100644
--- a/lib/RT/Lifecycle.pm
+++ b/lib/RT/Lifecycle.pm
@@ -276,6 +276,20 @@ sub IsValid {
     return 0;
 }
 
+=head3
+
+Lists all statuses for lifecycle object self.
+
+=cut
+
+sub Statuses {
+    my $self = shift;
+
+    return @{$self->{'data'}->{''}};
+}
+
+
+
 =head3 StatusType
 
 Takes a status and returns its type, one of 'initial', 'active' or
@@ -462,6 +476,12 @@ sub CheckRight {
     return $to eq 'deleted' ? 'DeleteTicket' : 'ModifyTicket';
 }
 
+sub Rights {
+    my $self = shift;
+
+    return $self->{'data'}->{'rights'};
+}
+
 =head3 RightsDescription [TYPE]
 
 Returns hash with description of rights that are defined for
@@ -1051,7 +1071,6 @@ sub ValidateLifecycle {
         for my $status ( @statuses ) {
             push @warnings, "Nonexistant status @{[lc $status]} in transitions in ".$self->Name." lifecycle"
                 unless $lifecycle->{canonical_case}{lc $status};
-            push @{ $lifecycle->{transitions}{lc $from} },
         }
     }
     my $rights = $lifecycle->{rights} || {};

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


More information about the rt-commit mailing list