How to build a Three Step Document Workflow using the SCXML engine
Kenan Salic
2017-11-03
A multi-step document workflow is a feature highly requested by partners and customers, especially from those with a large group of content editors with designated roles. And ever since the introduction of SCXML and the release of Hippo 10 the possibility of this feature has become a reality.
So, what did I build here? It is a Three Step Document Workflow also known as a Review Document Workflow. Historically when working in Hippo CMS, there were two specific content editing roles; the Author and the Editor. In essence, the Author creates a document and an Editor approves the document before going live.
For some customers, there’s a step missing right in the middle and that’s the Reviewer’s role. The Reviewer should check and approve the document created by the author before it is delivered to the editor who eventually decides if the document will be published.
To the drawing table
With an excessive amount of documentation about workflow, SCXML, repository authorization, toolbar plugins and more, you will need to pick a place to start. Best practices is to start by creating the role, group and user who will become the Reviewer.
Step 1: Setting up Authorization and Permissions.
For this step, I will refer to the documentation on Repository Authorization and Permissions. Simply create the Reviewer role, group and user by importing following XML snippet to the root node of your repository:
<?xml version="1.0" encoding="UTF-8"?>
<sv:node sv:name="hippo:configuration" xmlns:esv="http://www.onehippo.org/jcr/xmlimport" esv:merge="combine" xmlns:sv="http://www.jcp.org/jcr/sv/1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.onehippo.org/jcr/xmlimport ">
<sv:property sv:name="jcr:primaryType" sv:type="Name">
<sv:value>hipposys:configuration</sv:value>
</sv:property>
<sv:node sv:name="hippo:roles" esv:merge="combine">
<sv:node sv:name="reviewer">
<sv:property sv:name="jcr:primaryType" sv:type="Name">
<sv:value>hipposys:role</sv:value>
</sv:property>
<sv:property sv:name="hipposys:privileges" sv:type="String" sv:multiple="true">
<sv:value>jcr:read</sv:value>
<sv:value>jcr:write</sv:value>
<sv:value>reviewer</sv:value>
</sv:property>
<sv:property sv:name="hipposys:roles" sv:type="String" sv:multiple="true">
<sv:value>author</sv:value>
</sv:property>
</sv:node>
</sv:node>
<sv:node sv:name="hippo:groups" esv:merge="combine">
<sv:node sv:name="reviewer">
<sv:property sv:name="jcr:primaryType" sv:type="Name">
<sv:value>hipposys:group</sv:value>
</sv:property>
<sv:property sv:name="hipposys:description" sv:type="String">
<sv:value>Reviewer</sv:value>
</sv:property>
<sv:property sv:name="hipposys:members" sv:type="String" sv:multiple="true">
<sv:value>reviewer</sv:value>
</sv:property>
<sv:property sv:name="hipposys:securityprovider" sv:type="String">
<sv:value>internal</sv:value>
</sv:property>
</sv:node>
</sv:node>
<sv:node sv:name="hippo:users" esv:merge="combine">
<sv:node sv:name="reviewer">
<sv:property sv:name="jcr:primaryType" sv:type="Name">
<sv:value>hipposys:user</sv:value>
</sv:property>
<sv:property sv:name="hipposys:active" sv:type="Boolean">
<sv:value>true</sv:value>
</sv:property>
<sv:property sv:name="hipposys:password" sv:type="String">
<sv:value>$SHA-1$EeGZWlhAlqg=$ZwI3GozxkhdFF84uXqjVBa1d6aA=</sv:value>
</sv:property>
<sv:property sv:name="hipposys:securityprovider" sv:type="String">
<sv:value>internal</sv:value>
</sv:property>
</sv:node>
</sv:node>
</sv:node>
In the existing Hippo Document, Request, Folder (etc) domains will need to be added to the newly created reviewer group to distinguish the Author, Editor and Reviewer roles. You’ll do this by importing the following XML snippet:
<?xml version="1.0" encoding="UTF-8"?>
<sv:node xmlns:sv="http://www.jcp.org/jcr/sv/1.0" xmlns:esv="http://www.onehippo.org/jcr/xmlimport" sv:name="hippo:domains" esv:merge="combine">
<sv:node sv:name="hippofolders" esv:merge="combine">
<sv:node sv:name="hippo:authrole" esv:merge="combine">
<sv:property sv:name="hipposys:groups" sv:type="String" esv:merge="override" sv:multiple="true">
<sv:value>author</sv:value>
<sv:value>editor</sv:value>
<sv:value>reviewer</sv:value>
</sv:property>
</sv:node>
</sv:node>
<sv:node sv:name="hippodocuments" esv:merge="combine">
<sv:node sv:name="hippo:authrole">
<sv:property sv:name="jcr:primaryType" sv:type="Name">
<sv:value>hipposys:authrole</sv:value>
</sv:property>
<sv:property sv:name="hipposys:groups" sv:type="String" sv:multiple="true">
<sv:value>reviewer</sv:value>
</sv:property>
<sv:property sv:name="hipposys:role" sv:type="String">
<sv:value>reviewer</sv:value>
</sv:property>
</sv:node>
</sv:node>
<sv:node sv:name="hipporequests" esv:merge="combine">
<sv:node sv:name="hippo:authrole">
<sv:property sv:name="jcr:primaryType" sv:type="Name">
<sv:value>hipposys:authrole</sv:value>
</sv:property>
<sv:property sv:name="hipposys:groups" sv:type="String" sv:multiple="true">
<sv:value>author</sv:value>
</sv:property>
<sv:property sv:name="hipposys:role" sv:type="String">
<sv:value>author</sv:value>
</sv:property>
</sv:node>
<sv:node sv:name="hippo:authrole[2]" esv:merge="combine">
<sv:property sv:name="hipposys:role" sv:type="String" esv:merge="override">
<sv:value>reviewer</sv:value>
</sv:property>
<sv:property sv:name="hipposys:groups" sv:type="String" esv:merge="override" sv:multiple="true">
<sv:value>reviewer</sv:value>
</sv:property>
</sv:node>
</sv:node>
<sv:node sv:name="defaultwrite" esv:merge="combine">
<sv:node sv:name="hippo:authrole" esv:merge="combine">
<sv:property sv:name="hipposys:groups" sv:type="String" esv:merge="override" sv:multiple="true">
<sv:value>author</sv:value>
<sv:value>editor</sv:value>
<sv:value>admin</sv:value>
<sv:value>reviewer</sv:value>
</sv:property>
</sv:node>
</sv:node>
</sv:node>
At the end of this step, you have the test user which will eventually be able to review documents. But there are a few more steps before we get there!
Verify the user by logging in with the following credentials: reviewer/reviewer
Step 2: Thinking of a workable model
Before reading the following step, it’s good to have an understanding of the current publication workflow and Hippo requests. For this step, some research is necessary to solve the existing publication request model. Let’s take a look at the hippostdpubwf.cnd snippet for the node type definition of the current publication request model:
[hippostdpubwf:request] > hippo:request
- hippostdpubwf:type (String) < 'rejected', 'publish', 'depublish', 'scheduledpublish', 'scheduleddepublish', 'delete', 'collection'
- hippostdpubwf:reason (String)
- hippostdpubwf:reqdate (Date)
- hippostdpubwf:username (String)
- hippostdpubwf:document (Reference)
- hippostdpubwf:refId (String)
In this model, you need to have a type or state which is ‘review’. However, the node type definition of hippostdpubwf:request is strict on a set of states. To continue, you’ll need to extend the hippostdpubwf:request and add our own property to the node type definition.
This is the result:
[threestep:request] > hippostdpubwf:request
- threestep:type (string) mandatory < 'review', 'publish'
Pretty simple so far! Make sure you insert this Node Type Definition in the project.
Alright, so far you have the role (reviewer) and the request model (cnd), and the next steps are about glueing these together.
Step 3: Custom Workflow
The next step is to create a Custom Workflow for the author and reviewer role. You will create the ReviewWorkflow interface first.
You can cover several use cases with this interface:
Use case 1: As an author, I should be able to Request a Review.
Use case 2: As a reviewer, I should be able to Accept a request for Review.
Use case 3: As a reviewer, I should be able to Reject a request for Review.
This can be easily translated to an Interface:
public interface ReviewWorkflow extends Workflow {
/**
* Request document review for the reviewer role before publication.
* @throws WorkflowException
*/
void requestReview() throws WorkflowException;
/**
* Accept a review request.
* @param requestIdentifier
* @throws WorkflowException
*/
void acceptReview(String requestIdentifier) throws WorkflowException;
/**
* Reject a review request plus give reason to author why.
* @param requestIdentifier
* @param reason
* @throws WorkflowException
*/
void rejectReview(String requestIdentifier, String reason) throws WorkflowException;
}
Let’s continue with the implementation of this interface and the glue necessary for the SCXML engine.
Step 4: SCXML Engine and the Custom Workflow
In the previous step, you’ve created an interface for the ReviewWorkflow. Now you need to create an implementation of the interface and a way to trigger the SCXML executer engine when executing these methods.
In the following example you can implement the ReviewWorkflow:
public class DocumentReviewWorkflowImpl extends DocumentWorkflowImpl implements ReviewWorkflow {
public DocumentReviewWorkflowImpl() throws RemoteException {
}
@Override
public void requestReview() throws WorkflowException {
getWorkflowExecutor().start();
getWorkflowExecutor().triggerAction("requestReview");
}
}
Don’t worry about the SCXML specific code for now. I will cover it later on in the tutorial.
Step 5: Workflow Toolbar Plugin (UI/Frontend)
Luckily, one of my very smart co-workers already created a wonderful and elaborate blog about this step, and, once you read his explanation, adding a workflow to the toolbar will be simple.
For this step, you’ll need to do some UI work to support the Review Workflow. You’ll need to add a new workflow menu which displays the buttons:
-
Request Review
-
Accept Review
-
Reject Review
You start by creating an extension of the AbstractDocumentWorkflowPlugin. This is a helper class creating a Workflow (toolbar) Plugin. In this case, you won’t need to worry too much about Apache Wicket. It does a lot of magic in the background for you! In the example below I’ll name it ReviewRequestWorkflowPlugin
public class ReviewRequestsWorkflowPlugin extends AbstractDocumentWorkflowPlugin {}
In the ReviewRequestWorkflowPlugin you’ll need to add the 3 buttons: Request Review, Accept Review, Reject Review. You can do that by adding the following snippet in the constructor of the ReviewRequestWorkflowPlugin:
public ReviewRequestsWorkflowPlugin(IPluginContext context, IPluginConfig config) {
super(context, config);
final StdWorkflow requestReview;
add(requestReview = new StdWorkflow("requestReview", new StringResourceModel("request-review", this, null), context, getModel()) {
@Override
public String getSubMenu() {
return new StringResourceModel("review", ReviewRequestsWorkflowPlugin.this, null, null).getString();
}
@Override
protected Component getIcon(final String id) {
return HippoIcon.fromSprite(id, Icon.CHECK_CIRCLE);
}
@Override
protected IDialogService.Dialog createRequestDialog() {
...
}
@Override
protected String execute(Workflow wf) throws Exception {
ReviewWorkflow workflow = (ReviewWorkflow) wf;
workflow.requestReview();
return null;
}
});
final StdWorkflow acceptReview;
add(acceptReview = new StdWorkflow("acceptReview", new StringResourceModel("accept-review", this, null), context, getModel()) {
@Override
public String getSubMenu() {
return new StringResourceModel("review", ReviewRequestsWorkflowPlugin.this, null, null).getString();
}
@Override
protected Component getIcon(final String id) {
return HippoIcon.fromSprite(id, Icon.CHECK_CIRCLE);
}
@Override
protected String execute(Workflow wf) throws Exception {
final String idFromRequestKey = getIdFromRequestKey("acceptReview");
ReviewWorkflow workflow = (ReviewWorkflow) wf;
workflow.acceptReview(idFromRequestKey);
return null;
}
});
final StdWorkflow rejectReview;
add(rejectReview = new StdWorkflow("rejectReview", new StringResourceModel("reject-review", this, null), context, getModel()) {
public String reason;
@Override
public String getSubMenu() {
return new StringResourceModel("review", ReviewRequestsWorkflowPlugin.this, null, null).getString();
}
@Override
protected Component getIcon(final String id) {
return HippoIcon.fromSprite(id, Icon.MINUS_CIRCLE);
}
@Override
protected IDialogService.Dialog createRequestDialog() {
….
}
@Override
protected String execute(Workflow wf) throws Exception {
final String idFromRequestKey = getIdFromRequestKey("rejectReview");
ReviewWorkflow workflow = (ReviewWorkflow) wf;
workflow.rejectReview(idFromRequestKey, reason);
return null;
}
});
}
All of these buttons are an extension of the existing StdWorkflow. In the #execute(Workflow wf) method of the StdWorkflow you will have access to the ReviewWorkflow.
Other methods:
-
#getSubmenu : Information required on where to place the Menu item, in this case, you will have it bundled under “Review”
-
#getIcon: The icon which is displayed next to the button text, there are several you can already choose from. For this example, I’ve used the same as the publication workflow
-
#createRequestDialog : Will open up a dialogue before executing the #execute method, for example for the reason of Review Rejection etc.
The #execute method supplies you a workflow object which you’ll need to cast to a ReviewWorkflow to leverage the ReviewWorkflow to execute the desired actions.
@Override
protected String execute(Workflow wf) throws Exception {
ReviewWorkflow workflow = (ReviewWorkflow) wf;
workflow.requestReview();
return null;
}
The workflow and its frontend class need to be bootstrapped into the repository. To do this you add an entry in hippoecm-extension.xml that loads an XML with the registration of the workflow under hippo:workflows. Here’s the XML that accomplishes that:
<sv:node xmlns:sv="http://www.jcp.org/jcr/sv/1.0" xmlns:esv="http://www.onehippo.org/jcr/xmlimport" sv:name="default" esv:merge="combine">
<sv:node sv:name="handle" esv:merge="combine">
<sv:property sv:name="hipposys:classname" sv:type="String" esv:merge="override">
<sv:value>org.onehippo.repository.documentworkflow.DocumentReviewWorkflowImpl</sv:value>
</sv:property>
<sv:node sv:name="frontend:renderer" esv:merge="combine">
<sv:node sv:name="review">
<sv:property sv:name="jcr:primaryType" sv:type="Name">
<sv:value>frontend:plugin</sv:value>
</sv:property>
<sv:property sv:name="plugin.class" sv:type="String">
<sv:value>org.onehippo.threestep.cms.workflow.plugins.reviewedactions.ReviewRequestsWorkflowPlugin</sv:value>
</sv:property>
<sv:property sv:name="wicket.id" sv:type="String">
<sv:value>${item}</sv:value>
</sv:property>
</sv:node>
</sv:node>
..
</sv:node>
The end result of this step is that you have a toolbar menu which is always displayed for each user and displays all buttons. However, when the buttons are actually pressed nothing currently happens. Well, a log file entry happens but no real actions occur.
In the next steps, you are going to modify the correct menu buttons for the right role and actually code the actions (executed when clicked upon). This is where SCXML partially comes into place.
Step 6: Leverage SCXML to hide (and show) toolbar menu buttons
Let’s start looking at the SCXML file. I would now like to create a snippet which actually disables the publication request action from an Author and enables the Request for Review for an Author. I’ll also disable the Accept and Reject review for the Author because these actions are only available for a Reviewer.
The original SCXML snippet:
<state id="publishable">
<onentry>
<if cond="!requestPending or user=='system'">
<!-- if no request pending OR invoked by the 'system' user (scheduled workflow jobs daemon):
enable request publication operation -->
<hippo:action action="requestPublication" enabledExpr="true"/>
<if cond="workflowContext.isGranted(unpublished, 'hippo:editor')">
<!-- if (also) editor user (granted hippo:editor): enable publish operation -->
<hippo:action action="publish" enabledExpr="true"/>
</if>
</if>
The above configuration enables the publication request button (highlighted in yellow).
The snippet seen below disables the publication menu for the Author role (yellow highlight) and enables the request for review (orange highlight) for an author.
Custom SCXML snippet (for three-step workflow):
<state id="publishable">
<onentry>
<if cond="!requestPending or user=='system'">
<!-- if no request pending OR invoked by the 'system' user (scheduled workflow jobs daemon):
enable request publication operation -->
<if cond="workflowContext.isGranted(unpublished, 'hippo:author')">
<hippo:action action="requestReview" enabledExpr="true"/>
<hippo:action action="requestPublication" enabledExpr="false"/>
</if>
….
</if>
</onentry>
…..
What actually happens here? Well the SCXML configuration passes parameters to our Workflow Plugin (UI) which we call “hints”. It’s a bit of a legacy terminology but with the hints, we can retrieve parameters in our Workflow Plugin and hide certain buttons.
The following steps are required for the Review Workflow Plugin to hide the Accept and Reject review buttons.
-
Retrieve “Hints”:
final Map<String, Serializable> info = getHints();
-
Use “Hints” to hide buttons:
if (isActionAllowed(info, "requestReview") || isActionAllowed(info, "acceptReview")||isActionAllowed(info, "rejectReview")) {
if (!info.containsKey("publish")||info.containsKey("requestReview")) {
if (!info.containsKey("publish")) {
hideOrDisable(info, "requestReview", requestReview);
}
if (info.containsKey("requestReview")) {
hideOrDisable(info, "acceptReview", acceptReview);
hideOrDisable(info, "rejectReview", rejectReview);
}
} else {
requestReview.setVisible(false);
acceptReview.setVisible(false);
rejectReview.setVisible(false);
}
} else {
requestReview.setVisible(false);
acceptReview.setVisible(false);
rejectReview.setVisible(false);
}
Now, when I’m logged in as an Author I’ll only see Request Review button from the Review menu and I won’t see the Publication menu because there are no actions for me available from the Publication menu.
Step 7: SCXML - Assign and trigger Actions
Ok, what happens when I actually click on Request Review as an Author? Nothing right now. You need a snippet in the SCXML which will trigger some event which you’ve defined a listener to in our custom workflow from step 4.
You’ll need to read up on some of the basics on SCXML workflow actions and tasks.
The following SCXML snippets will call upon an action which is triggered through the custom ReviewWorkflowImpl you created in step 4.
<state id="publishable">
<onentry>
<if cond="!requestPending or user=='system'">
<!-- if no request pending OR invoked by the 'system' user (scheduled workflow jobs daemon):
enable request publication operation -->
<if cond="workflowContext.isGranted(unpublished, 'hippo:author')">
<hippo:action action="requestReview" enabledExpr="true"/>
<hippo:action action="requestPublication" enabledExpr="false"/>
</if>
<if cond="workflowContext.isGranted(unpublished, 'reviewer')">
<hippo:action action="requestPublication" enabledExpr="true"/>
<hippo:action action="requestReview" enabledExpr="false"/>
</if>
<if cond="workflowContext.isGranted(unpublished, 'hippo:editor')">
<hippo:action action="requestReview" enabledExpr="false"/>
<hippo:action action="publish" enabledExpr="true"/>
</if>
</if>
</onentry>
…..
<!-- target-less transition to create a publish request when no event payload parameter targetDate is provided -->
<transition event="requestReview" cond="!_event.data?.targetDate">
<hippo:requestReview type="publish" threeStepType="review" contextVariantExpr="unpublished"/>
</transition>
The above snippet corresponds with the custom workflow implementation from step 4:
public DocumentReviewWorkflowImpl() throws RemoteException {
}
@Override
public void requestReview() throws WorkflowException {
getWorkflowExecutor().start();
getWorkflowExecutor().triggerAction("requestReview");
}
The SCXML will trigger an Action: hippo:requestReview with 3 parameters:
- type=publish
- threeStepType=review
- contextVariantExpression=unpublished
You are currently triggering an Action from SCXML which doesn’t exist. In the next step, you are going to define an Action and Task in the Repository and take a look at the implementation of this Action and Task.
Step 8: SCXML - Executing Actions and Tasks
In the previous step you’ve added some snippets in the SCXML to trigger an action. But you don’t have any implementation for this action defined.
You can add the implementation of this action in the repository below, which has been fully described in our documentation here and here:
/hippo:configuration/hippo:modules/scxmlregistry/hippo:moduleconfig/hipposcxml:definitions/documentworkflow/
The following XML snippet will add the appropriate configuration for the SCXML action from the previous step:
<?xml version="1.0" encoding="UTF-8"?>
<sv:node sv:name="requestReview" xmlns:sv="http://www.jcp.org/jcr/sv/1.0">
<sv:property sv:name="jcr:primaryType" sv:type="Name">
<sv:value>hipposcxml:action</sv:value>
</sv:property>
<sv:property sv:name="hipposcxml:classname" sv:type="String">
<sv:value>org.onehippo.threestep.cms.workflow.scxml.RequestReviewAction</sv:value>
</sv:property>
<sv:property sv:name="hipposcxml:namespace" sv:type="String">
<sv:value>http://www.onehippo.org/cms7/repository/scxml</sv:value>
</sv:property>
</sv:node>
The above repository configuration makes sure the SCXML (see below) gets executed
<!-- target-less transition to create a publish request when no event payload parameter targetDate is provided -->
<transition event="requestReview" cond="!_event.data?.targetDate">
<hippo:requestReview type="publish" threeStepType="review" contextVariantExpr="unpublished"/>
</transition>
The action is a pretty simple class containing some parameters passed from the SCXML and a task. The actual magic happens in the task. The task contains a #doExecute method which can execute some custom code from the parameters which are passed through the SCXML to the action and finally executed in the task.
..
@Override
public Object doExecute() throws WorkflowException, RepositoryException, RemoteException {
DocumentHandle dm = getDocumentHandle();
if (!dm.isRequestPending()) {
if (targetDate == null) {
new ReviewRequest(getType(), getThreeStepType(), contextVariant.getNode(getWorkflowContext().getInternalWorkflowSession()),
contextVariant, getWorkflowContext().getUserIdentity());
} else {
new ReviewRequest(getType(), getThreeStepType(), contextVariant.getNode(getWorkflowContext().getInternalWorkflowSession()),
contextVariant, getWorkflowContext().getUserIdentity(), targetDate);
}
getWorkflowContext().getInternalWorkflowSession().save();
} else {
throw new WorkflowException("publication request already pending");
}
return null;
}
...
What actually happens in the RequestReviewTask code above is that a new request node is being created of type threestep:request and the threestep:type property is being persisted with “review”:
Now you have created the correct workflow for an Author. The publish menu isn’t shown, the review menu is visible with a request for review and whenever you click on request for review the correct action is being triggered to create a request node of type threestep:request and type is “review”
The next step is continuing on how to arrange the buttons, actions, etc for the reviewer and the editor. I’ll go through this a bit more quickly than before because all the basic concepts have been explained.
Step 9: Let’s speed things up...
In the last step, you’ve created everything you need for the author to request a review. From hiding and showing certain workflow toolbar buttons to actually assigning tasks with some code in the backend. All that remains now is that you use the same methodology to create the actions for the reviewer and, eventually, the editor.
You first start with the reviewer
Logged in as a reviewer you are able to walk 2 roads with a document. Either create a document of your own or review a document created by an author. When you create a document of your own, you’ll have the same default authorization that was available to the author before we implemented the three-step workflow. A reviewer can request publication to an editor directly, so the publication menu should be visible to the reviewer. This SCXML snippet will enable the publication menu for the reviewer (highlighted in yellow) and disable the request for review:
<state id="publishable">
<onentry>
<if cond="!requestPending or user=='system'">
<!-- if no request pending OR invoked by the 'system' user (scheduled workflow jobs daemon):
enable request publication operation -->
<if cond="workflowContext.isGranted(unpublished, 'hippo:author')">
<hippo:action action="requestReview" enabledExpr="true"/>
<hippo:action action="requestPublication" enabledExpr="false"/>
</if>
<if cond="workflowContext.isGranted(unpublished, 'reviewer')">
<hippo:action action="requestPublication" enabledExpr="true"/>
<hippo:action action="requestReview" enabledExpr="false"/>
</if>
<if cond="workflowContext.isGranted(unpublished, 'hippo:editor')">
<hippo:action action="requestReview" enabledExpr="false"/>
<hippo:action action="publish" enabledExpr="true"/>
</if>
</if>
</onentry>
…..
Now you have covered some available buttons for the reviewer. But what about the Accept and Reject Review?
For this, you’ll need to take a look at another section of the SCXML. The requested state of the document SCXML:
<state id="requested">
…
</state>
The requested state becomes active when document workflow requests are present.
Let’s take a look at the original SCXML state:
<!-- the requested state becomes active when document workflow requests are present -->
<state id="requested">
<onentry>
<foreach item="request" array="requests.values()">
<!-- for all requests determine the available request actions and report them through the special 'requests'
feedback map variable -->
<!-- for document workflow requests: -->
<if cond="request.workflowRequest">
<if cond="workflowContext.isGranted(request, 'hippo:editor')">
<!-- editor users (granted hippo:editor) may reject and accept as well as cancel requests -->
<if cond="request.workflowType!='rejected'">
<!-- if request not rejected yet, enable reject operation -->
<hippo:requestAction identifierExpr="request.identity" action="rejectRequest" enabledExpr="true"/>
</if>
<if cond="request.workflowType=='delete'">
<!-- if request for delete: enable accept operation if not live and not editing -->
<hippo:requestAction identifierExpr="request.identity" action="acceptRequest" enabledExpr="!live and !editing"/>
<elseif cond="request.workflowType=='publish'">
<!-- if request for publish: enable accept operation if modified and not editing -->
<hippo:requestAction identifierExpr="request.identity" action="acceptRequest" enabledExpr="modified and !editing"/>
</elseif>
…..
You can see that in the requested state you can iterate for each request action on the document and directly assign certain actions based on properties on the request node e.g. “rejected”, “delete”, “publish” etc. or based on the request type (e.g. worflowRequest, Scheduled operation) or even on the authorized role (e.g. reviewer, editor etc)
You’ll need to target requests of type “threestep:request” and assign actions according to the request type and the logged in user role.
You can easily add the following snippet to the requested state when you iterate for the request.
<if cond="request.requestType=='threestep:request'">
<if cond="workflowContext.isGranted(request, 'reviewer')">
<if cond="request.threeStepType=='review' and request.workflowType!='rejected'">
<hippo:action action="acceptReview" enabledExpr="!editing"/>
<hippo:requestAction identifierExpr="request.identity" action="acceptReview" enabledExpr="!editing"/>
<if cond="request.workflowType!='rejected'">
<hippo:action action="rejectReview" enabledExpr="!live and !editing"/>
<hippo:requestAction identifierExpr="request.identity" action="rejectReview" enabledExpr="true"/>
</if>
</if>
</if>
<if cond="workflowContext.isGranted(request, 'hippo:editor')">
<if cond="request.threeStepType=='publish'">
<if cond="request.workflowType!='rejected'">
<hippo:requestAction identifierExpr="request.identity" action="rejectRequest" enabledExpr="true"/>
</if>
….
...
</if>
<if cond="request.threeStepType=='review'">
<hippo:requestAction identifierExpr="request.identity" action="cancelRequest" enabledExpr="true"/>
</if>
</if>
</if>
The purple selection checks if the request type is of type “threestep:request”
The green selection checks if the authorized user is a reviewer (in the yellow it checks for an editor). If the logged in user is a reviewer and is accessing a node of type “threestep:request the orange section is triggered.
The orange section makes sure that when the “threestep:type” property on the request is “review”, the acceptReview and rejectReview buttons and actions are available.
When the SCXML is updated you will get the following result whenever a document is requested for review and you log is as a Reviewer:
The review menu will show you the accept and reject review items.
And of course you’ll need to assign actions whenever you click on “Accept publication review” or “Reject publication review” the same way you did before in Step 8 of this tutorial:
<transition event="acceptReview">
<!-- define temporary request variable for the event payload request parameter -->
<cs:var name="request" expr="_event.data?.request"/>
<!-- store the request workflow type as temporary variable -->
<hippo:acceptReview requestExpr="request" reviewer="user" />
</transition>
…
<transition event="rejectReview">
<!-- update the specific request to type rejected with an optional reason, using the event payload
'request' and optional 'reason' parameters -->
<hippo:rejectRequest requestExpr="_event.data?.request" reasonExpr="_event.data?.reason"/>
</transition>
You can find the code for the acceptReview Action in the following package:
- org.onehippo.threestep.cms.workflow.scxml.AcceptReviewAction
What the AcceptReviewAction does is pushing the review request forward as a publication request (for the editor)
Once you are logged in as an editor and the request type is “threestep:request” you’d want the same workflow available as you would have with a normal publication request and be able to either accept or reject the publication request.
This concludes the basics of how the three-step workflow plugin is set up and what the key points are with workflow and SCXML.
You can find the full source of the three-step workflow plugin and the demo project here:
https://github.com/ksalic/hippo-three-step-workflow