Last updated Feb 19, 2024

Crucible workflow conditions plugin tutorial

About the tutorial

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:

  • Create a Fisheye/Crucible plugin
  • Implement the WorkflowCondition SPI class
  • Configure the  workflow-condition  module
  • Implement simple *Actions *to engage user with interaction after failed transition

This tutorial teaches you how to:

  • Implement the WorkflowCondition SPI class
  • Render a warning or error message for the user performing the transition
  • Check the condition on important transitions only.

For general information about the plugin development, see Fisheye and Crucible Plugin Guide - Atlassian Developers

Source

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
2
git clone https://bitbucket.org/atlassian_tutorial/crucible-workflow-condition-tutorial.git

Disclaimer

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.

Stage 1: Create plugin structure and run Fisheye/Crucible

Follow the steps below to learn how to create your plugin structure and run Fisheye/Crucible.

For more details on the initial setup of the SDK, and the first steps below, see Developing with the Atlassian Plugin SDK.

a) Create your plugin skeleton using the Atlassian Plugin SDK:

1
2
$ 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

Workflow Conditions API was introduced in a final shape in version 4.3.

1
2
<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 *

Stage 2: Implement Workflow Condition

Definition

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. 

How to start

Implement the com.atlassian.crucible.workflow.WorkflowCondition SPI class.  

1
2
@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
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:

  • OK - condition passes, review transition can be performed
  • WARN - condition fails, warnings can be skipped and the review transition performed
  • ERROR - conditions fails, review can't be transitioned without fixing validation errors

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
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:

Configuration

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
<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
$ 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
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();
    }
}

Stage 3: Implement interaction with user

Workflow Condition actions

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:

  • URL action
  • Javascript action

URL Action

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
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.

Javascript Action

The Javascript action represents a link and the Javascript handler to that link. To write such action, you'll need the following:

  • AMD module which exports an object with the *onClick *function
  • Web Resource Module which contains Javascript files 

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
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
<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
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 again

  • showError → shows error banner in the current dialog, accepts error message

  • hideError → hides error banner

In 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
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.  

Crucible core can't validate if the problem which triggered the condition failure was indeed solved. Because of that it'll just remove the item from the list. However, once user tries to proceed with the transition, Crucible will validate it once again. If the problem isn't solved (Workflow Condition fails again), the error message will simply reappear.

workflowConditionContext.showError(); shows the following error banner:

Rate this page: