Jira Plugins fail to load when using a filter


#1

I’ve run into a strange issue with my Jira plugin. We have a plugin that is used to force employees to choose a visibility level for comments they make on issues. To do this, we pop up a dialog when the user adds their comment asking them if they want the comment to be sent to all users, or just developers. We only display this dialog if the issue has a customer support case associated with it (this is important because we have had issues with people accidentally sending important comments to internal users only, leaving the support people wondering what had happened). We also add buttons to a few dialogs that have comment fields so that the user must click the group they’d like their comment to be sent to.

The issue only occurs when I perform the following steps:

  1. I create a custom issue filter
  2. I click to view this filter from the “issues” dropdown on Jira
  3. I select “list view” instead of detail view by clicking the icon on the upper right
  4. I click the title of an issue to view it

When I access a Jira following these steps, my plugin does not load and I am not prompted to choose a visibility level when I comment. I also see no console output mentioning loading any files. Strangely, while the plugin does not load on the page and the comment field on the issue itself is not modified by my plugin, the plugin will still work on the dialogs. When I click to edit the Jira, for example, I see files being loaded in my console and my custom buttons appear on the dialog.

When I refresh the page, my plugin loads properly. When I access the Jira in any other way, it works fine. I have noticed that accessing the Jira in this way adds the “?filter” query parameter to the issue URL, whereas the other methods of accessing it do not. This query parameter seems to cause a different layout to be used… but I don’t understand why the plugin fails to load in this case. And when I refresh, this query parameter is still present, but the plugin starts working.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>com.x.jira.plugins</groupId>
    <artifactId>x-commentLevelPlugin</artifactId>
    <version>2.4</version>

    <organization>
        <name>x</name>
        <url>http://www..com/</url>
    </organization>

    <name>x-commentLevelPlugin</name>
    <description>This is the com.x.jira.plugins:x-commentLevelPlugin plugin for Atlassian JIRA.</description>
    <packaging>atlassian-plugin</packaging>

    <dependencies>
        <dependency>
            <groupId>com.atlassian.jira</groupId>
            <artifactId>jira-api</artifactId>
            <version>${jira.version}</version>
            <scope>provided</scope>
        </dependency>
        <!-- Add dependency on jira-core if you want access to JIRA implementation classes as well as the sanctioned API. -->
        <!-- This is not normally recommended, but may be required eg when migrating a plugin originally developed against JIRA 4.x -->
        <!--
        <dependency>
            <groupId>com.atlassian.jira</groupId>
            <artifactId>jira-core</artifactId>
            <version>${jira.version}</version>
            <scope>provided</scope>
        </dependency>
        -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.10</version>
            <scope>test</scope>
        </dependency>

        <!-- WIRED TEST RUNNER DEPENDENCIES -->
        <dependency>
            <groupId>com.atlassian.plugins</groupId>
            <artifactId>atlassian-plugins-osgi-testrunner</artifactId>
            <version>${plugin.testrunner.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>javax.ws.rs</groupId>
            <artifactId>jsr311-api</artifactId>
            <version>1.1.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.2.2-atlassian-1</version>
        </dependency>

		<!-- Uncomment to use TestKit in your project. Details at https://bitbucket.org/atlassian/jira-testkit -->
		<!-- You can read more about TestKit at https://developer.atlassian.com/display/JIRADEV/Plugin+Tutorial+-+Smarter+integration+testing+with+TestKit -->
		<!--
		<dependency>
			<groupId>com.atlassian.jira.tests</groupId>
			<artifactId>jira-testkit-client</artifactId>
			<version>${testkit.version}</version>
			<scope>test</scope>
		</dependency>
		-->
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>com.atlassian.maven.plugins</groupId>
                <artifactId>maven-jira-plugin</artifactId>
                <version>${amps.version}</version>
                <extensions>true</extensions>
                <configuration>
                    <productVersion>${jira.version}</productVersion>
                    <productDataVersion>${jira.version}</productDataVersion>
                    <jvmArgs>-Xms1g -Xmx2g -XX:MaxPermSize=1g -XX:-UseGCOverheadLimit -server</jvmArgs>
                    <enableQuickReload>true</enableQuickReload>
                    <enableFastdev>false</enableFastdev>
					<!-- Uncomment to install TestKit backdoor in JIRA. -->
					<!--
					<pluginArtifacts>
						<pluginArtifact>
							<groupId>com.atlassian.jira.tests</groupId>
							<artifactId>jira-testkit-plugin</artifactId>
							<version>${testkit.version}</version>
						</pluginArtifact>
					</pluginArtifacts>
					-->
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <properties>
        <jira.version>7.9.1</jira.version>
        <amps.version>5.0.13</amps.version>
        <plugin.testrunner.version>1.2.3</plugin.testrunner.version>
		<!-- TestKit version 5.x for JIRA 5.x, 6.x for JIRA 6.x -->
		<testkit.version>5.2.26</testkit.version>
    </properties>

</project>

atlassian-plugin.xml

<atlassian-plugin key="${project.groupId}.${project.artifactId}" name="${project.name}" plugins-version="2">
    <plugin-info>
        <description>${project.description}</description>
        <version>${project.version}</version>
        <vendor name="${project.organization.name}" url="${project.organization.url}" />
        <param name="plugin-icon">images/pluginIcon.png</param>
        <param name="plugin-logo">images/pluginLogo.png</param>
    </plugin-info>

    <!-- add our i18n resource -->
    <resource type="i18n" name="i18n" location="company-commentLevelPlugin"/>
    
    <!-- add our web resources -->
    <web-resource key="company-commentLevelPlugin-resources" name="company-commentLevelPlugin Web Resources">
        <dependency>com.atlassian.auiplugin:ajs</dependency>
        
        <resource type="download" name="company-commentLevelPlugin.css" location="/css/company-commentLevelPlugin.css"/>
        <resource type="download" name="company-commentLevelPlugin.js" location="/js/company-commentLevelPlugin.js"/>
        <resource type="download" name="images/" location="/images"/>

        <context>company-commentLevelPlugin</context>
        <context>jira.view.issue</context>
    </web-resource>
    
    <!-- publish our component -->
    <component key="myPluginComponent" class="com.company.jira.plugins.commentLevel.MyPluginComponentImpl" public="true">
        <interface>com.company.jira.plugins.commentLevel.MyPluginComponent</interface>
    </component>
    
    <!-- import from the product container -->
    <component-import key="applicationProperties" interface="com.atlassian.sal.api.ApplicationProperties" />
    
</atlassian-plugin>

plugin javascript code:

/* This plugin adds a dialog to Jira that prompts the user to choose a level
 * for a comment after they click the Comment button.
 */


/* Common constants and functions */
var version = 26;
var deg = 1;
var devTesting = false;

var labelForButton1 = 'Case users';
var labelForButton2 = 'Jira users';
var labelForButton3 = 'Developers only';

var hoverTextForButton1 = 'Automatically pushed to the case';
var hoverTextForButton2 = 'Viewable by everyone at the company, but not pushed to case';
var hoverTextForButton3 = 'Only viewable by developers';

// These are the levels defined in Jira, not their relatively clearer labels used on the buttons.
// Note: These levels may instead take the format of "role:10000" wherein the number is the role ID,
// but I'm using the displayed text as the role IDs are mutable.
var levelForButton1 = 'All Users'; // no role ID
var levelForButton2 = 'Users'; // role:10000
var levelForButton3 = 'Developers'; // role:10001

//These are the IDs of dialogs that will have visibility buttons displayed when a user adds a comment
//An 'includes' operation is used here because the workflow-transition ids are generated dynamically with random integers
var dialogIDsToTarget = ['workflow-transition', 'edit-issue-dialog', 'assign-dialog'];

var shouldForceUserToChooseLevel = function () {
    // Since the customField ID is mutable, look for the text instead.
    var issueHasCase = AJS.$('#customfield-tabs .item:contains("Case/s")').length > 0;

    //If a dialog is displayed, determine whether it's a targetted dialog
    if(!!AJS.$('.jira-dialog')[0]) {
        for (var i = 0; i < dialogIDsToTarget.length; i++) {
            if (AJS.$('.jira-dialog').attr('id').indexOf(dialogIDsToTarget[i]) > -1) {
                return true;
            }
        }
        return false;
    }
    // This is a test case for local development.
    //return AJS.$('#issuedetails .item:contains("Medium")').length > 0;
    return issueHasCase || devTesting;
};

var developerLevelAvailable = function () {
    return AJS.$('#commentLevel option:contains(Developers)').length > 0;
};

var resetCommentLevelToDefault = function () {
    AJS.$("#commentLevel").val("");
    AJS.$('span.current-level').html(AJS.format(AJS.params.securityLevelViewableByAll));
};

var updateCommentLevelDropdown = function (newLevel) {
    // All Users, i.e. Push to Case will not have a value
    var commentLevel = AJS.$('#commentLevel option').filter(function () {
        return AJS.$(this).text() === newLevel;
    }).val();

    if (!!commentLevel) {
        AJS.$("#commentLevel").val(commentLevel);
        AJS.$('span.current-level').html(AJS.format(AJS.params.securityLevelViewableRestrictedTo, newLevel));
    } else {
        // Default is All Users, i.e. Push to Case
        resetCommentLevelToDefault();
    }
};

var updateCommentLevelDropdownInWorkflow = function (newLevel) {
    // All Users, i.e. Push to Case will not have a value
    var commentLevel = AJS.$('#commentLevel option').filter(function () {
        return AJS.$(this).text() === newLevel;
    }).val();

    if (!!commentLevel) {
        AJS.$(".jira-dialog-open #commentLevel").val(commentLevel);
        AJS.$('.jira-dialog-open span.current-level').html(AJS.format(AJS.params.securityLevelViewableRestrictedTo, newLevel));
        AJS.$(".jira-dialog-open #commentLevel option[value=\'" + commentLevel + "\']").attr('selected', 'selected');
    } else {
        // Default is All Users, i.e. Push to Case
        AJS.$(".jira-dialog-open #commentLevel").val("");
        AJS.$('.jira-dialog-open span.current-level').html(AJS.format(AJS.params.securityLevelViewableByAll));
        AJS.$(".jira-dialog-open #commentLevel option[value=\'\']").attr('selected', 'selected');
    }
};

/* Constants for the comment level popup dialog */

var template = '<section id="comment-level-dialog" class="aui-dialog2 aui-dialog2-small aui-layer" data-aui-modal="false" data-aui-focus-selector="#dialog-submit-button-1" role="dialog" aria-hidden="true">' +
    '<header class="aui-dialog2-header">' +
    '<h2 class="aui-dialog2-header-main">Comment Visibility</h2>' +
    '<a class="aui-dialog2-header-close">' +
    '<span class="aui-icon aui-icon-small aui-iconfont-close-dialog">Close</span>' +
    '</a>' +
    '</header>' +
    '<div class="aui-dialog2-content">' +
    '<p>Who should see this comment?</p>' +
    '</div>' +
    '<footer class="aui-dialog2-footer">' +
    '<div class="aui-dialog2-footer-actions">' +
    '<button id="dialog-submit-button-1" class="aui-button aui-button-primary" title="' + hoverTextForButton1 + '">' + labelForButton1 + '</button>' +
    '<button id="dialog-submit-button-2" class="aui-button" title="' + hoverTextForButton2 + '">' + labelForButton2 + '</button>' +
    '<button id="dialog-submit-button-3" class="aui-button" title="' + hoverTextForButton3 + '">' + labelForButton3 + '</button>' +
    '</div>' +
    ' </footer>' +
    '</section>';

/* Constants for adding comment level buttons to a dialog, e.g. when changing the workflow state */

var button1 = '<button accesskey="s" class="aui-button aui-button-primary dialog-custom-button" id="workflow-transition-button1" name="Transition" title="' + hoverTextForButton1 + '">' + labelForButton1 + '</button>';
var button2 = '<button accesskey="s" class="aui-button dialog-custom-button" id="workflow-transition-button2" name="Transition" title="' + hoverTextForButton2 + '">' + labelForButton2 + '</button>';
var button3 = '<button accesskey="s" class="aui-button dialog-custom-button" id="workflow-transition-button3" name="Transition" title="' + hoverTextForButton3 + '">' + labelForButton3 + '</button>';

var textToAppendToCommentLevelButton = ' by selecting comment visibility:';

var labelizeDefaultButton = function () {
    var $submitButton = AJS.$('.jira-dialog input[type=submit]');
    if (!$submitButton.hasClass('as-label')) {
        $submitButton.css('display', 'none');
        $submitButton.attr('disabled', 'disabled');
        $submitButton.addClass('as-label');
        var buttonText = $submitButton.val() + ' and push comment to:';
        $submitButton.val(buttonText);
        $submitButton.fadeIn('fast');
    }
};

var buttonizeDefaultButton = function () {
    var $submitButton = AJS.$('.jira-dialog input[type=submit]');
    if ($submitButton.hasClass('as-label')) {
        $submitButton.css('display', 'none');
        $submitButton.removeAttr('disabled');
        $submitButton.removeClass('as-label');
        var buttonText = $submitButton.val().replace(' and push comment to:', '');
        $submitButton.val(buttonText);
        $submitButton.fadeIn('fast');
    }
};

AJS.toInit(function () {
    console.log('AJS.toInit has been called!');

    AJS.$('body').append(template);
    var commentLevelChosen = false;

    if (shouldForceUserToChooseLevel()) {
        AJS.$('#addcomment .security-level').css('display', 'none');
    }

    AJS.$('#issue-comment-add-submit').on('click', function (event) {

        if (!shouldForceUserToChooseLevel()) {
            return;
        }

        if (!!event.originalEvent) {
            // this event was initiated by a user action
            event.preventDefault();
            event.stopPropagation();
            resetCommentLevelToDefault();

            if (developerLevelAvailable()) {
                AJS.$('#dialog-submit-button-3').show();
            } else {
                AJS.$('#dialog-submit-button-3').hide();
            }

            // open dialog to choose new level
            AJS.dialog2('#comment-level-dialog').show();
        } else {
            // this wasn't initiated by a user action
            commentLevelChosen = false;
            setTimeout(function () {
                resetCommentLevelToDefault();
            }, 250);
        }
    });

    // Adjust the visibility level and close the dialog
    AJS.$('.aui-dialog2-footer-actions > button').click(function (event) {

        event.preventDefault();

        // Default to Users level
        var htmlEscapedLabel = levelForButton2;

        if (event.currentTarget.id === 'dialog-submit-button-1') {
            htmlEscapedLabel = levelForButton1;
        } else if (event.currentTarget.id === 'dialog-submit-button-3') {
            htmlEscapedLabel = levelForButton3;
        }

        updateCommentLevelDropdown(htmlEscapedLabel);

        commentLevelChosen = true;

        AJS.dialog2('#comment-level-dialog').hide();
        if (commentLevelChosen) {
            AJS.$('#issue-comment-add-submit').trigger('click', event);
        }
    });

    // Comment level buttons in Workflow dialogs
    //TODO: prevent cancel button from fading
    var addCommentButtonsToWorkflowDialog = function () {

        AJS.$(button1).insertBefore(AJS.$('.jira-dialog-content .buttons a'));
        AJS.$(button2).insertBefore(AJS.$('.jira-dialog-content .buttons a'));
        if (developerLevelAvailable()) {
            AJS.$(button3).insertBefore(AJS.$('.jira-dialog-content .buttons a'));
        }
        AJS.$('.jira-dialog-content .buttons').hide();
        AJS.$('.jira-dialog-content .buttons').fadeIn();
        AJS.$('.throbber').addClass('throbber-left');
    };

    var removeCommentButtonsFromWorkflowDialog = function () {
        AJS.$('.dialog-custom-button').fadeOut('fast', function () {
            AJS.$('.dialog-custom-button').remove();
            buttonizeDefaultButton();
        });
        AJS.$('.throbber').removeClass('throbber-left');
    };

    var workflowCommentChangedHandler = function (event) {
        if (event.currentTarget.value.trim().length < 1) {
            removeCommentButtonsFromWorkflowDialog();
        } else if (AJS.$('.jira-dialog-content .buttons .dialog-custom-button').length === 0) {
            addCommentButtonsToWorkflowDialog();
            labelizeDefaultButton();
        }
    };

    var workflowCommentLevelButtonClickHandler = function (event) {
        event.preventDefault();
        event.stopPropagation();

        // Default to Users level
        var htmlEscapedLabel = levelForButton2;

        if (event.currentTarget.id === 'workflow-transition-button1') {
            htmlEscapedLabel = levelForButton1;
        } else if (event.currentTarget.id === 'workflow-transition-button3') {
            htmlEscapedLabel = levelForButton3;
        }

        updateCommentLevelDropdownInWorkflow(htmlEscapedLabel);
        AJS.$('.jira-dialog input[type=submit]').removeAttr('disabled');
        AJS.$('.jira-dialog input[type=submit]').click();
    };

    AJS.$(document).bind('dialogContentReady', function (event, dialog) {
        if (shouldForceUserToChooseLevel()) {

            var commentEditor = AJS.$('.jira-dialog-open #comment');

            if (commentEditor[0]) {

                commentEditor.on('input', workflowCommentChangedHandler);
                //Trigger this listener in case we already have text in the comment field
                //(which happens if errors are found when the form is validated, causing it to be re-loaded)
                commentEditor.trigger('input');
                //Clear existing click handlers to prevent doubling up (this has happened)
                //AJS.$('.jira-dialog-content .buttons').off('click');
                AJS.$('.jira-dialog-content .buttons').on('click', '.dialog-custom-button', workflowCommentLevelButtonClickHandler);
                AJS.$('.jira-dialog-open .security-level').hide();
            }
        }
    });
});

I have tried many work-arounds but nothing is working. I realize this is an obscure bug, but if anybody has any ideas, I’d appreciate it!


#2

You need to bind using Jira Events:
https://developer.atlassian.com/server/jira/platform/extending-inline-edit-for-jira-plugins/

AJS.$(function() {
    JIRA.bind(JIRA.Events.NEW_CONTENT_ADDED, function(e, context, reason) {
        if (reason === JIRA.CONTENT_ADDED_REASON.pageLoad) {
            ....

#3

This worked, thanks much!!


#4

Happy to help my man


#5

Unfortunately, after further testing, I’ve found that the plugin still does not activate until a refresh of the page when the above workflow is performed. So, when a user uses list view while viewing a custom issue filter and clicks an issue, the plugin fails to load until they refresh the issue’s page. Sorry for the delay/confusion here, but I’m hoping there’s a way to fix this behavior.