Use the table grid editor to track subtasks (and get notified about it)
Introduction
Use TGE as a storage of simple sub-tasks and get email notification on change of the task assignee
This article details out how to setup a table grid to track 'sub' tasks, and be notified when something is assigned to yourself. It is using Script Listeners (from ScriptRunner) to send out the notifications.
The table grid where the subtasks are created
The Notifications sent by the Script Listener
Setup the environment
Configure a grid to track tasks
Prepare a script to send out notifications
Configure a script listener to send out notifications
Configure a grid
This configuration makes use of such features of TGE as userlists, column hiding.
gd.columns=isummary,iassignee,previousiassignee,istatus,idue
gd.tablename=actions
gd.ds=jira
col.isummary=Summary
col.isummary.name=summary
col.isummary.type=string
col.isummary.required=true
col.isummary.maxLength=128
col.isummary.width=400
col.iassignee=Assignee
col.iassignee.type=userlist
col.iassignee.required=true
col.iassignee.autocomplete=true
col.iassignee.width=100
col.previousiassignee=previousiassignee
col.previousiassignee.type=string
col.previousiassignee.hidden=true
col.istatus=Status
col.istatus.type=list
col.istatus.list.size=2
col.istatus.name1=Open
col.istatus.value1=O
col.istatus.name2=Done
col.istatus.value2=D
col.istatus.width=60
col.istatus.defaultValue=O
col.idue=Date due
col.idue.type=date
col.idue.defaultDate = +1w
Create the TGE Listener
Install latest ScriptRunner add-on, create a file "TgeListener.groovy" with the following content in the "$JIRA_HOME/scripts/com/idalko/scripts" directory.
TgeListener.groovy
import com.atlassian.crowd.embedded.api.User
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.config.properties.APKeys
import com.atlassian.jira.config.properties.ApplicationProperties
import com.atlassian.jira.event.issue.AbstractIssueEventListener
import com.atlassian.jira.event.issue.IssueEvent
import com.atlassian.jira.issue.CustomFieldManager
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.fields.CustomField
import com.atlassian.jira.security.JiraAuthenticationContext
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.user.UserUtils
import com.atlassian.mail.Email
import com.atlassian.mail.queue.MailQueue
import com.atlassian.mail.queue.SingleMailQueueItem
import com.atlassian.plugin.PluginAccessor
import org.apache.commons.lang3.ObjectUtils
import org.apache.log4j.Logger
import org.ofbiz.core.entity.GenericValue
class TgeListener extends AbstractIssueEventListener {
// configuration
private static final String TGE_FIELD_NAME = "TGE";
private static final String TRACKED_USERLIST_COLUMNS_NAME = "iassignee";
private static final String PREVIOUS_USERLIST_COLUMNS_NAME = "previousiassignee";
private static final Logger LOG = Logger.getLogger("com.idalko.scripts");
private final CustomFieldManager customFieldManager;
private final PluginAccessor pluginAccessor;
private final JiraAuthenticationContext jiraAuthenticationContext;
private final ApplicationProperties applicationProperties;
private final MailQueue mailQueue;
private final def tgeGridDataManager;
public TgeListener() {
customFieldManager = get(CustomFieldManager.class);
pluginAccessor = get(PluginAccessor.class);
jiraAuthenticationContext = get(JiraAuthenticationContext.class);
Class dataManagerClass = pluginAccessor.getClassLoader().findClass("com.idalko.jira.plugins.igrid.api.data.TGEGridTableDataManager");
tgeGridDataManager = get(dataManagerClass)
applicationProperties = get(ApplicationProperties.class);
mailQueue = get(MailQueue.class);
}
private <T> T get(Class<T> clazz) {
return ObjectUtils.firstNonNull(
ComponentAccessor.getComponent(clazz),
ComponentAccessor.getComponentOfType(clazz),
ComponentAccessor.getOSGiComponentInstanceOfType(clazz)
);
}
@Override
void issueUpdated(IssueEvent event) {
Set<Long> updatedCustomFieldIds = getUpdatedCustomFieldsIds(event);
LOG.debug("Updated custom fields IDs are: " + updatedCustomFieldIds);
CustomField tgeCustomField = customFieldManager.getCustomFieldObjectByName(TGE_FIELD_NAME);
Long tgeCustomFieldId = tgeCustomField.getIdAsLong();
LOG.debug("TGE custom fields ID is: " + tgeCustomFieldId);
if (updatedCustomFieldIds.contains(tgeCustomFieldId)) {
Issue issue = event.getIssue()
processTgeFieldChange(issue, tgeCustomFieldId);
}
}
private Set<Long> getUpdatedCustomFieldsIds(IssueEvent event) {
List<GenericValue> changeItems = event.getChangeLog().getRelated("ChildChangeItem");
Set<Long> updatedCustomFields = new HashSet<Long>();
for (GenericValue changeItem : changeItems) {
String changeType = changeItem.getString("fieldtype");
if (changeType.equalsIgnoreCase("custom")) {
String customFieldName = changeItem.getString("field");
CustomField customFieldObject = customFieldManager.getCustomFieldObjectByName(customFieldName);
Long customFieldId = customFieldObject.getIdAsLong()
updatedCustomFields.add(customFieldId);
}
}
return updatedCustomFields;
}
private void processTgeFieldChange(Issue issue, long tgeCustomFieldId) {
User user = getCurrentUser();
Long issueId = issue.getId();
def result = tgeGridDataManager.readGridData(issueId, tgeCustomFieldId, null, null, 0, 10, user);
List<Map<String, Object>> gridData = result.getValues();
for (Map<String, Object> row : gridData) {
String selectedUserName = getSelectedUserNameFromRow(row);
String previousUserName = getPreviousUserNameFromRow(row);
if (selectedUserName != null && !selectedUserName.equals(previousUserName)) {
sendEmailNotificationToUser(selectedUserName, issue);
setPreviousUserEqualToSelected(row, issueId, tgeCustomFieldId, user);
}
}
}
private User getCurrentUser() {
Object userObject = jiraAuthenticationContext.getLoggedInUser();
User user = userObject instanceof ApplicationUser ? ((ApplicationUser) userObject).getDirectoryUser() : (User) userObject;
return user;
}
private String getSelectedUserNameFromRow(Map<String, Object> row) {
Map<String, String> selectedUser = (Map<String, String>) row.get(TRACKED_USERLIST_COLUMNS_NAME)
String selectedUserName = selectedUser.get("value");
return selectedUserName;
}
private String getPreviousUserNameFromRow(Map<String, Object> row) {
return row.get(PREVIOUS_USERLIST_COLUMNS_NAME);
}
private void sendEmailNotificationToUser(String userName, Issue issue) {
// prepare data
User user = UserUtils.getUser(userName);
String userEmail = user.getEmailAddress();
String baseUrl = applicationProperties.getString(APKeys.JIRA_BASEURL);
String issueKey = issue.getKey();
String issueLink = baseUrl + "/browse/" + issueKey;
String userDisplayName = user.getDisplayName();
// build email
Email email = new Email(userEmail);
email.setSubject("TGE notification");
email.setBody("Hi " + userDisplayName + "! A new task has been assigned to you in issue " + issueKey
+ "! Please visit <a href=\"" + issueLink + "\">" + issueKey + "</a>.");
email.setMimeType("text/html");
// send it
SingleMailQueueItem mailItem = new SingleMailQueueItem(email);
mailQueue.addItem(mailItem);
}
private void setPreviousUserEqualToSelected(Map<String, Object> row, Long issueId, Long tgeCustomFieldId, User user) {
List changes = new ArrayList();
// get row values
Long rowId = (Long) row.get("id");
String selectedUserName = getSelectedUserNameFromRow(row);
// prepare new values
Map<String, Object> newRowValues = new HashMap<String, Object>();
newRowValues.put(PREVIOUS_USERLIST_COLUMNS_NAME, selectedUserName);
// create changeset
Class changeSetClass = pluginAccessor.getClassLoader().loadClass("com.idalko.jira.plugins.igrid.api.data.dto.TGEChangeSet");
def changeSet = changeSetClass.newInstance();
changeSet.setRowId(rowId);
changeSet.setValues(newRowValues);
changes.add(changeSet);
// update row
tgeGridDataManager.applyChanges(issueId, tgeCustomFieldId, changes, user);
}
}
Then Configure Scripted Listener
Usage
Create new rows in the grid, edit existing ones. On the change of the value of the "Assignee" column new Assignee will get a TGE email notification like this
Notification
Subject: TGE notification
Body: Hi Samuel Stone! A new task has been assigned to you in issue TGE-1! Please visit http://seriouscopr.com/browse/TGE-1.