The plugin created in this tutorial makes use of the workflow-condition modules. The plugin prevents a user from closing a review until at least one reviewer completes the review.
In this tutorial you will:
WorkflowCondition
SPI classThis tutorial teaches you how to:
WorkflowCondition
SPI classFor general information about the plugin development, see Fisheye and Crucible Plugin Guide - Atlassian Developers
We encourage you to work through this tutorial. If you want to skip ahead or check your work when you are done, you can find the plugin source code on Atlassian Bitbucket. Bitbucket serves as a public Git repository containing the tutorial's code. To clone the repository, run:
1
git clone https://bitbucket.org/atlassian_tutorial/crucible-workflow-condition-tutorial.git
The Workflow Conditions API was introduced to Fisheye/Crucible in version 4.2 as an experimental API and had changed in 4.3 release. From version 4.3, Fisheye/Crucible is considered as final and there are no planned changes towards it anymore.
Follow the steps below to learn how to create your plugin structure and run Fisheye/Crucible.
a) Create your plugin skeleton using the Atlassian Plugin SDK:
1 2 3 4 5 6
$ atlas-create-fecru-plugin
...
Define value for groupId: : com.example.ampstutorial
Define value for artifactId: : fecrutwitter
Define value for version: 1.0-SNAPSHOT: : # just accept the default
Define value for package: com.example.ampstutorial: : # again, just press enter for the default
b) (optional) If atlas-create-fecru-plugin creates a plugin skeleton with a different version of Fisheye/Crucible than you want to use, update the properties in pom.xml
1 2 3 4
<properties>
<fecru.version>4.3.0-20170119092650</fecru.version>
<fecru.data.version>4.3.0-20170119092650</fecru.data.version>
</properties>
c) Run Fisheye/Crucible with the skeleton plugin:
1 2
$ cd workflowcondition
$ mvn fecru:debug
d) Go to http://localhost:3990/fecru/
e) Authenticate with the following credentials: username: admin password: admin
In this tutorial you're going to implement the condition which prevents a user from closing a review when none of the reviewers have completed the review.
Implement the com.atlassian.crucible.workflow.WorkflowCondition
SPI class.
1 2 3 4 5 6
@Named
public class CompletedReviewerWorkflowCondition implements WorkflowCondition {
public ValidationResult validateTransition(WorkflowTransitionContext transitionContext) {
return ValidationResult.ok();
}
}
The condition in the current state doesn't do anything useful yet. To check for some conditions during the review transition, you must implement the validateTransition
method. This method is called every time a review is being transitioned. It's up to you to verify that it's the transition type the condition should react to.
1 2 3 4 5 6 7 8 9 10
public ValidationResult validateTransition(WorkflowTransitionContext transitionContext) {
if (isCloseReview(transitionContext.getTransitionAction())) {
return doValidate(transitionContext.getReviewId());
}
return ValidationResult.ok();
}
private boolean isCloseReview(String transitionAction) {
return ReviewService.Action.Close.getActionString().equals(transitionAction);
}
The validateTransition
method should return a ValidationResult
, which can be of one of the following states:
As soon as you defined the programmed condition to react only to closing a review you need to implement the condition check. For the purpose of this tutorial, the check verifies only if there's at least one reviewer who completed the review.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
private ValidationResult doValidate(PermId<ReviewData> reviewId) {
if (isAtLeastOneReviewerCompleted(reviewId)) {
return ValidationResult.ok();
}
return ValidationResult.error(CompletedReviewerWorkflowCondition.class.getSimpleName(),
new ResultMessage("No one completed the review",
"This review can't be closed until at least one reviewer completes it",
"This review can't be closed until at least one reviewer completes it"));
}
private boolean isAtLeastOneReviewerCompleted(PermId<ReviewData> reviewId) {
return reviewService.getAllReviewers(reviewId).isEmpty()
|| !reviewService.getCompletedReviewers(reviewId).isEmpty();
}
A proper ValidationResult
can be created using static factory methods and consists of:
In previous step, you implemented CompletedReviewerWorkflowCondition
, which defines the behaviour of your condition. Now, lis the condition in atlassian-plugin.xml in workflow-condition
module to let Fisheye/Crucible's plugin system know about it.
1 2 3 4
<workflow-condition
key="completed-reviewers-condition"
class="com.example.ampstutorial.CompletedReviewerWorkflowCondition">
</workflow-condition
The class is the class of the workflow condition, and the key are unique identifiers required by the plugin system. A single plugin can define multiple workflow-condition
modules. Once the plugin module is configured, a review can't be closed it, until at least one reviewer completes it.
Invoking the transition using REST and JAVA APIs is also prohibited and will fail with an error.
1 2 3 4 5 6 7 8 9 10 11 12
$ curl -u admin:admin -X POST http://localhost:3990/fecru/rest-service/reviews-v1/CR-2/transition.json\?action\=action:closeReview
{
"reviewId": "CR-2",
"message": "Some of workflow validation conditions have failed",
"failedConditions": [
{
"resultKey": "CompletedReviewerWorkflowCondition",
"message": "This review can't be closed until at least one reviewer completes it",
"severity":"error"
}
]
}
CompletedReviewerWorkflowCondition.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
package com.example.ampstutorial;
import com.atlassian.crucible.spi.PermId;
import com.atlassian.crucible.spi.data.ReviewData;
import com.atlassian.crucible.spi.services.ReviewService;
import com.atlassian.crucible.workflow.ResultMessage;
import com.atlassian.crucible.workflow.ValidationResult;
import com.atlassian.crucible.workflow.WorkflowCondition;
import com.atlassian.crucible.workflow.WorkflowTransitionContext;
import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
import javax.inject.Inject;
import javax.inject.Named;
@Named
public class CompletedReviewerWorkflowCondition implements WorkflowCondition {
private final ReviewService reviewService;
@Inject
public CompletedReviewerWorkflowCondition(@ComponentImport ReviewService reviewService,
@ComponentImport SoyTemplateRenderer renderer) {
this.reviewService = reviewService;
this.renderer = renderer;
}
public ValidationResult validateTransition(PermId<ReviewData> reviewId, String transitionAction, @Nullable UserData currentUser) {
if (isCloseReview(transitionAction)) {
return doValidate(reviewId);
}
return ValidationResult.ok();
}
private boolean isCloseReview(String transitionAction) {
return ReviewService.Action.Close.getActionString().equals(transitionAction);
}
private ValidationResult doValidate(PermId<ReviewData> reviewId) {
if (isAtLeastOneReviewerCompleted(reviewId)) {
return ValidationResult.ok();
}
return ValidationResult.error(CompletedReviewerWorkflowCondition.class.getSimpleName(),
new ResultMessage("No one completed the review",
"This review can't be closed until at least one reviewer completes it",
"This review can't be closed until at least one reviewer completes it"));
}
private boolean isAtLeastOneReviewerCompleted(PermId<ReviewData> reviewId) {
return reviewService.getAllReviewers(reviewId).isEmpty()
|| !reviewService.getCompletedReviewers(reviewId).isEmpty();
}
}
In previous steps, you wrote a workflow condition which presents simple error message to a user but doesn't allow for any user interaction. ResultMessage can also contain actions which allow for such interaction. The starting point for building any kind of action is the com.atlassian.crucible.workflow.ResultAction
interface. Crucible allows for two types of actions:
The URL action represents a simple HTML link. It can be constructed with the buildUrlAction
method. For the purpose of this tutorial, let's include the link to a Users page in a failed condition message.
1 2 3 4 5 6 7 8 9 10 11 12
private ValidationResult doValidate(PermId<ReviewData> reviewId) {
if (isAtLeastOneReviewerCompleted(reviewId)) {
return ValidationResult.ok();
}
return ValidationResult.error(CompletedReviewerWorkflowCondition.class.getSimpleName(),
new ResultMessage("No one completed the review",
"This review can't be closed until at least one reviewer completes it",
"This review can't be closed until at least one reviewer completes it",
Lists.newArrayList(
ResultAction.buildUrlAction("See users", applicationProperties.getBaseUrl() + "/users")
)));
}
As a result of such ValidationResult, Crucible will include the link to a Users page as a part of the failure message.
The Javascript action represents a link and the Javascript handler to that link. To write such action, you'll need the following:
In this tutorial, we'll write an action using the AJAX request which sends a mail reminder to users who didn't complete a review. To achieve that goal, we'll need a REST resource which accepts HTTP requests with the review id, and sends the mail messages to every reviewer who didn't complete their review. Writing such resource isn't a part of this tutorial, so we'll use a ready component, which is accessible in the tutorial repository. To start writing the Javascript action, we'll create Javascript with the AMD module.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
define('FECRU/workflow-condition-tutorial/remind-handler', ['require'], function (require) {
'use strict';
var $ = require('jquery');
var fecru = require('global-ns/fecru');
var onClickFunc = function (workflowConditionContext) {
var url = fecru.pageContext + '/rest/workflowcondition-tutorial/latest/' + workflowConditionContext.reviewId;
$.ajax({
type: "get",
url: url,
dataType: "json",
success: function() {
},
error: function() {
}
});
};
return {
onClick: onClickFunc
};
});
Next, we'll create Web Resource which serves that file:
1 2 3
<web-resource key="remind-handler" name="remind-handler JS action">
<resource type="download" name="remind-handler.js" location="/js/remind-handler.js"/>
</web-resource>
Once front-end side is configured, your plugin needs to notify the Crucible core that here's an additional Javascript action.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
private ValidationResult doValidate(PermId<ReviewData> reviewId) {
if (isAtLeastOneReviewerCompleted(reviewId)) {
return ValidationResult.ok();
}
return ValidationResult.error(CompletedReviewerWorkflowCondition.class.getSimpleName(),
new ResultMessage("No one completed the review",
"This review can't be closed until at least one reviewer completes it",
"This review can't be closed until at least one reviewer completes it",
Lists.newArrayList(
ResultAction.buildUrlAction("See users", applicationProperties.getBaseUrl() + "/users"),
ResultAction.buildJavaScriptAction("Send reminders",
"com.example.ampstutorial.workflowcondition:remind-handler",
"FECRU/workflow-condition-tutorial/remind-handler")
)));
}
Given code will result in the following dialog:
Workflow Condition Context
Crucible passes Workflow Condition Context to Javascript on click handler in order to extend possibilities of plugin developers. Context object exports the following functions:
reviewId
→ returns the review id
hideDialog
→ hides transition dialog
resolveMe
→ resolves current condition on the front-end side, which results in removing it from the dialog. Note: This action will affect only the front-end side. If the problem which triggered the workflow condition is not fixed, the transition will fail againshowError
→ shows error banner in the current dialog, accepts error messagehideError
→ hides error bannerIn the next step, we'll extend remind-handler to resolve the condition when AJAX call succeeds and show the error banner in case of an error. To do that, we'll use the context object as a handler.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
define('FECRU/workflow-condition-tutorial/remind-handler', ['require'], function (require) {
'use strict';
var $ = require('jquery');
var fecru = require('global-ns/fecru');
var onClickFunc = function (workflowConditionContext) {
var url = fecru.pageContext + '/rest/workflowcondition-tutorial/latest/' + workflowConditionContext.reviewId;
$.ajax({
type: "get",
url: url,
dataType: "json",
success: function() {
workflowConditionContext.resolveMe();
},
error: function() {
workflowConditionContext.showError();
}
});
};
return {
onClick: onClickFunc
};
});
With such handler, if the request succeeds, Crucible will remove given message from the list. In this particular case there was only one error message so the user is allowed to proceed.
workflowConditionContext.showError();
shows the following error banner: