So Now what?

In my last Blog entry (First Day at the SPA) I showed how to create a simple Newsletter Signup Form with the JSON API and some AngularJs, and earned a relaxing day at the Spa, but now what?

I still want more, more relaxing days at the pool, good and healthy food, muscle relaxing shiatsu massages, and still be able the deliver robust solutions.

So how can you continue optimizing my workflow, and still have time to chill?

 

The short Answer

JSON Webservices and a little bit of HTML and Javascript, and XML(And Yes we are still on a Vanilla Liferay Installation)

 

The long Answer

To show my take on the long answer, I will continue optimizing the small example, of my last Blog entry. 

The example Task: Adapt and Optimize the Mailing List sign up (control), to reflect a more realistic Signup form.

So the basic appearance won't change, only the signup process in the background.

Lets first look at the current process

Step 1) You enter an email-address.
Step 2) Your email-address is saved.

Simple Signup Process

 

Now lets look at the "improved" process

Step 1) The user enters an email-address.
Step 2) An automatic email is send to the entered email-address
Step 3) The user has to activate the subscription, with the provided link in the email
Step 4) After activation (clicking the link), the email-address is marked as activated

New signup process

Like this achiving a "double opt in" for the Mailing List, which can save you much problems, at least in austria.

So Lets begin

I'am still using the prior coded liferay-service-provider.js, which was updated since the last post. (It can be found on Github

Step 1: Create our “database”

Since I want Liferay's Workflowengine to take care of the backendprocess, but I don't want all the created webcontents to passthrough the same Mailinglist Workflow, the data entries will now be saved as Dynamic Data List Records. Here we can specify different workflows to individual List, and so being totally independent. 
 
We create a new Data Definition, with the Fields Email and Activated. Later will not be shown it is only to hold the status of the entry.
create new DDL
 
And create a new Data Defenition List with the currently created Data Defenition. (Here we could already set the worklfow if it would be present)
create Data Defenition List
 

Step 2: Adapat the HTML/Javascript "CODING"

Although there is no change in the frontend, there will be some minor changes in the HTML / Javascript portion, to help with the activation link and the minor changes in the liferay-service-provider API.
 
<div class="name-box">
<style type="text/css">
        .name-box .span4.no-margin{margin:0;}
</style>
    <div class="span4 no-margin"  data-ng-app="newApp">
        <div class="ng-view"></div>
        <script src="/angular/angular.min.js"></script>
        <script src="/angular/angular-route.min.js"></script>
        <script src="/angular-addon/liferay-service-provider.js"></script>
        <script>
            (function(app){
                app.config(["liferayProvider","$routeProvider", function (liferayProvider,$routeProvider) {
                    liferayProvider.setToken(btoa("newsletterservice:newsletterservice"));
                    liferayProvider.addTypes({
                        name:"Newsletter",
                          type: TYPES.DDLRECORD,
                          groupId: 24913,
                          recordSetId: 25719
                    });
                    $routeProvider
                        .when("/", {
                              template: "<form class='form-search' id='nameBox'>" +
                                        "<div class='input-append'>" +
                                        "<input class='search-query' data-ng-model='Email' placeholder='Enter Name ... ' type='text' />" +
                                        "<button class='btn' data-ng-click='saveName()' type='button'><i class='icon-envelope'>&nbsp;</i>Add Name</button>" +
                                        "</div></form>",
                            controller: "baseController"
                        })
                        .when("/success", {
                            template: "<div class='alert alert-success'><b>Success!</b> Your name is now entered in our mailingslist!</div>"
                          })
                          .when("/failure", {
                             template: "<div class='alert alert-error '><b>Error!</b> Please try it again <a href='#/' > go back </a>!</div>"
                          })
                          .when("/activate/:id",{
                              template: "<div class='alert alert-info'>{{message}}</div>",
                              controller: "activationController"
                          }).otherwise("/");
                }]).controller("activationController", ["$scope", "$routeParams", "liferay", function($scope, $routeParams, liferay){
                    liferay["Newsletter"].update({Activated:true}, $routeParams.id).then(function(){
                        $scope.message = "Email activated!";
                    }).catch(function(){
                        $scope.message = "Error!";
                    });
             }]).controller("baseController", ["$scope", "$location", "liferay", function($scope, $location, liferay){
                  $scope.saveName = function(){
                       liferay["Newsletter"].create({Email:$scope.Email}).then(function(){
                            $location.url("/success");
                        }).catch(function(){
                           $location.url("/failure");
                       });
                  };
            }]);
        }(angular.module("newApp",["LiferayService", "ngRoute"])));
        </script>
    </div>
</div><div class="name-box">
<style type="text/css">
        .name-box .span4.no-margin{margin:0;}
</style>
    <div class="span4 no-margin"  data-ng-app="newApp">
        <div class="ng-view"></div>
        <script src="/angular/angular.min.js"></script>
        <script src="/angular/angular-route.min.js"></script>
        <script src="/angular-addon/liferay-service-provider.js"></script>
        <script>
            (function(app){
                app.config(["liferayProvider","$routeProvider", function (liferayProvider,$routeProvider) {
                    liferayProvider.setToken(btoa("newsletterservice:newsletterservice"));
                    liferayProvider.addTypes({
                        name:"Newsletter",
                          type: TYPES.DDLRECORD,
                          groupId: 24913,
                          recordSetId: 25719
                    });
                    $routeProvider
                        .when("/", {
                              template: "<form class='form-search' id='nameBox'>" +
                                        "<div class='input-append'>" +
                                        "<input class='search-query' data-ng-model='Email' placeholder='Enter Name ... ' type='text' />" +
                                        "<button class='btn' data-ng-click='saveName()' type='button'><i class='icon-envelope'>&nbsp;</i>Add Name</button>" +
                                        "</div></form>",
                            controller: "baseController"
                        })
                        .when("/success", {
                            template: "<div class='alert alert-success'><b>Success!</b> Your name is now entered in our mailingslist!</div>"
                          })
                          .when("/failure", {
                              template: "<div class='alert alert-error '><b>Error!</b> Please try it again <a href='#/' > go back </a>!</div>"
                          })
                          .when("/activate/:id",{
                              template: "<div class='alert alert-info'>{{message}}</div>",
                              controller: "activationController"
                          }).otherwise("/");
                }]).controller("activationController", ["$scope", "$routeParams", "liferay", function($scope, $routeParams, liferay){
                    liferay["Newsletter"].update({Activated:true}, $routeParams.id).then(function(){
                        $scope.message = "Email activated!";
                    }).catch(function(){
                        $scope.message = "Error!";
                    });
             }]).controller("baseController", ["$scope", "$location", "liferay", function($scope, $location, liferay){
                  $scope.saveName = function(){
                       liferay["Newsletter"].create({Email:$scope.Email}).then(function(){
                            $location.url("/success");
                        }).catch(function(){
                           $location.url("/failure");
                       });
                  };
            }]);
        }(angular.module("newApp",["LiferayService", "ngRoute"])));
        </script>
    </div>
</div>
 
Due to the fact the the SPA has now to react to some urls, I added ngRoute Module and updated the Code to use the benefits of this module. (the activation link logic, could have been coded with 2-3 lines of vanilla Javascript code, but updating the code to benefite of ngRoute module makes it a real SPA, and it maintainable)
Also since the liferay-service-provider.js should use DDRecord instead of WebContent, some minor changes in the config were needed, to set it up.
 

Step 3: Create a workflow XML file that reflects the needed/wanted process/flow

I must say the Liferay Workflowengine is pretty neat, and has some nice features. In my opinion it could improve documentation wise, but I cant be that bad, if I could wipout this xml in a few hours.

<?xml version="1.0"?>
<workflow-definition 
    xmlns="urn:liferay.com:liferay-workflow_6.2.0" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="urn:liferay.com:liferay-workflow_6.2.0 http://www.liferay.com/dtd/liferay-workflow-definition_6_2_0.xsd">
    <name>Newsletter process</name>
    <description>Newsletter</description>
    <version>1</version>
    <state>
      <name>created</name>
      <metadata></metadata>
      <actions>
        <action>
          <name>start</name>
          <script><![CDATA[ 
            var _log = Packages.com.liferay.portal.kernel.log.LogFactoryUtil.getLog("WORKFLOW"); 
            _log.error("START"); 
            var email = ""; 
            var id = 1; 
            try{ 
                var idOffset = 3; 
                id = workflowContext.get("entryClassPK") - idOffset; 
                var record = Packages.com.liferay.portlet.dynamicdatalists.service.DDLRecordLocalServiceUtil.getRecord(id); 
                email = record.getFieldValue("Email"); 
           } catch(e) { 
              _log.error(e); 
           } 
           Packages.com.liferay.util.mail.MailEngine.send(
             Packages.javax.mail.internet.InternetAddress("test@liferay.com"),
             Packages.javax.mail.internet.InternetAddress(email),
             "Activation Email",
             "<html><body><a href='http://liferay:8080/web/spa-tests/main-page#/activate/" + id + "'>activation</a></body></html>",
             true
          );
        ]]>
        </script>
        <script-language>javascript</script-language>
        <execution-type>onEntry</execution-type>
      </action>
    </actions>
    <initial>true</initial>
    <transitions>
      <transition>
        <name>timer</name>
        <target>timer</target>
      </transition>
    </transitions>
  </state>
  <task>
    <name>timer</name>
    <assignments>
      <user/>
    </assignments>
    <task-timers>
      <task-timer>
        <name>Timer</name>
        <description>1</description>
        <delay>
          <duration>1</duration>
          <scale>minute</scale>
        </delay>
        <recurrence>
          <duration>1.0</duration>
          <scale>minute</scale>
        </recurrence>
        <timer-actions>
          <timer-action>
            <name>approve</name>
            <script>
            <![CDATA[
              var idOffset = 3;
              var id = workflowContext.get("entryClassPK") - idOffset;
              var record = Packages.com.liferay.portlet.dynamicdatalists.service.DDLRecordLocalServiceUtil.getRecord(id);
              if(record.getFieldValue("Activated") == true){
                var companyId = workflowContext.get("companyId");
                var workflowInstanceLink = Packages.com.liferay.portal.service.WorkflowInstanceLinkLocalServiceUtil.getWorkflowInstanceLink(
                  companyId,
                  workflowContext.get("groupId"),
                  workflowContext.get("entryClassName"),
                  workflowContext.get("entryClassPK")
                );
               var workflowInstanceId = workflowInstanceLink.getWorkflowInstanceId();
               Packages.com.liferay.portal.kernel.workflow.WorkflowInstanceManagerUtil.signalWorkflowInstance(
                 companyId, 
                 workflowContext.get("userId"), 
                 workflowInstanceId, 
                 "end", 
                 workflowContext
               );
             }
           ]]>
           </script>
           <script-language>javascript</script-language>
           <priority>1</priority>                       
         </timer-action>
       </timer-actions>
     </task-timer>
   </task-timers>
   <transitions>
     <transition>
       <name>end</name>
       <target>end</target>
       <default>true</default>
     </transition>
   </transitions>
 </task>
<state>
  <name>end</name>
  <actions>
    <action>
      <name>Approve</name>
      <description>Approve</description>
      <script>
        <![CDATA[
          Packages.com.liferay.portal.kernel.workflow.WorkflowStatusManagerUtil.updateStatus(                      Packages.com.liferay.portal.kernel.workflow.WorkflowConstants.toStatus("approved"),                      workflowContext                  );
        ]]>
      </script>
      <script-language>javascript</script-language>
      <execution-type>onEntry</execution-type>
   </action>
 </actions>
</state>
</workflow-definition>
 
Basically what this code does workflow diagramm

1) On entering the intial State, an email is sent, with the activiation link
2) Now the Engine is checking periodically (1 minute interval), if the email-address was activated, through the link
3) If the entry was activated, the workflow will terminate. (this is a great place to an some code, that sends a welcome email, or so)

Now, just upload the workflow

Upload Workflow definition

and finally add it to the prior created DDL List

Dynamic Data Lists add Workflow

Last Step

Lean back and enjoy the time you have saved, and will save in the future, if you take advantage of the rich toolset that Liferay provides out-of-the-box.

Please share any Ideas, possible Improvements, Comments, Feedback and/or nice Spa locations in/near austria. I would like to hear from you.

 

Obviously there are still some improvements, since it is merely a proof of concept, but this I leave for you to explore.
Blogues
I will probably extract most of the code out of the article, since it got a bit long. :-S
Hi Charles,

What's about angular 2 with Liferay?
Hello Viktor,
Angular2 is probally the future and a good Idea, I just wanted to keep project setup and the code simple. Without transpiler, bundlers, and other stuff. (I coded this all in Notepad++ and in the ckeditor of liferay), but I will try to make a short post, with an updated angular 2 liferay-provider-service.js. Thanks for your feedback.
Hi Charles,

I am working with Liferay also. Could you provide an information how do you plan to use angular2 with Liferay?
Hello Viktor,
Sorry for my late response. I must say I'm not an expert Java developer and my blog / Ideas, are so that administrators, can leverage the builtin functionality, to not need to rely on the developer. So at present I plan to use angular 2, similar as in my angular 1 Blogposts. I am updating the services an trying to optimize the code.
But I just would use it for small (adhoc) needs that are short lived, anything else should be done in my opinion with portlets and/or theme depends on how heavy angular is used on the website.
What do you think about this? Does it make sense?

btw.: I will try to get my angular2 example out this weekend.