Last updatedMay 6, 2019

Creating a custom field in Jira

Level of experience:

Beginner. You can follow this tutorial even if you have never developed an app before. 

Time estimate:

It should take you approximately 30 minutes to complete this tutorial.

Applicable:

Jira 7.0.0 and later.

Overview of the tutorial

This tutorial shows you how to create a simple custom field that can store a currency value. To create this field, you build a Jira app consisting of the following components:

  1. Java classes encapsulating the app logic.
  2. Resources for display of the app UI (Velocity templates).
  3. An app descriptor (XML file) to enable the plugin module in the Atlassian application.

Each component is discussed later in this tutorial.

About these instructions

You can use any supported combination of operating system and IDE to construct this app. These instructions were written using IntelliJ IDEA 2017.3 on macOS Sierra. If you use another combination, you should use the equivalent operations for your specific environment.

This tutorial was last tested with Jira 7.7.1 using Atlassian Plugin SDK 6.3.10. 

Before you begin

To complete this tutorial, you need to: 

  1. Know the basics of Java development: classes, interfaces, methods, how to use the compiler, and so on.
  2. Have the latest version of the Atlassian Plugin SDK on your development system. If you don't have the SDK or you are not familiar with it, start with the Getting started information.

App source

We encourage you to work through this tutorial. If you want to skip ahead or check your work when you have finished, you can find the app source code on Atlassian Bitbucket. To clone the repository, run the following command:

1
git clone https://bitbucket.org/atlassian_tutorial/jira-custom-field

Alternatively, you can download the source as a ZIP archive

Step 1. Create the app project

In this step, you'll use two atlas- commands to generate stub code for your app. The atlas- commands are part of the Atlassian Plugin SDK and automate much of the work of app development for you.

  1. Open a Terminal and navigate to directory where you would like to keep your app code.
  2. To create a Jira app skeleton, run the following command:

    1
    atlas-create-jira-plugin
  3. To identify your app, enter the following information.

    group-id

    com.example.plugins.tutorial.jira

    artifact-id

    tutorial-jira-custom-field

    version

    1.0-SNAPSHOT

    package

    com.example.plugins.tutorial.jira.customfields

  4. Confirm your entries when prompted.

  5. Navigate to the tutorial-jira-custom-field directory created in the previous step.

  6. Delete the test directories.

    Setting up testing for your app isn't part of this tutorial. To delete the generated test skeleton, run the following commands:

    1
    2
    rm -rf ./src/test/java
    rm -rf ./src/test/resources/
  7. Delete the unneeded Java class files.

    1
    rm -rf ./src/main/java/com/example/plugins/tutorial/confluence/*
  8. Import project into your favorite IDE.

Step 2. Review and tweak the generated stub code

It is a good idea to familiarize yourself with the stub app code. In this section, we'll check a version value and tweak a generated stub class. Open your app project in IDE and follow the next steps.

Add app metadata to the POM

In this step you add some metadata about your app and your company or organization.

  1. Navigate to the root folder of your app and open the pom.xml file.
  2. Add your company or organization name and your website URL to the organization element:

    1
    2
    3
    4
    <organization>
        <name>Example Company</name>
        <url>http://www.example.com/</url>
    </organization>
  3. Update the descriptionelement:

    1
    <description>Provides a custom field to store money amounts.</description>
  4. Save the file.

Review the generated app descriptor

Your stub code contains an app descriptor file atlassian-plugin.xml. This is an XML file that identifies the app to the host application (Jira) and defines the required app functionality.

  1. In your IDE, navigate to src/main/resources and open the descriptor file.

    You should see something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<atlassian-plugin key="${atlassian.plugin.key}" name="${project.name}" plugins-version="2">
    <plugin-info>
        <description>${project.description}</description>
        <version>${project.version}</version>
        <vendor name="${project.organization.name}" url="${project.organization.url}" />
        <param name="plugin-icon">images/pluginIcon.png</param>
        <param name="plugin-logo">images/pluginLogo.png</param>
    </plugin-info>
    <resource type="i18n" name="i18n" location="tutorial-jira-custom-field"/>
    <web-resource key="tutorial-jira-custom-field-resources" name="tutorial-jira-custom-field Web Resources">
        <dependency>com.atlassian.auiplugin:ajs</dependency>
        <resource type="download" name="tutorial-jira-custom-field.css" location="/css/tutorial-jira-custom-field.css"/>
        <resource type="download" name="tutorial-jira-custom-field.js" location="/js/tutorial-jira-custom-field.js"/>
        <resource type="download" name="images/" location="/images"/>
        <context>tutorial-jira-custom-field</context>
    </web-resource>

In the next step, you'll use the plugin module generator (another atlas- command) to generate the stub code for modules needed by the app.

Step 3. Add your plugin modules to the app descriptor

For this tutorial, you will need a Custom Field plugin module. You'll add this by using the atlas-create-jira-plugin-module command.

  1. In your Terminal, navigate to the root folder where the pom.xml is located.
  2. Run the following command:

    1
    atlas-create-jira-plugin-module
  3. Select the Custom Field option.

  4. Enter the module parameters when prompted.

    Enter New Classname

    MoneyCustomField (This is the CustomFieldType class.)

    Package Name

    com.example.plugins.tutorial.jira.customfields

  5. Select N for Show Advanced Setup.

  6. Select N for Add Another Plugin Module.

SDK adds the module declaration to the app descriptor and creates the source files for the module, including a class file and presentation templates. 

Step 4. Write the app code

You have already generated the stubs for your plugin modules. Now you will write code for the app. Our app will add a custom text field in a Jira issue that stores a money value. To do this, you need to implement the CustomFieldType interface and create Velocity templates for viewing and editing the field.

  1. Navigate to /src/main/java/com/example/plugins/tutorial/jira/customfields and open MoneyCustomField class.

    This class was generated by the atlas-create-jira-plugin-module command you ran earlier.

  2. Add the following import statements to the ones the SDK added for you:

    1
    2
    3
    import com.atlassian.jira.issue.customfields.impl.AbstractSingleFieldType;
    import java.math.BigDecimal;
    import com.atlassian.jira.issue.customfields.persistence.PersistenceFieldType;
  3. Custom Fields can store single values or multiple values. In our case, we want to store a single value. Change the class declaration to extend the AbstractSingleFieldType. Also, put a @Scanned annotation, so Atlassian Spring Scanner will 'notice' our class.

    1
    2
    @Scanned
    public class MoneyCustomField extends AbstractSingleFieldType<BigDecimal>

    This class provides much of the field implementation for you. Notice also that we'll use BigDecimal as our 'transport object' (for dealing with a currency in Java). A transport object is just a plain old Java object (POJO). The object type represents the custom field used.

  4. Create a constructor that passes arguments to superclass and annotates them with @JiraImport. Atlassian Spring Scanner will import them from host application.

    1
    2
    3
    4
    5
    public MoneyCustomField(
            @JiraImport CustomFieldValuePersister customFieldValuePersister,
            @JiraImport GenericConfigManager genericConfigManager) {
        super(customFieldValuePersister, genericConfigManager);
    }

    We won't override anything in getVelocityParameters(). Our field will use the default implementation from the AbstractCustomFieldType superclass.

    We'll implement a few abstract methods from AbstractSingleFieldType too.

  5. Add the getStringFromSingularObject() method to your class:

    1
    2
    3
    4
    5
    6
    7
    @Override
    public String getStringFromSingularObject(final BigDecimal singularObject) {
        if (singularObject == null)
            return null;
        else
            return singularObject.toString();
    }

    This method turns a value in our Transport object (BigDecimal, in our case) into text.

  6. Add the getSingularObjectFromString() method:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Override
    public BigDecimal getSingularObjectFromString(final String string) throws FieldValidationException {
        if (string == null)
            return null;
        try {
            BigDecimal decimal = new BigDecimal(string);
        if (decimal.scale() > 2) {
           throw new FieldValidationException(
                   "Maximum of 2 decimal places are allowed.");
        }
            return decimal.setScale(2);
        } catch (NumberFormatException ex) {
            throw new FieldValidationException("Not a valid number.");
        }
    }

    The method takes input from the user, validates it, and then puts it into a transport object. We want to validate that the user enters a valid number, and that there are no more than two decimal places.

  7. To tell Jira in what kind of database column to store the data, add the getDatabaseType() method. You can choose text, long text, numeric, or date. We could use numeric, but we will use text to keep it simple.

    1
    2
    3
    4
    @Override
    protected PersistenceFieldType getDatabaseType() {
       return PersistenceFieldType.TYPE_LIMITED_TEXT;
    }
  8. Add the getObjectFromDbValue() method:

    1
    2
    3
    4
    5
    @Override
    protected BigDecimal getObjectFromDbValue(final Object databaseValue)
            throws FieldValidationException {
        return getSingularObjectFromString((String) databaseValue);
    }

    This takes a value from the database and converts it to transport object. The value parameter is declared as Object, but will be String, Double, or Date depending on the database type defined above. Because we chose FieldType TEXT, we will get a String and can reuse getSingularObjectFromString().

  9. Add the getDbValueFromObject() method. It takes a value as our transport object and converts it to an Object suitable for storing in the database. In our case we want to convert to String.

    1
    2
    3
    4
    @Override
    protected Object getDbValueFromObject(final BigDecimal customFieldObject) {
        return getStringFromSingularObject(customFieldObject);
    }

That's it for the MoneyCustomField class. Your class should look like this (unused code removed):

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
53
54
55
56
57
58
59
60
61
package com.example.plugins.tutorial.jira.customfields;

import com.atlassian.jira.issue.customfields.impl.AbstractSingleFieldType;
import com.atlassian.jira.issue.customfields.persistence.PersistenceFieldType;
import com.atlassian.plugin.spring.scanner.annotation.component.Scanned;
import com.atlassian.plugin.spring.scanner.annotation.imports.JiraImport;
import com.atlassian.jira.issue.customfields.manager.GenericConfigManager;
import com.atlassian.jira.issue.customfields.persistence.CustomFieldValuePersister;
import com.atlassian.jira.issue.customfields.impl.FieldValidationException;
import java.math.BigDecimal;

@Scanned
public class MoneyCustomField extends AbstractSingleFieldType<BigDecimal> {

    public MoneyCustomField(
            @JiraImport CustomFieldValuePersister customFieldValuePersister,
            @JiraImport GenericConfigManager genericConfigManager) {

        super(customFieldValuePersister, genericConfigManager);
    }

    @Override
    public String getStringFromSingularObject(final BigDecimal singularObject) {
        if (singularObject == null)
            return null;
        else
            return singularObject.toString();
    }

    @Override
    public BigDecimal getSingularObjectFromString(final String string) throws FieldValidationException {
        if (string == null)
            return null;
        try {
            BigDecimal decimal = new BigDecimal(string);
            if (decimal.scale() > 2) {
                throw new FieldValidationException(
                        "Maximum of 2 decimal places are allowed.");
            }
            return decimal.setScale(2);
        } catch (NumberFormatException ex) {
            throw new FieldValidationException("Not a valid number.");
        }
    }

    @Override
    protected PersistenceFieldType getDatabaseType() {
        return PersistenceFieldType.TYPE_LIMITED_TEXT;
    }

    @Override
    protected BigDecimal getObjectFromDbValue(final Object databaseValue)
            throws FieldValidationException {
        return getSingularObjectFromString((String) databaseValue);
    }

    @Override
    protected Object getDbValueFromObject(final BigDecimal customFieldObject) {
        return getStringFromSingularObject(customFieldObject);
    }
}

By default, Atlassian apps use the Simple Logging Facade for Java (SLF4J) for logging. In the sample we do not show how to use logging. In general, you can add log messages in your code where appropriate for troubleshooting and debugging. Then set the desired logging level in the Jira administration console.

For a tutorial of an app that uses logging, see Tutorial - Writing Jira event listeners with the atlassian-event library. Also see the Confluence logging guidelines. While targeted for Confluence, the page covers some general concepts related to logging as well. 

Step 6. Build and test your app

We're not finished yet, but let's try what we've created so far.

Start Jira with your app and create a project

  1. Make sure you have saved all your code changes to this point.
  2. Open a Terminal and navigate to the app root folder where the pom.xml file is located.
  3. Run the following command:

    1
    atlas-run

    This command builds your app code, starts a Jira instance, and installs your app in it. This may take several seconds or so. When the process completes you see many status lines on your screen concluding with something like the following:

    1
    2
    3
    [INFO] jira started successfully in 71s at http://localhost:2990/jira
    [INFO] Type CTRL-D to shutdown gracefully
    [INFO] Type CTRL-C to exit
  4. Open localhost:2990/jira in your browser.

  5. Log in with admin/admin.
    Because this is a new instance, Jira prompts you to select a project type. 
  6. Create a new Jira project

  7. Enter the project name and key.

  8. Click Submit.
    Jira displays an overview page for your new project.

Add your custom field to the project configuration

  1. On the overview page of your new project, click cog icon >  Issues > Custom Fields

  2. Click Add Custom Field, and then Advanced.

  3. Select your app, and then click Money Custom Field > Next.

  4. Enter the following details when prompted. 

    Name

    Expense

    Description

    Accepts a money amount

  5. To associate the Field Tab with all screens, select all checkboxes and click Next.

    Associate fields
  6. Click Update.

Test the new field

Now we can see our new field in action by creating an issue.

  1. On the menu bar, click Create Issue.
  2. Select your newly created project.
  3. In the Create Issue form, scroll to the bottom of the page and notice the placeholder text edit.vm.
    The app worked, which is good, but it's not usable yet, which is not so good. We need to customize our template code to allow for user input to the field.
  4. Click Cancel, but keep Jira running and your browser window open for now. We'll get back to it in a minute.

Step 7. Edit the Velocity templates

Apps can expose interface in Jira through Velocity templates. The SDK gave us two templates, one for viewing the field value and another for editing it.

There are two additional templates you can create: column-view, which lets you customize the field value rendering in the issue navigator (Jira uses the view.vm rendering otherwise), and xml, which renders your field in the XML and RSS output for the issue.

See Tutorial - Creating a custom field type for more information.

Customize the default templates as follows:

  1. In a new Terminal window, navigate to src/main/resources/templates/customfields/money-custom-field.

  2. Open the view.vm file and replace its contents with the following:

    1
    2
    3
    #if ($value)
        $$value
    #end

    The #if clause simply checks for null values in the value variable. If its value is null, nothing is shown in the template rendering. Otherwise, $value in the second line is replaced with the value of our transport object in the rendered template.

    The additional '$' symbol is a literal dollar sign. You can change this to another currency symbol if desired (for example, €$value).

  3. Open edit.vm file and replace its contents with the following:

    1
    2
    3
    #customControlHeader ($action $customField.id $customField.name $fieldLayoutItem.required $displayParameters $auiparams)
    <input class="text" id="$customField.id" name="$customField.id" type="text" value="$textutils.htmlEncode($!value)" />
    #customControlFooter ($action $customField.id $fieldLayoutItem.fieldDescription $displayParameters $auiparams)

    This is the same code found in the edit-basictext.vm template in the Jira core code. This means that this field will look the same as many other text fields in Jira that use the 'basic text' input template.

    The #customControlHeader and #customControlFooter are Velocity macros defined in the macros.vm file built into Jira. These macros check certain conditions and add standard UI text around the field, including a label, description, and validation error messages.

    The interesting bit is the input element. It's the HTML form element that exposes the text field for entering a value for our custom field.

Step 8. Do a live reload and try the app again

  1. To rebuild your app, run the following command that triggers QuickReload:

    1
    atlas-package
  2. Create an issue as you did before.
    This time, our Expense field appears at the bottom of the form.

  3. Enter a value in Expense field.
  4. Click Create.

Experiment some more with this field. You can try entering invalid values. Try entering something like '2' or '2.5' and see how it gets rendered after you save. You could also leave the field empty. Edit some of the issues you created. Notice that if you don't enter value in the field, the field is hidden when you edit the issue later.

Congratulations, that's it!

Have a treat!

Next steps

You can keep going by experimenting with different field types.

Also, try extending the app by adding a custom field searcher. You can use the Atlassian Plugin SDK to generate the initial code for your custom field searcher, using atlas-create-jira-plugin-module and selecting the Custom Field Searcher option as the module to create. From there, you can select the custom field searcher class to extend, such as text searcher or exact text searcher.

For more about custom field searchers, see custom field searcher.

Also, check out these resources on custom fields: