JIRA 4.3 : How to create a new Custom Field Type
This page last changed on Nov 09, 2010 by mdoar.
Since JIRA 3.0 you have been able to create your own Custom Field Types through the plugin interface. In this tutorial, we'll take a look at a few simple examples and explain how you can easily achieve this. Before you start, you may also want to familiarise yourself with the JIRA Plugin Guide and Adding a Custom Field.
A Quick Custom Field Types PrimerThere's a few things you need to understand before diving into custom fields. A custom field type can have three components.
A custom field class extends the interface CustomFieldType. This interface provides methods to retrieve and store custom fields values. There are several extension points that are available to make creating new custom field types easier (e.g. CalculatedCFType, AbstractSingleFieldType, AbstractMultiSettableCFType). It is also possible to extend existing custom field types to add functionality (e.g. A currency type extending NumberCFType). The second component are the resource templates which render the custom field. There are four view types available, each representing a different context to render the custom field.
The values of these templates are usually Velocity template files that are either standard JIRA .vm files, or ones written for the custom field and reside in your templates directory. Make sure that their names are unique. Linking the Java code and rendering views are the plugin-module descriptors in your atlassian-plugin.xml. They allow JIRA to recognise what custom fields are available to the system and how to render them. Example module descriptor <atlassian-plugin key="com.atlassian.jira.plugin.customfield.example" name="JIRA Customfields Examples Plugin"> <plugin-info> <description>Customfields Examples Plugin.</description> <version>1.0</version> <application-version min="3.3" max="3.3"/> <vendor name="Atlassian Software Systems Pty Ltd" url="http://www.atlassian.com"/> </plugin-info> <customfield-type key="textarea" name="Free Text Field (unlimited text)" class="com.atlassian.jira.issue.customfields.impl.TextAreaCFType"> <description>A multiline text area custom field to allow input of longer text strings.</description> <resource type="velocity" name="view" location="templates/plugins/fields/view/view-basictext.vm"/> <resource type="velocity" name="column-view" location="templates/plugins/fields/view/view-limited-text.vm"/> <resource type="velocity" name="edit" location="templates/plugins/fields/edit/edit-textarea.vm"/> <resource type="velocity" name="xml" location="templates/plugins/fields/xml/xml-basictext.vm"/> </customfield-type> </atlassian-plugin> Information about setting up a complete plugin development environment for a plugin can be found here. ExamplesAdmin-only editable fieldFor the first example, we'll construct a custom field that is only be editable by JIRA administrators and appear as a plain text to others. This is a simple customisation of the shipped TextCFType field and can be customized by changing one template and extending TextCFType in a new class. Step 1. Create a Plugin Skeleton is a good starting point. First, we need to extend TextCFType. After creating and testing the skeleton by itself, edit MyPlugin.java like so (notice namespace.packagename.MyPlugin is this example's namespace and class name): package namespace.packagename; import com.atlassian.jira.issue.customfields.converters.StringConverter; import com.atlassian.jira.issue.customfields.impl.TextCFType; import com.atlassian.jira.issue.customfields.manager.GenericConfigManager; import com.atlassian.jira.issue.customfields.persistence.CustomFieldValuePersister; public class MyPlugin extends TextCFType { public MyPlugin(CustomFieldValuePersister customFieldValuePersister, StringConverter stringConverter, GenericConfigManager genericConfigManager) { super(customFieldValuePersister, stringConverter, genericConfigManager); } } ... <customfield-type key="admintextfield" name="Admin Editable Text Field" class="namespace.packagename.MyPlugin"> <description>A text field only editable by JIRA-administrators. Others will see only text.</description> <resource type="velocity" name="view" location="templates/plugins/fields/view/view-basictext.vm"/> <resource type="velocity" name="edit" location="templates/edit-jiraadminonlytext.vm"/> <resource type="velocity" name="xml" location="templates/plugins/fields/xml/xml-basictext.vm"/> </customfield-type> ...
This module definition exactly matches that of a standard text field except for one line. <resource type="velocity" name="edit" location="templates/edit-jiraadminonlytext.vm"/> edit-jiraadminonlytext.vm #controlHeader ($action $customField.id $customField.name $fieldLayoutItem.required $displayParameters.noHeader) #if ($authcontext.user.inGroup('jira-administrators')) <input type="text" name="$customField.id" value="$!value" /> #else #if($value && ! $value.equals("")) #set ($displayValue = ${value}) #else #set ($displayValue = 'N/A') #end <span title="This field is editable only by JIRA administrators">$!displayValue</span> <input type="hidden" name="$customField.id" value="$!value" /> #end #controlFooter ($action $fieldLayoutItem.fieldDescription $displayParameters.noHeader) There's a few points to note.
And that's it. Rebuild the plugin, deploy the JAR, login as an administrator and you should see it in the list of plugins. Once enabled, look for the custom field under 'Administration -> Issue Fields -> Custom Fields -> Add Custom Field.' Once added, log in as a normal user as well to test it out.
Last commented user calculated fieldThe next example deals with a Calculated Custom Field. Calculated don't actually store any values. You often want or need this when you want to search on fields not normally available in JIRA, but the information can be derived. In this case, we want to return the last user who commented on the issue, if they are not an administrator. We only want this field to be visible in the issue navigator and not the edit or view pages. Coding the Custom Field TypeBefore you implement the interface CustomFieldType you should check out the latest javadoc. A useful extension point for calculated custom fields is, unsurprisingly, CalculatedCFType, where only three methods need to be implemented (getStringFromSingularObject, getSingularObjectFromString, and getValueFromIssue). If you also choose to implement SortableCustomField you will need to implement compare() as well. The key method used to retrieve the value of our custom field is getValueFromIssue. public Object getValueFromIssue(CustomField field, Issue issue) { User currentUser = authenticationContext.getUser(); User lastUser = null; try { List comments = actionManager.getComments(issue.getGenericValue(), currentUser); if (comments != null && !comments.isEmpty()) { Comment lastComment = (Comment) comments.get(comments.size()-1); User commenter = lastComment.getUser(); if (!commenter.inGroup(JIRA_ADMIN)) { lastUser = commenter; } } } catch (GenericEntityException e) { // Handle this } return lastUser; } The return type Object is also known as the Transport Object. In this instance it is of type User, but it could be any other type. The Transport type must remain consistent across all methods such as create, update and also the view and edit templates. Wiring it togetherMuch like the previous example, we can reuse some of the the templates that ship with JIRA. <customfield-type key="lastusercommented" name="Last user commenter" class="com.atlassian.jira.plugin.customfield.example.LastUserCommentedCFType"> <description>This is a lookup field that displays the last commenter who is not a JIRA administrator</description> <resource type="velocity" name="column-view" location="templates/plugins/fields/view/view-user.vm"/> <resource type="velocity" name="xml" location="templates/plugins/fields/xml/xml-user.vm"/> </customfield-type>
Enable SearchingThe last commenter field in itself isn't all that useful. While we can see it in on the issue navigator, we can't search for a particular user who commented it last. Searching in JIRA is handled by CustomFieldSearchers. Again several pre-configured searchers are available. You must ensure that the Transport Object are compatible between the custom field and the custom field searcher. Thus we can only use the UserPicker searcher since this is the only one that indexes User objects. <customfield-searcher key="userpickersearcher" name="User Picker Searcher" i18n-name-key="admin.customfield.searcher.userpickersearcher.name" class="com.atlassian.jira.issue.customfields.searchers.UserPickerSearcher"> <description key="admin.customfield.searcher.userpickersearcher.desc">Allow to search for a user using a userpicker.</description> <resource type="velocity" name="label" location="templates/plugins/fields/view-searcher/label-searcher-user.vm"/> <resource type="velocity" name="search" location="templates/plugins/fields/edit-searcher/search-userpicker.vm"/> <resource type="velocity" name="view" location="templates/plugins/fields/view-searcher/view-searcher-basictext.vm"/> <valid-customfield-type package="com.atlassian.jira.plugin.customfield.example" key="lastusercommented"/> </customfield-searcher> Now when you create/edit your Last User Commented custom field, you'll be able to select the User Picker as a search template. You can now search on the last commenter field in the issue issue navigator. Important When you change a search template for a custom field, you may need to perform a reindex before the search will work correctly. This issue is being tracked at JRA-4641.
Sorting in Issue NavigatorTo enable sorting you simply need to implement the interface SortableCustomField public class LastUserCommentedCFType extends AbstractCustomFieldType implements SortableCustomField public int compare(Object customFieldObjectValue1, Object customFieldObjectValue2, CustomFieldConfig customFieldConfig) { return new BestNameComparator().compare(customFieldObjectValue1, customFieldObjectValue2); } Amazon search pluginLastly, a frivolous plug-in to give you some ideas on how to implement custom fields that perform remote look ups. Basically, we want a custom field that will take a text string and display a results from a search through the Amazon. There are several approaches to this, but by simplest solution is to treat the stored value as a simple text field and then add a viewHelper object that effectively transforms the string into the desired result. Coding and Attaching the view helperFirst we need to code our Amazon view helper. You can take a look in the source, but how it's been implemented isn't all that relevant. Once we have the view helper, we can pass this helper to the Velocity templates through the method getVelocityParameters public Map getVelocityParameters(Issue issue) { Map map = new HashMap(); map.put("amazonSearchViewHelper", new AmazonSearchViewHelper()); return map; } #if ($value) Results for search query "${value}" <br /> <table class="grid"> <tr> <th>Title</th> <th>Primary Author</th> </tr> #foreach ($book in $amazonSearchViewHelper.searchForBooks($value)) <tr> <td><a target="_new" href="${book.link}">${book.title}</a></td> <td>${book.firstAuthor}</td> </tr> #end </table> #end ![]() Confluence Page Link custom field
The 'Confluence Page Link' custom field plugin provides an example of implementing a custom field that performs a remote look up through XML/RPC.. This custom field provides a pop-up searcher - allowing the user to enter a search query that is executed over publicly accessible pages within a specified Confleunce instance. The user can select a result and the URL of thatpage is stored in the custom field - a simple text field. The Confluence instance to search against is specified in a properties file. A new webwork action 'ConfluencePageBrowserAction' is required - allowing the popup window view to be associated with the action that performs and returns results from the Confluence page search. The webwork action is registered in the atlassian-plugin.xml file as follows: <webwork1 key="confluencepagebrowseraction" name="Confluence Page Browser Action" class="java.lang.Object"> <actions> <action name="com.atlassian.jira.plugin.confluencelinker.ConfluencePageBrowserAction" alias="ConfluencePageBrowser"> <view name="success">/templates/confluence_page_browser.vm</view> </action> </actions> </webwork1> XmlRpcClient rpcClient = new XmlRpcClient(confluenceURL); Vector xmlrpcResults = (Vector) rpcClient.execute("confluence1.search", makeParams(getSearchQuery(), 100)); if (xmlrpcResults != null) { earchResults = new ArrayList(); for (Iterator iterator = xmlrpcResults.iterator(); iterator.hasNext();) { Hashtable xmlrpcResult = (Hashtable) iterator.next(); searchResults.add(new SearchMatch(xmlrpcResult)); } ... #foreach ($result in $action.getSearchResults()) <tr onmouseover="rowHover(this)" onclick="selectLink('$result.getUrl()')"> <td> <div class="borderedbox"> <b>Title</b>: $result.getTitle()<br> <b>URL</b>: $result.getUrl()<br> <b>Excerpt</b>: #if($result.getExcerpt())$result.getExcerpt() #else None #end </div> </td> </tr> ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() |
![]() |
Document generated by Confluence on Mar 27, 2011 18:54 |