Available: | Activity Streams 4.0 and later. |
Activity streams are great. They funnel lots of information into an easily accessible view to allow users an easy way to keep up with the latest activities. They are even better if they allow the users to act on the items they see, right there on the stream itself.
As a plugin developer, you can use the Atlassian Activity Streams 4 API to add your own inline actions whenever you please. The API is entirely in JavaScript.
The instructions below show you how to write your own pluggable action for Atlassian Activity Streams.
A sample pluggable inline action plugin can be found here.
You need to know a little bit about the Atlassian Plugin SDK and a small amount of experience in writing plugins.
Follow the Atlassian plugin SDK guide to create a plugin skeleton.
Your pluggable action should be implemented as an anonymous function. This function will be invoked upon Activity Streams loading the file.
The most important part is calling ActivityStreams.registerAction(type, element, sortBy)
. This function has the potential to add a link (or any other arbitrary HTML) to the bottom of Activity Streams entries.
For example, to add a 'Trigger Build' inline action, you could implement the following build-trigger.js
:
1 2/** * Registers a "Trigger build" action against any feed items with a "build" type. * * Creates a link which triggers a build for the specified plan. */ (function() { /** * Builds a "Trigger" link that triggers the action * * @method buildTriggerBuildLink * @param {Object} feedItem Object representing the activity item * @return {HTMLElement} */ function buildTriggerBuildLink(feedItem) { ... } // Registers the trigger action for any builds in the feed ActivityStreams.registerAction('job', buildTriggerBuildLink, 5); })();
registerAction
takes three parameters:
issue
, comment
, and file
(attachment)page
, article
(blog), comment
, file
(attachment), space
, personal-space
, and status
(user status)changeset
review
and comment
job
activity:object-type
belonging to your entry
's activity:object
.feedItem
parameter containing information about the specific Activity Streams entry.For the 'Trigger Build' example, we only want to register this action towards feedItem
items of type build
. Now that you can add hyperlinks to build entries, let's hook up the link to actually do something.
1 2... /** * Adds a Bamboo build to the queue. * * @method addBuildToQueue * @param {Event} e Event object */ function addBuildToQueue(e) { var activityItem = AJS.$(e.target).closest('div.activity-item'), triggerUrl; if (e.data && e.data.feedItem) { triggerUrl = e.data.feedItem.links['http://streams.atlassian.com/syndication/build-trigger']; } else { return; } e.preventDefault(); AJS.$.ajax({ type : 'POST', url : ActivityStreams.InlineActions.proxy(triggerUrl) }); } /** * Builds a "Trigger" link that triggers the action * * @method buildTriggerBuildLink * @param {Object} feedItem Object representing the activity item * @return {HTMLElement} */ function buildTriggerBuildLink(feedItem) { //if no build-trigger link exists in the feed item, do not bind the entry to a trigger handler if (!feedItem.links['http://streams.atlassian.com/syndication/build-trigger']) { return; } var link = AJS.$('<a href="#" class="activity-item-build-trigger-link"></a>') .text('Trigger Build') .bind('click', {feedItem: feedItem}, addBuildToQueue); return link; } ...
The above code adds a Trigger Build
link to any build
entry specified as being capable of executing builds. For this example, the Bamboo server determines whether or not a build is capable of being triggered (disabled builds cannot be triggered) and communicates this capability by including the link in the feedItem
. This example is still as minimal as possible. It does not yet i18n text nor offer any user-visible feedback.
When the newly created link is clicked, the addBuildToQueue()
event handler is executed, POSTing an AJAX call to the Bamboo REST API.
You probably noticed that instead of hitting triggerUrl
directly, our POST hits ActivityStreams.InlineActions.proxy(triggerUrl)
. Since this POST request originates from a JIRA/Confluence instance to a Bamboo instance, we cannot make a cross-machine request from JavaScript. Instead, we will need to proxy this URL to ensure the request's success. GET, POST, and PUT types are supported when proxying.
This part is easy. Activity Streams 4.0 defines a new plugin module descriptor to use via the <streams-action-handlers>
tag.
Have your JavaScript all ready to go? In order to register it as an Activity Streams action handler, all you need to do is define it as a resource within a <streams-action-handlers>
module.
1 2<atlassian-plugin key="com.atlassian.streams.bamboo.inlineactions" name="Bamboo Streams Inline Actions Plugin" pluginsVersion="2"> ... <streams-action-handlers key="actionHandlers"> <resource type="download" name="build-trigger.js" location="/js/inline-actions/build-trigger.js"/> </streams-action-handlers> ... </atlassian-plugin>
That's it! Activity Streams will take care of the rest for you.
Once you have your plugin packaged into a JAR, install it on your application containing the Activity Streams gadget (currently either JIRA or Confluence). You do not need to install it on the application involved in the action handle (in the above example, Bamboo).
The logic here is that we want to restrict our applications to running JavaScript installed on their own servers. It would not be secure for JIRA to run JavaScript downloaded from a remote Bamboo server.
Congratulations! You have a functioning pluggable action for your Activity Streams!
Now let's jazz it up a bit and make it better.
You will probably want to internationalise your plugin. And it is easy enough to do, so why not?
First, create your i18n.properties
file. Let's start with just a single i18n property for the hyperlink label.
1 2streams.bamboo.action.trigger.title=Run
Now register your i18n.properties
file with your plugin.
1 2<atlassian-plugin key="com.atlassian.streams.bamboo.inlineactions" name="Bamboo Streams Inline Actions Plugin" pluginsVersion="2"> ... <resource type="i18n" name="bamboo-actions-i18n" location="com.atlassian.streams.bamboo.inline-actions.i18n"/> ... </atlassian-plugin>
To make the internationalized properties available to your JavaScript, you'll want to use Activity Streams' support to transform the properties into a format accessible from your JavaScript. Again in your atlassian-plugin.xml
, let's add to your existing streams-action-handlers
. The new resource name and location (in this case, streams.bamboo.action.i18n.js
and /js/inline-actions/streams.bamboo.i18n.js
should have the filename in the form of <i18n-prefix-pattern>.i18n.js
. For this example, all of this inline action's internationalization properties will begin with streams.bamboo.action
.
1 2<atlassian-plugin key="com.atlassian.streams.bamboo.inlineactions" name="Bamboo Streams Inline Actions Plugin" pluginsVersion="2"> ... <streams-action-handlers key="actionHandlers"> <transformation extension="i18n.js"> <transformer key="action-i18n-transformer" /> </transformation> <resource type="download" name="streams.bamboo.action.i18n.js" location="/js/inline-actions/streams.bamboo.action.i18n.js"/> <resource type="download" name="build-trigger.js" location="/js/inline-actions/build-trigger.js"/> </streams-action-handlers> ... </atlassian-plugin>
Now we're ready to use the i18n properties in your JavaScript. Activity Streams 4.0 has implemented a utility method ActivityStreams.i18n.get(key)
to access internationalized values. Values are fetched from the dynamic JavaScript resource defined in the previous step and cached for future accessibility.
1 2function buildTriggerBuildLink(feedItem) { ... var link = AJS.$('<a href="#" class="activity-item-build-trigger-link"></a>') .text(ActivityStreams.i18n.get('streams.bamboo.action.trigger.title')) .bind('click', {feedItem: feedItem}, addBuildToQueue); ... }
Note: Because the Atlassian plugins system will be looking up a JavaScript file which does not exist (in this case, /js/inline-actions/build-trigger.js
), you will probably want to create an empty file at that location. While this is not a required step, it will avoid any related warnings being output to the system logs.
You will probably want to provide some sort of feedback to the user specifying whether or not your inline action was successful in completing its task. For this task we provide a utility function, statusMessage(activityItem, message, type, additionalEvents)
.
This method takes the following parameters:
activity-item
<div>
used by your action handlerAtlassian Gadgets have some built-in ajax error handling, so if you're issuing an ajax request and using statusMessage()
, you'll want to disable the global error handling by setting global
to false
in the jQuery ajax method.
Now let's add some status messages to our Trigger Build action.
1 2function addBuildToQueue(e) { var activityItem = AJS.$(e.target).closest('div.activity-item'), triggerUrl; if (e.data && e.data.feedItem) { triggerUrl = e.data.feedItem.links['http://streams.atlassian.com/syndication/build-trigger']; } else { ActivityStreams.InlineActions.statusMessage(activityItem, ActivityStreams.i18n.get('streams.bamboo.action.trigger.failure.general'), 'error'); return; } e.preventDefault(); AJS.$.ajax({ type : 'POST', url : ActivityStreams.InlineActions.proxy(triggerUrl), global: false, success : function() { ActivityStreams.InlineActions.statusMessage(activityItem, ActivityStreams.i18n.get('streams.bamboo.action.trigger.success'), 'info'); }, error : function(request) { if (request.rc == 401) { // User triggering build likely does not exist on Bamboo instance, // or trusted apps configuration is not properly set up. ActivityStreams.InlineActions.statusMessage(activityItem, ActivityStreams.i18n.get('streams.bamboo.action.trigger.failure.authentication'), 'error'); } else { ActivityStreams.InlineActions.statusMessage(activityItem, ActivityStreams.i18n.get('streams.bamboo.action.trigger.failure.general'), 'error'); } } }); }
If the POST request completes successfully, statusMessage()
will be invoked with a type of info
. Otherwise, in the case of an error, statusMessage()
will be invoked with a type of failure
.
Let's go even farther now and say that we want the inline action link to disable upon clicking, and then re-enable when the status message fades out. We can add this functionality with the optional additionalEvents
callback parameter. For this example, we will add a hidden <span>
label with identical text next to the trigger link, and will toggle both of their visibilities based on the action's current state. When the action is first invoked, hideTriggerLink()
will hide the link and display the <span>
label. When the action's status message fades out, showTriggerLink()
will redisplay the link and hide the <span>
label.
1 2function addBuildToQueue(e) { ... e.preventDefault(); hideTriggerLink(activityItem); AJS.$.ajax({ type : 'POST', url : ActivityStreams.InlineActions.proxy(triggerUrl), global: false, success : function() { ActivityStreams.InlineActions.infoStatusMessage(activityItem, ActivityStreams.i18n.get('streams.bamboo.action.trigger.success'), function() { showTriggerLink(activityItem); }); }, error : function(request) { if (request.rc == 401) { // User triggering build likely does not exist on Bamboo instance, // or trusted apps configuration is not properly set up. ActivityStreams.InlineActions.errorStatusMessage(activityItem, ActivityStreams.i18n.get('streams.bamboo.action.trigger.failure.authentication'), function() { showTriggerLink(activityItem); }); } else { ActivityStreams.InlineActions.errorStatusMessage(activityItem, ActivityStreams.i18n.get('streams.bamboo.action.trigger.failure.general'), function() { showTriggerLink(activityItem); }); } } }); } /** * Hide the trigger link, showing the non-hyperlinked label instead. * * @method hideTriggerLink * @param {Object} activityItem the .activity-item div */ function hideTriggerLink(activityItem) { activityItem.find('a.activity-item-build-trigger-link').addClass('hidden'); activityItem.find('span.activity-item-build-trigger-label').removeClass('hidden'); } /** * Show the trigger link, hiding the non-hyperlinked label in the process. * * @method showTriggerLink * @param {Object} activityItem the .activity-item div */ function showTriggerLink(activityItem) { activityItem.find('a.activity-item-build-trigger-link').removeClass('hidden'); activityItem.find('span.activity-item-build-trigger-label').addClass('hidden'); } function buildTriggerBuildLink(feedItem) { ... var link = AJS.$('<a href="#" class="activity-item-build-trigger-link"></a>') .text(ActivityStreams.i18n.get('streams.bamboo.action.trigger.title')) .bind('click', {feedItem: feedItem}, addBuildToQueue), label = AJS.$('<span class="activity-item-build-trigger-label hidden"></span>') .text(ActivityStreams.i18n.get('streams.bamboo.action.trigger.title')); return link.add(label); } ...
Even with status messages to indicate successful or failed actions, users get antsy when they initiate an action and don't get immediate visual feedback. So it's a good idea to let the user know that something is happening while waiting for an asynchronous (ajax) request to return. We've provided a mechanism to easily hide and show a throbber to indicate that something is happening.
To hide and show the throbber, simply trigger the beginInlineAction
and completeInlineAction
events (respectively). These can be triggered on any element in the dom, though it's recommended that you trigger them on the target element for your pluggable action.
We recommend using the beforeSend
and complete
callbacks jQuery provides in its ajax
function, but the events can be triggered at any time you feel is appropriate:
1 2function addBuildToQueue(e) { var target = AJS.$(e.target), activityItem = target.closest('div.activity-item'), triggerUrl; if (e.data && e.data.feedItem) { triggerUrl = e.data.feedItem.links['http://streams.atlassian.com/syndication/build-trigger']; } else { ActivityStreams.InlineActions.statusMessage(activityItem, ActivityStreams.i18n.get('streams.bamboo.action.trigger.failure.general'), 'error'); return; } e.preventDefault(); AJS.$.ajax({ type : 'POST', url : ActivityStreams.InlineActions.proxy(triggerUrl), global: false, beforeSend: function() { target.trigger('beginInlineAction'); }, complete: function() { target.trigger('completeInlineAction'); } }); }
You can also check out the source code for the example used in this tutorial.
After completing all of the above steps and enhancements, we have a fully functional pluggable inline action for our Activity Streams! Our final build-trigger.js
is as follows:
1 2/** * Registers a "Trigger build" action against any feed items with a "build" type. * * Creates a link which triggers a build for the specified plan. */ (function() { /** * Adds a Bamboo build to the queue. * * @method addBuildToQueue * @param {Event} e Event object */ function addBuildToQueue(e) { var target = AJS.$(e.target), activityItem = target.closest('div.activity-item'), triggerUrl; if (e.data && e.data.feedItem) { triggerUrl = e.data.feedItem.links['http://streams.atlassian.com/syndication/build-trigger']; } else { ActivityStreams.InlineActions.errorStatusMessage(activityItem, ActivityStreams.i18n.get('streams.bamboo.action.trigger.failure.general')); return; } e.preventDefault(); hideTriggerLink(activityItem); AJS.$.ajax({ type : 'POST', url : ActivityStreams.InlineActions.proxy(triggerUrl), global: false, beforeSend: function() { target.trigger('beginInlineAction'); }, complete: function() { target.trigger('completeInlineAction'); }, success : function() { ActivityStreams.InlineActions.infoStatusMessage(activityItem, ActivityStreams.i18n.get('streams.bamboo.action.trigger.success'), function() { showTriggerLink(activityItem); }); }, error : function(request) { if (request.rc == 401) { // User triggering build likely does not exist on Bamboo instance, // or trusted apps configuration is not properly set up. ActivityStreams.InlineActions.errorStatusMessage(activityItem, ActivityStreams.i18n.get('streams.bamboo.action.trigger.failure.authentication'), function() { showTriggerLink(activityItem); }); } else { ActivityStreams.InlineActions.errorStatusMessage(activityItem, ActivityStreams.i18n.get('streams.bamboo.action.trigger.failure.general'), function() { showTriggerLink(activityItem); }); } } }); } /** * Hide the trigger link, showing the non-hyperlinked label instead. * * @method hideTriggerLink * @param {Object} activityItem the .activity-item div */ function hideTriggerLink(activityItem) { activityItem.find('a.activity-item-build-trigger-link').addClass('hidden'); activityItem.find('span.activity-item-build-trigger-label').removeClass('hidden'); } /** * Show the trigger link, hiding the non-hyperlinked label in the process. * * @method showTriggerLink * @param {Object} activityItem the .activity-item div */ function showTriggerLink(activityItem) { activityItem.find('a.activity-item-build-trigger-link').removeClass('hidden'); activityItem.find('span.activity-item-build-trigger-label').addClass('hidden'); } /** * Builds a "Trigger" link that triggers the action * * @method buildTriggerBuildLink * @param {Object} feedItem Object representing the activity item * @return {HTMLElement} */ function buildTriggerBuildLink(feedItem) { //if no build-trigger link exists in the feed item, do not bind the entry to a trigger handler if (!feedItem.links['http://streams.atlassian.com/syndication/build-trigger']) { return; } var link = AJS.$('<a href="#" class="activity-item-build-trigger-link"></a>') .text(ActivityStreams.i18n.get('streams.bamboo.action.trigger.title')) .bind('click', {feedItem: feedItem}, addBuildToQueue), label = AJS.$('<span class="activity-item-build-trigger-label hidden"></span>') .text(ActivityStreams.i18n.get('streams.bamboo.action.trigger.title')); return link.add(label); } // Registers the trigger action for any builds in the feed ActivityStreams.registerAction('job', buildTriggerBuildLink, 5); })();
And our final atlassian-plugin.xml
is:
1 2<atlassian-plugin key="com.atlassian.streams.bamboo.inlineactions" name="Bamboo Streams Inline Actions Plugin" pluginsVersion="2"> <plugin-info> <description>Bamboo Streams Inline Actions Plugin</description> <version>${project.version}</version> <vendor name="Atlassian Software Systems Pty Ltd" url="http://www.atlassian.com/"/> </plugin-info> <streams-action-handlers key="actionHandlers"> <transformation extension="i18n.js"> <transformer key="action-i18n-transformer" /> </transformation> <resource type="download" name="streams.bamboo.action.i18n.js" location="/js/inline-actions/streams.bamboo.action.i18n.js"/> <resource type="download" name="build-trigger.js" location="/js/inline-actions/build-trigger.js"/> </streams-action-handlers> <resource type="i18n" name="bamboo-actions-i18n" location="com.atlassian.streams.bamboo.inline-actions.i18n"/> </atlassian-plugin>
Rate this page: