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. |
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:
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.
To complete this tutorial, you need to:
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 2git clone https://bitbucket.org/atlassian_tutorial/jira-custom-field
Alternatively, you can download the source as a ZIP archive.
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.
Open a Terminal and navigate to directory where you would like to keep your app code.
To create a Jira app skeleton, run the following command:
1 2atlas-create-jira-plugin
To identify your app, enter the following information.
group-id |
|
artifact-id |
|
version |
|
package |
|
Confirm your entries when prompted.
Navigate to the tutorial-jira-custom-field
directory created in the previous step.
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 2rm -rf ./src/test/java rm -rf ./src/test/resources/
Delete the unneeded Java class files.
1 2rm -rf ./src/main/java/com/example/plugins/tutorial/confluence/*
Import project into your favorite IDE.
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.
In this step you add some metadata about your app and your company or organization.
Navigate to the root folder of your app and open the pom.xml
file.
Add your company or organization name and your website URL to the organization
element:
1 2<organization> <name>Example Company</name> <url>http://www.example.com/</url> </organization>
Update the description
element:
1 2<description>Provides a custom field to store money amounts.</description>
Save the file.
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.
In your IDE, navigate to src/main/resources
and open the descriptor file.
You should see something like this:
1 2<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.
For this tutorial, you will need a Custom Field plugin module.
You'll add this by using the atlas-create-jira-plugin-module
command.
In your Terminal, navigate to the root folder where the pom.xml
is located.
Run the following command:
1 2atlas-create-jira-plugin-module
Select the Custom Field
option.
Enter the module parameters when prompted.
Enter New Classname |
|
Package Name |
|
Select N
for Show Advanced Setup.
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.
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.
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.
Add the following import statements to the ones the SDK added for you:
1 2import com.atlassian.jira.issue.customfields.impl.AbstractSingleFieldType; import java.math.BigDecimal; import com.atlassian.jira.issue.customfields.persistence.PersistenceFieldType;
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.
Create a constructor that passes arguments to superclass and annotates them with @JiraImport
.
Atlassian Spring Scanner will import them from host application.
1 2public 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.
Add the getStringFromSingularObject()
method to your class:
1 2@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.
Add the getSingularObjectFromString()
method:
1 2@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.
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@Override protected PersistenceFieldType getDatabaseType() { return PersistenceFieldType.TYPE_LIMITED_TEXT; }
Add the getObjectFromDbValue()
method:
1 2@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()
.
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@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 2package 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.
We're not finished yet, but let's try what we've created so far.
Make sure you have saved all your code changes to this point.
Open a Terminal and navigate to the app root folder where the pom.xml
file is located.
Run the following command:
1 2atlas-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[INFO] jira started successfully in 71s at http://localhost:2990/jira [INFO] Type CTRL-D to shutdown gracefully [INFO] Type CTRL-C to exit
Open localhost:2990/jira in your browser.
Log in with admin/admin.
Because this is a new instance, Jira prompts you to select a project type.
Create a new Jira project
Enter the project name and key.
Click Submit.
Jira displays an overview page for your new project.
On the overview page of your new project, click > Issues > Custom Fields.
Click Add Custom Field, and then Advanced.
Select your app, and then click Money Custom Field > Next.
Enter the following details when prompted.
Name | Expense |
Description | Accepts a money amount |
To associate the Field Tab with all screens, select all checkboxes and click Next.
Click Update.
Now we can see our new field in action by creating an issue.
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:
In a new Terminal window, navigate to src/main/resources/templates/customfields/money-custom-field
.
Open the view.vm
file and replace its contents with the following:
1 2#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
).
Open edit.vm
file and replace its contents with the following:
1 2#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.
To rebuild your app, run the following command that triggers QuickReload:
1 2atlas-package
Create an issue as you did before.
This time, our Expense field appears at the bottom of the form.
Enter a value in Expense field.
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!
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:
Rate this page: