Validate grid data on transition



Problem

You have a table grid with the data that has to be validated on issue transition.

Solution

Since Table Grid Editor does not yet support specifying of the custom validation rules, it can be done now using Java API (available since TGE v.1.18.0) and ScriptRunner. We will implement scripted validator where check if grid data matches our requirements.

Environment

  • Install the Script Runner add-on from the Atlassian marketplace

  • Create a workflow with a transition 'All done'

    • This transition should have a dialog box containing the grid


  • Add a validator function of type 'Script Validator' and choose 'Custom Script validator'

  • Point the script file parameter to the script location. 

    • you can use relative paths but this goes beyond the scope of this faq.

    • using a script file instead of an inline script allows you to easily change the content, without editing the workflow.

Script

We have two Java API calls that can be usefull for validation of the grid data during the transition: Grid Row Count in Edit Mode and Read Grid Data in Edit Mode.

See examples of usage of the Java API here: Java API: Groovy script exampleshttps://wiki.idalko.com/docs/overview.action.

Let's suppose, that you don't want to allow empty grids. The validator script cab be built around the Grid Row Count in Edit Mode call.

Validate empty grids
/** * Since TGE 1.19 only! Can be used only as scripted validator of issue transition. * Cannot be run from the Script Console. */ import com.atlassian.crowd.embedded.api.User import com.atlassian.jira.component.ComponentAccessor import com.atlassian.jira.issue.CustomFieldManager import com.atlassian.jira.issue.fields.CustomField import com.atlassian.jira.security.JiraAuthenticationContext import com.atlassian.jira.user.ApplicationUser import com.atlassian.plugin.PluginAccessor import com.opensymphony.workflow.InvalidInputException // find TGE custom field String tgeCustomFieldName = "TGE_TEST"; CustomFieldManager customFieldManager = ComponentAccessor.getCustomFieldManager(); CustomField tgeCustomField = customFieldManager.getCustomFieldObjectByName(tgeCustomFieldName); String tgeCustomFieldId = tgeCustomField.getId(); // get user PluginAccessor pluginAccessor = ComponentAccessor.getPluginAccessor(); JiraAuthenticationContext jiraAuthenticationContext = ComponentAccessor.getOSGiComponentInstanceOfType(JiraAuthenticationContext.class); Object userObject = jiraAuthenticationContext.getLoggedInUser(); User user = userObject instanceof ApplicationUser ? ((ApplicationUser) userObject).getDirectoryUser() : (User) userObject; // get row count Class dataManagerClass = pluginAccessor.getClassLoader().findClass("com.idalko.jira.plugins.igrid.api.data.TGEGridTableDataManager"); def tgeGridDataManager = ComponentAccessor.getOSGiComponentInstanceOfType(dataManagerClass); Integer rowCount = 0; StringBuilder result = new StringBuilder(); try { rowCount = tgeGridDataManager.getRowCountInEditMode(issue, tgeCustomField, user); result.append("TGE row count: " + rowCount); } catch (Exception e) { result.append("Grid ID=" + tgeCustomFieldId + " row count for issue ID=" + issue.getId() + " cannot be retrieved: " + e.getMessage() + "\n"); } // check if the grid is empty if (rowCount == 0) { // throw an exception with the field ID and an error message for TGE field // it will prevent transition with the noted error, user will stay on the transition screen invalidInputException = new InvalidInputException(tgeCustomFieldId, "Add at least one record to TGE grid to proceed"); throw invalidInputException; } println(result.toString()); return result.toString();

If case if you have more specific requirements to the data of the specific columns, use Read Grid Data in Edit Mode call like the following script.

Validate grid column values
/** * Since TGE 1.19 only! Can be used only as scripted validator of issue transition. * Cannot be run from the Script Console. */ import com.atlassian.crowd.embedded.api.User import com.atlassian.jira.component.ComponentAccessor import com.atlassian.jira.issue.CustomFieldManager import com.atlassian.jira.issue.fields.CustomField import com.atlassian.jira.security.JiraAuthenticationContext import com.atlassian.jira.user.ApplicationUser import com.atlassian.plugin.PluginAccessor import com.opensymphony.workflow.InvalidInputException // find the TGE custom field String tgeCustomFieldName = "TGE_TEST"; CustomFieldManager customFieldManager = ComponentAccessor.getCustomFieldManager(); CustomField tgeCustomField = customFieldManager.getCustomFieldObjectByName(tgeCustomFieldName); Long tgeCustomFieldId = tgeCustomField.getIdAsLong(); // get user PluginAccessor pluginAccessor = ComponentAccessor.getPluginAccessor(); JiraAuthenticationContext jiraAuthenticationContext = ComponentAccessor.getOSGiComponentInstanceOfType(JiraAuthenticationContext.class); Object userObject = jiraAuthenticationContext.getLoggedInUser(); User user = userObject instanceof ApplicationUser ? ((ApplicationUser) userObject).getDirectoryUser() : (User) userObject; // read grid data Class dataManagerClass = pluginAccessor.getClassLoader().findClass("com.idalko.jira.plugins.igrid.api.data.TGEGridTableDataManager"); def tgeGridDataManager = ComponentAccessor.getOSGiComponentInstanceOfType(dataManagerClass); List<Map<String, Object>> gridDataList = new ArrayList<Map<String, Object>>(); StringBuilder result = new StringBuilder(); try { def gridData = tgeGridDataManager.readGridDataInEditMode(issue.getId(), tgeCustomFieldId, null, null, 0, 10, user); gridDataList = gridData.getValues(); result.append("Grid ID=" + tgeCustomFieldId + " data in edit mode: " + gridDataList); } catch (Exception e) { result.append("Grid ID=" + tgeCustomFieldId + " data in edit mode cannot be retrieved: " + e.getMessage() + "\n"); } // check if grid is empty if (gridDataList.isEmpty()) { // throw an exception with the field ID and an error message for TGE field // it will prevent transition with the noted error, user will stay on the transition screen invalidInputException = new InvalidInputException(tgeCustomFieldId, "Add at least one record to TGE grid to proceed") throw invalidInputException; } // check if any row has an 'Open' status selected for (int rowNumber = 0; rowNumber < gridDataList.size(); rowNumber++) { Map<String, Object> rowData = gridDataList.get(rowNumber); Map<String, Object> status = rowData.get( "istatus" // name of the column from TGE configuration ); String statusName = status.get( "name" // pre-defined property for 'list', 'userlist', 'radio' column types ); if ("Open" // name of the status I want to forbid .equals(statusName)) { // throw an exception with the field ID and an error message for TGE field // it will prevent transition with the noted error, user will stay on the transition screen invalidInputException = new InvalidInputException(tgeCustomFieldId, "Row #" + (rowNumber + 1) + " of TGE grid has an 'Open' status selected"); throw invalidInputException; } } println(result.toString()); return result.toString();