In this tutorial we will develop the Status Bar column type, which shows a progress-like bar filled with color stripes, each stripe's color representing a particular issue status, and each stripe's width being proportional to the number of issues having that status in the current issue's subtree.
You can download both the compiled plugin and its source code from API Usage Samples. |
The PlanA column type consists of several components. The client-side components are written in JavaScript and have two responsibilities:
The server-side components are written in Java and responsible for:
For the Status Bar column we'll need to write code to cover all of the above responsibilities. In general, however, only the client-side part is strictly necessary. If the attributes provided by Structure are enough for your column, you can skip the server-side attribute provider. You can also skip the components related to export, if this functionality is not critical. In that case, you can jump straight to the client-side part, consulting the other chapters as necessary. For the complete treatment, please continue reading from top to bottom. The AttributesBefore we begin, let's decide which attributes we need to pass from the server side to render a status bar. Obviously, the status bar depends on the statuses of all the issues in the given issue's subtree. This suggests that we need to use an "aggregate" attribute, and because Structure does not provide such an aggregate out of the box, we'll need to write our own. Secondly, the colors and the order of statuses in the status bar are only a presentational matter. If we had a map from status IDs to sub-issue counts in the given issue's subtree, we could count the total number of sub-issues, scale the colored stripes so that they'd fill the whole status bar, and render them in any given order. Thirdly, the "Include itself" option is somewhat trickier. When it's on, the current issue's status is shown in its status bar, as if there is one more sub-issue. When it's off, the current issue is excluded, and the status bar shows only its sub-issues (on all levels). We could try to implement this on the server side as a separate aggregate, however, this approach has a couple of drawbacks:
So, we'll do things differently, and use a single, simpler, aggregate, calculating the data with the "Include itself" option turned on. If it's off, we'll adjust the data on the client side. To do that, we'll need another piece of data – the status ID for the current issue, but that can be provided by Structure itself, and the overhead of requiring it is less than that of a separate aggregate. AttributeSpec for Status BarOnce we understood which attributes will our JavaScript code need, we have to define or find the appropriate attribute specifications for it. Our status bar is going to be a new attribute, so we need to create an AttributeSpec. The ID for this spec should be something unique to our add-on. And the format should be a generic
We don't need any parameters for this attribute specification – regardless of column configuration, we'll always load the same attribute. The value will be the map from the Status ID to the number of cases that status is encountered in the sub-tree, including the parent issue. As for the status ID of the current row, we'll use Status Bar AttributeNow that we know which attribute we need to implement, let's write a loader of that attribute. A loader is an instance of AttributeLoader that loads specific attributes for a specific request. We need to start by looking for the most convenient base class for our loader. It seems that
As the loader does not have any other parameters, we'll only need a single instance, which we'll keep in a
Our loader will have a dependency on the
The calculation of the result is pretty straightforward. The base class,
Attribute ProviderAttribute providers are registered as modules in the plugin descriptor, and their instances are created by the JIRA module system. If the attribute provider "recognizes" the attribute specification and can serve it, it must return a non-null
When the data provider is ready, we register it in the plugin descriptor.
Client-Side ColumnWe now come to the most visible part of the column – the client-side JavaScript code, responsible for rendering the cells of the Structure grid and showing the column configuration UI. Having almost 400 lines of JavaScript, the code is too long to be reproduced in its entirety. We advise you to download the API examples source code from the API Usage Samples page and open First, we'll take a high-level overview of the API and look at a few common concepts – column specifications, column context, and the metadata. After that we'll discuss each of the API classes and their implementations. API OverviewThe whole API is accessible through the
Column SpecificationsA column specification is a JSON object representing the complete configuration of a Structure widget column. Column specifications are stored as parts of view specifications. Each
Here is an example of a Status Bar column specification.
The Column ContextA column context is a JavaScript object providing various kinds of information about the environment, in which columns and their configurators operate. It is not to be confused with the somewhat similar in purpose, but unrelated
In our column we'll use Requesting and Using MetadataMetadata, in the context of the column API, is any data needed by column types, columns, and configurators to to do their duties, except for attributes. For example, the Status Bar column needs to know the IDs and names of all the issue statuses in order to render tooltips and create presets – this is metadata. Structure provides some metadata by default – the Metadata is requested by overriding one or more of the methods in
The method is supposed to return a JavaScript object. Each key in that object will become a metadata key for obtaining the corresponding result from the column context. In this example, the status-related metadata object will be obtained by calling The values in the returned object are request specifications. Let's look at the request properties:
Different metadata may be required for different operations. Therefore, there are several methods in the API that you can override to request metadata:
Please note that the corresponding type-level metadata is also available to the columns and configurators created by the type. So, for example, there is no need to issue the same requests in both Structure will delay loading the metadata for as long as possible. For example:
Structure guarantees that the metadata request will be completed by the time it calls your type, column, and configurator methods (obviously, except for the
|
var StatusBarConfigurator = api.subClass('StatusBarConfigurator', api.ColumnConfigurator, { init: function() { this.spec.key = COLUMN_KEY; this.spec.params || (this.spec.params = {}); }, getColumnTypeName: function() { return AJS.I18n.getText("sbcolumn.name"); }, getGroupKey: function() { return GROUP_KEY; }, getOptions: function() { return [new StatusesOption({ configurator: this }), new IncludeItselfOption({ configurator: this })]; } }); |
The constructor, init()
simply sanitizes the current column specification.
getColumnTypeName()
returns the human-readable name for the column type. This name is used in the "Type" drop-down of the column configuration panel. You can also override getDefaultColumnName()
to generate column names if the type name cannot always be used as the default column name.
getGroupKey()
returns the key of the group in the "Add Column" menu that will contain this preset. See the sections on ColumnType
and column groups below.
getOptions()
creates and returns an array of ColumnOption
instances that create input controls for the column configuration panel and route events. Please note how the configurator instance is passed to each option's constructor – this is crucial. The order of the options in the resulting array is also important – the rows of the configuration panel will be created in that order.
Although the methods of StatusBarConfigurator
always return the same values, this is not a requirement. The result of any of the methods can depend on the current column specification (this.spec
) and metadata.
ColumnOption
Each api.ColumnOption
instance is responsible for editing a single logical "part" of the column specification, and corresponds to a single "row" of the column configuration panel. The option creates the actual input elements and sets up event handlers to transfer the values between the inputs and its column specification. An option can hide itself if it's not applicable to the current specification. Also, each option can prohibit saving the column configuration if it considers the current specification invalid – see isInputValid()
method in the class reference.
Status Bar column has two options:
StatusesOption
is responsible for status selection, colors, and ordering. It "owns" the statuses
and colors
arrays of a Status Bar column specification. This option is somewhat more involved than the next one, but you can still refer to its source code in sbcolumn.js
.IncludeItselfOption
is responsible for the "Include itself" checkbox and "owns" the includeItself
specification parameter. This is one of the simplest options imaginable, so we'll look at its code in detail.var IncludeItselfOption = api.subClass('IncludeItselfOption', api.ColumnOption, { createInput: function(div$) { this.checkbox$ = div$.append( AJS.template('<div class="checkbox"><label><input type="checkbox"> {label}</label></div>') .fill({ label: AJS.I18n.getText("sbcolumn.include-itself") }) .toString()).find('input'); var params = this.spec.params; this.checkbox$.on('change', function() { if ($(this).is(':checked')) { params.includeItself = true; } else { delete params.includeItself; } div$.trigger('notify'); }); }, notify: function() { this.checkbox$.prop('checked', !!this.spec.params.includeItself); return true; } }); |
Because the option class specifies no title
and doesn't override createLabel()
, there is no label to the left of the checkbox.
The createInput()
method creates the checkbox and sets up event handling. It is passed a jQuery object to append the input elements to.
Please note that Structure column configuration panels use the AUI Forms HTML layout (with modified CSS styles). You should use the same layout in your HTML code to make your options look consistent with Structure's. In the example above, the checkbox is wrapped in a <div class="checkbox">
element to comply with AUI Forms.
Also note how the change
event handler of the checkbox modifies the current specification parameters and always triggers a notify
event on the provided jQuery object. These are the crucial parts of the option contract.
The notify()
method is called whenever the current specification changes. Its job is to transfer the data in the opposite direction – from the specification to the input elements. This method also decides whether the option is applicable – if it returns a "falsy" value, the option's row on the configuration panel is hidden from the user.
ColumnType
The api.ColumnType
class is the main entry point used by the Structure plugin to call your client-side column code. A column type instance creates column presets, columns, and configurators. To find the complete source code for the Status Bar column type, please open sbcolumn.js
from the API example sources in your favorite editor and scroll to the StatusBarType
class definition.
The getMetadataRequests()
method declares the column-level metadata request to load the available issue statuses from JIRA. See Requesting and Using Metadata above for details.
The createSwitchTypePreset()
method creates a single column specification, which is used as a preset when the user selects our type in the "Type" drop-down on the column configuration panel.
Note the call to the isAvailable()
function that checks that the preset is needed for the primary panel and that the status metadata is indeed available. If that check fails, the method returns null
, making it impossible to switch to the Status Bar column type. You can try it yourself – open the Search Result secondary panel, add any column to it and try to change its column type. You should see that the Status Bar type is not available.
The switching preset doesn't have to be fully configured, because the configuration panel is already open when it's used. However, because the Status Bar column configuration is quite complex, we make an extra effort and pre-populate the preset with all the known statuses and some default colors for them. This way the user will quickly see what a status bar looks like without having to configure anything at all. This tactic can be useful for other columns with a lot of parameters.
The createAddColumnPresets()
method creates an array of column specifications that will be used as presets in the "Add Column" menu. Unlike the "switch" preset above, these presets must be completely configured. Like createSwitchTypePreset()
, this method calls isAvailable()
first, so a Status Bar column cannot be added to a secondary Structure panel.
Because the "Add Column" menu is the first place where the user discovers your column type, it would be best if your presets are interesting and cover the whole range of the type's functionality. It's not easy to be creative with the Status Bar column though, unless we know the semantics of statuses, which can be arbitrary. So, for simplicity StatusBarType
adds only a single preset to the "Add Column" menu, reusing the "switch" preset, which is fully configured.
Besides the usual key
, name
, and params
, the "add" presets can have two special properties:
presetName
is a string that specifies the name of the preset in the "Add Column" menu. This name will be used only in the menu, the added column will have either the name
from the specification or the default name generated for it. If omitted, the column name will be used as the preset name.shouldOpenConfigurator
– if this flag is set to true
, the column configuration panel will open immediately after adding the column with this preset. This can be used to create a "Custom..." kind of preset that lets the user explore the available options.The createColumn()
and createConfigurator()
methods return a Column
or a ColumnConfigurator
for the given specification, respectively. The methods are similar – they check whether the type is available and the given specification is valid, and if both checks succeed, they instantiate the appropriate subclass. Please note how the column context and the specification are passed to the constructors, this is crucial.
Finally, at the end of the script we instantiate and register our column type, making it available to Structure:
api.registerColumnType(new StatusBarType(), COLUMN_KEY); |
Structure will use our column type instance to handle the columns with the given key. You can also pass an array of keys as the second argument, to associate your type with more than one column key.
Column groups are used to organize column presets in the "Add Column" menu. Each group has a string key and a human-readable name. Column configurator's getGroupKey()
method should return the appropriate group key for its preset specification.
Structure specifies four column groups for its built-in columns – fields
, icons
, totals
, and progress
. For the Status Bar column we will register a separate column group:
api.registerColumnGroup({ groupKey: GROUP_KEY, title: AJS.I18n.getText("sbcolumn.name"), order: 1000 }); |
The order
parameter determines the position of the group within the menu. The higher the order, the lower the group will be. Structure's predefined groups have order between 100 and 400, inclusive.
You need to register your JavaScript and CSS code as a web resource in the plugin descriptor. The Status Bar column has no CSS of its own, and all of its JavaScript code is in a single file, sbcolumn.js
. Because we use the Structure JavaScript API and the AJS.template()
function from the Atlassian API, we need to declare two dependencies. We also declare a resource transformation to make AJS.I18n.getText()
calls work.
<web-resource key="wr-sbcolumn" name="web-resource:Status Bar Column"> <dependency>com.atlassian.auiplugin:ajs</dependency> <dependency>com.almworks.jira.structure:widget</dependency> <transformation extension="js"> <transformer key="jsI18n"/> </transformation> <resource type="download" name="sbcolumn.js" location="js/sbcolumn/sbcolumn.js"/> <context>structure.widget</context> </web-resource> |
We use structure.widget
web resource context to make our JavaScript (and CSS, if we had any) load on Structure Board. It also works for the Structure's Dashboard Gadget. However, if you'd like your column to work on other pages – Project page, Issue page or Agile Board page, you need to include other web contexts too – see Loading Additional Web Resources For Structure Widget.
Any structure can be exported into printable HTML and Microsoft Excel formats. Exporting is different from rendering the Structure widget in several aspects:
It is because of these differences, that the exporting architecture and APIs are different from their widget rendering counterparts, being simpler in some aspects and more complex in others, while quite similar overall, sometimes making it non-trivial to avoid "repeating yourself".
Please refer to the javadocs for an overview of the export API and SPI. In short, to export a column, you need to write and register an export renderer provider, that would recognize the column specification and return an export renderer instance for the given column and export format. The returned renderer will then be given an export column instance to configure and export cell instances to render the values. The export context and export row instances will provide all the data, including the required attributes.
Speaking of the interfaces that must be implemented, ExportRendererProvider
is analogous to AttributeLoaderProvider
, and ExportRenderer
is a mixture of AttributeLoader
and the client-side Column
.
The main difficulty with export is having different output formats with different features. For example, if you have a method for converting a value to HTML, you could reuse it for the printable HTML export. But when exporting to Excel, HTML support is very limited, and if your values correspond to one of Excel's data types, e.g. date, you need to set an appropriate column style. On the other hand, if you have a simple plain-text column, the format doesn't matter – you can have a single export renderer that calls setText()
on any type of cell.
The export SPI is flexible, and allows you to use different strategies for different column types. There are three basic kinds of export renderer providers.
ExportCell
and ExportColumn
interfaces. Though limited, such a provider will work for any other export format that may be added in the future.Exploring the extremes, we will create two export renderer providers for the Status Bar column. The first will be a generic provider, that will present the data as plain text instead of drawing a progress bar. The second one will be an advanced Excel provider that will use the underlying low-level Apache POI API to draw pseudo-graphic progress bars in Excel cells.
The StatusBarRendererProvider
class in the status-bar-column
example plugin source contains both the generic provider and its renderer. The code is quite long, but that's mostly due to defensive checks and the general verbosity of Java. The operation of both the provider and the renderer is quite straight-forward.
The provider's getColumnRenderer()
method does the following:
statuses
array and the includeItself
flag from the specification parameters. These are needed for rendering.StatusBarRenderer
inner class, passing it the column name and parameters.The renderer has prepare()
method that lets it specify which attributes it will need loaded to do the export. Like in StatusBarColumn
, we request our histogram-based custom attribute and status for the current row.
The renderer's configureColumn()
method sets the column name by calling setText()
on the given column's header cell.
The renderer's renderCell()
method does the following:
StringBuilder
.setText()
on the given cell.Here is the module declaration for the generic renderer provider. Note that it specifies the column key, but no export format.
<structure-export-renderer-provider key="erp-sbcolumn" name="export-renderer:Status Bar Column Provider" class="com.almworks.jira.structure.sbcolumn.StatusBarRendererProvider"> <column-key>com.almworks.jira.structure.sbcolumn</column-key> </structure-export-renderer-provider> |
The StatusBarExcelProvider
class contains the advanced Excel renderer and the corresponding provider.
The provider's getColumnRenderer()
method is very similar to the generic provider's, with two additions:
MS_EXCEL
;colors
array from the specification parameters, as the renderer will use those (or similar) colors for the progress bar.The renderer's prepare()
and configureColumn()
methods are the same as the generic version. The renderCell()
method begins in a similar way, by extracting the data map and adjusting it for the "Include itself" option, if needed.
The interesting part is the actual rendering. The pseudo-graphic "progress bar" that the renderer creates is a string of 30 "pipe" characters, split into colored stripes with lengths proportional to issue counts. ExcelCell
provides no support for rich text formatting (besides setRichTextFromHtml()
, which is not up to the task), but we can access the lower-level API, Apache POI HSSF, by obtaining the underlying POI objects from ColumnContext.getObject()
using the keys from ColumnContextKeys.Excel
.
The code that distributes the 30 characters among the stripes is ported from sbcolumn.js
. To completely understand how the rich text part works, you'll need some knowledge of the POI HSSF API, which is quite complex and outside of the scope of this document. Please refer to the POI documentation and the StatusBarExcelProvider
source code for more information.
The module declaration for the Excel renderer provider is given below. Note that it specifies both a column key and an export format, thus overriding the generic provider for the Excel format.
<structure-export-renderer-provider key="erp-sbcolumn-excel" name="export-renderer:Status Bar Column Excel Provider" class="com.almworks.jira.structure.sbcolumn.StatusBarExcelProvider"> <column-key>com.almworks.jira.structure.sbcolumn</column-key> <export-format>ms-excel</export-format> </structure-export-renderer-provider> |