How to customize Jira with JavaScript and CSS

Still need help?

The Atlassian Community is here for you.

Ask the community

Platform notice: Server and Data Center only. This article only applies to Atlassian products on the Server and Data Center platforms.

Support for Server* products ended on February 15th 2024. If you are running a Server product, you can visit the Atlassian Server end of support announcement to review your migration options.

*Except Fisheye and Crucible

 

Important supportability disclaimer!

Customizations like these are not supported by Atlassian (in any support offer) and should be used at the Jira Admins' own discretion and risk.

You may find support from fellow users in the Community or through the services provided by Atlassian Solution Partners. This article only offers some best practices should you really need to implement something like this.

On version upgrades

Also keep in mind there's no "contract" for the HTML elements in any of Atlassian products and they may change on any release without any public notice — unlike Jira's public Java API, REST API or AJS itself. This means you should include tests on your upgrade checklist to make sure the customizations you may have applied through custom Javascript still work in the new version you're upgrading to.

Troubleshooting

Also keep in mind a crucial step of troubleshooting is to disable or remove any JavaScript customization (even temporarily) and observe if the reported issue still persists. If you have implemented any such customizations, be advised Atlassian Support will ask you to remove or disable them to isolate any possible interference during troubleshooting — these customizations tamper with Jira in ways it was not designed to handle and may result in malfunctions on unrelated features or interfaces.

Summary

It's possible to add punctual customizations in Jira to work on the users' Browser side and change the look & feel or even overwrite elements in the screens and their behaviors.

This article lists some best practices when implement such customizations to make them less elusive to troubleshoot:

  1. Make use of comments
  2. Make use of console log
  3. Narrow the scope of the change as much as possible
  4. Try and Catch errors
  5. Waiting for elements to render (optional, use with caution)


Environment

Any currently supported version of Jira Core, Software or Service Management.


Solution

Always test in pre-Prod!

Never apply changes in Production without first applying them in a pre-Prod environment! (Dev, Stage, UAT, pre-Prod, etc)

Even a silly copy & paste mistake or typo can render Jira unavailable even for Admins and require a database update and Jira restart in worst cases (see the Reverting changes section).


There are three places custom Javascript, CSS and HTML tags can be inserted in Jira:

  • The Announcement Banner (see Configuring an announcement banner)
  • Project descriptions (work when the browsing the /PROJECT/summary page)
  • Custom Field descriptions (work wherever the field's present in the screen, like Issue view or edit screens, transition screens, bulk edit pages, etc)

For the Project and Custom Field to work, these settings must be enabled in Jira's General Configuration:


Best practice 1. Make use of comments

Both for intelligibility and troubleshooting, make extensive use of Comments.

  • HTML comments: <!-- comment goes here -->
  • Javascript comments: // comment goes here


Best practice 2. Make use of console log

When changing anything with a custom Javascript code, use the Browser native "console.log()" functions (info(), warn() and error()).

This prints a message in the Browser's console and can be visible through the Browser's "Developer Tools".

If you can standardize the logs to make it easier to troubleshoot and track down what may be interfering with Jira's default behavior, all the better — but always log!


Best practice 3. Narrow the scope of the change as much as possible

Unless you're working with some specific scenario of a global-system-wide change, it's good practice to limit the scope of the change as much as possible.

This can be achieved through — but not limited to:

  • Checking the current page URL
  • Checking if a certain element exists in the current page

This will help you mitigate the risk of changing (and potentially breaking) a screen you didn't consider in your tests in a pre-Prod environment.


Best practice 4. Try and Catch errors

Even with a simple and cautiously-coded script, it's a good practice to surround the custom code with try/catch blocks. This allows handling (or silencing) errors that may arise from unexpected situations (like trying to hide a button that doesn't even exist in the screen yet) and potentially compromise the rest of the script.

Success and Error logging example
<script>
try {
  document.querySelector('button[title="With unresolved tickets"]').style.display = "none";
  console.info("[My-Company Custom JavaScript] Success message here.");
}
catch (error) {
  console.info("[My-Company Custom JavaScript] ERROR: Error message here.");
}
</script>

Depending on what you're trying to achieve, it may not make sense to log the error. The example snippet below "silences" the error and prevents the error from stopping the rest of the script:

No error logging
<script>
try {
  document.querySelector('button[title="With unresolved tickets"]').style.display = "none";
  console.info("[My-Company Custom JavaScript] Success message here.");
}
catch (error) {}
</script>

As these are customization on top of Jira, you may choose not to use console.warn() or console.error(), leaving those methods for the "official" JavaScript code from Jira and apps/plugins. No harm in using them, though, as long as you prefix your custom messages or somehow write them in a way to facilitate troubleshooting.


5. Waiting for elements to render (optional, use with caution)

Jira (and most web applications today) renders most of it's UI elements asynchronously, meaning the user's not held at a blank page until everything is fully loaded. This results in UI elements being rendered by the Browser at not always the same order, and each element potentially taking a different time to load each time.

If you need to interact with the UI elements, you may need to wait for them to be fully loaded — or at least defined in the document already — to find them and edit them.

Please note these Javascript functions are examples only and should be used with caution, as they add a contention on Browser Threads (they hang for some time waiting for the specified amount of time to pass).

5.1. setTimeout (try once)

The setTimeout Javascript function waits for a number of milliseconds then executes what's inside the function:

<!-- Hiding the "With unresolved tickets" button on Assets/Insight -->
<script>
setTimeout(function () {
  AJS.toInit(function() {
    if (document.URL.indexOf("/secure/insight/search") >= 0) {
      try {
        document.querySelector('button[title="With unresolved tickets"]').style.display = "none";
        console.info("[My-Company Custom Announcement Banner] Assets "With unresolved tickets" button removed from screen");
      } catch (error) {}
    }
  });
}, 1000);
</script>

This waits for 1000ms (1 second) then tries to hide a button (.style.display = "none") named "With unresolved tickets" in the page if it matches "/secure/insight/search".

5.2. setInterval (keep trying)

The setInterval is very handy but also much more dangerous than the setTimeout: it executes the function code again and again at every interval in milliseconds.

This is useful if you want to "retry" fetching the element in case it's not there yet, but can also lead to a continuous loop if not handled properly: the "setInterval" runs forever in the Browser until it's explicitly stopped (clearInterval(myInterval)) or an uncaught error happens or the user browses another page.

<!-- Hiding the "With unresolved tickets" button on Assets/Insight -->
<script>
let myCounter = 0;
let myInterval = setInterval(function () {
  myCounter++;
  if (myCounter > 10) {
    console.info('[My-Company Custom Announcement Banner] Exhausted attempts to locate and remove the "With unresolved tickets" Assets button');
    clearInterval(myInterval);
  }
  else {
    AJS.toInit(function() {
      if (document.URL.indexOf("/secure/insight/search") >= 0) {
        try {
          document.querySelector('button[title="With unresolved tickets"]').style.display = "none";
          console.info("[My-Company Custom Announcement Banner] Assets "With unresolved tickets" button removed from screen");
          clearInterval(myInterval);
        } catch (error) {}
      }
    });
  }
}, 500);
</script>

This example script tries to locate the "With unresolved tickets" button on the "/secure/insight/search" page every 500ms and removes it when it finds it. It also drops out after 10 attempts (to prevent an infinite loop in case somehow the button never shows up or had it's named changed).

Remember that there's no "contract" on UI elements and elements names, labels, ids and even layout/tree may change from one version to another.

It's best to implement these failsafe mechanisms (like a counter to stop the interval execution) at your scripts just in case.


Examples

Example 1. Removing the + Create issue button from the Project Issues page

In this example we make use of a custom Javascript in the Announcement Banner:

<!-- Hiding the Inline Create Issue Container from URL JIRA/issues -->
<script>
if (document.URL.indexOf("JIRA/issues") >= 0) {
  AJS.toInit(function() {
    try {
      document.getElementsByClassName('inline-issue-create-container')[0].style.display = "none";
      console.info("[My-Company Custom JavaScript] Screen updated!");
    }
    catch (error) {
      console.info("[My-Company Custom JavaScript] ERROR: Log an error here if it makes sense.");
    }
  });
}
</script>

Best practices checklist

  • ✅  Comments
  • ✅  Console log
  • ✅  Limiting scope to the "JIRA/issues" page (JIRA is the Project's Key in the example)
  • ✅  Try / Catch

Outcomes

The + Create issues button's gone for project JIRA.


The Browser's console suggests a customization interfered with the screen.


Example 2. Hiding a custom field on the Bulk Edit screen

In this example we use a custom field's Description attribute to inject the Javascript:

<!-- Hiding field "Order Id" from the Bulk Edit Screen -->
<script>
if (document.URL.indexOf("views/bulkedit") >= 0) {
  AJS.toInit(function() {
    try {
      document.getElementById("customfield_10300_container").parentNode.style.display = "none";
      console.info("[My-Company Custom JavaScript] Custom field #10300 hidden from screen.");
    }
    catch (error) {}
  });
}
</script>

In this example, the field's id is 10300.

Best practices checklist

  • ✅  Comments
  • ✅  Console log
  • ✅  Limiting scope to the "views/bulkedit" pages
  • ✅  Try / Catch

Outcomes

Before the script the "Order Id" is present:

After the script the "Order Id" is gone and the console log helps identify a custom behavior:


It's not uncommon for Admins to want to restrict the Clone Issue functionality. Jira unfortunately doesn't has this feature — yet:

Admins have been working with the Workflow Permission "jira.permission.createclone" but that's a coincidence it works — it actually removes the Create Permission, but Jira still allows the Issue creation through some UI paths (though it shouldn't, actually).

The example Javascript below adds a "display: none !important;" CSS rule to the "issueaction-clone-issue" class:

<!-- Hiding the Clone Issue in Jira CSS Stylesheet -->
<script>
if (document.URL.indexOf("my-company-custom-param-to-allow-issue-clone=true") < 0) {
  AJS.toInit(function() {
    try {
      for (let styleSheet of stylesheet = document.styleSheets) {
        styleSheet.insertRule(".issueaction-clone-issue { display: none !important; }");
      }
      console.info("[My-Company Custom JavaScript] Clone Issue removed from the CSS Stylesheet");
    }
    catch (error) {
      console.info("[My-Company Custom JavaScript] ERROR trying to remove the Clone Issue from the CSS Stylesheet");
    }
  });
}
</script>

Best practices checklist

  • ✅  Comments
  • ✅  Console log
  • ❌  Limiting scope (though it has the IF block and a possible bypass mechanism)
  • ✅  Try / Catch

As a bypass mechanism, it only overrides the CSS Stylesheet if the text "my-company-custom-param-to-allow-issue-clone=true" is not present in the URL. So if the Admin needs to actually bypass this and be able to clone the issue through the URL, just append this parameter to the URL and press enter on the Browser:

https://jira-base-url/browse/ISSUE-12345?my-company-custom-param-to-allow-issue-clone=true

Or append other URLs with the ampersand if there are already other parameters:

&my-company-custom-param-to-allow-issue-clone=true

You can change this to whatever other bypass text you want or remove this bypass mechanism entirely.

REST API or direct Clone URL access will still work, though. This example only hides the elements with the "issueaction-clone-issue" class in the UI — it doesn't block the Clone feature in the server-side.


Example 4. Hiding the Announcement Banner completely

When adding anything to the Announcement Banner, even if it's not visible, you'll notice a narrow gray stripe where the banner would be located.

To get rid of it completely, simply add this snippet at the beginning or the end of the Banner — careful not to nest it inside any other script, html or style tag:

<!-- Hiding the banner completely -->
<style>
  #announcement-banner { display: none; }
</style>


Reverting changes

If things have gone wrong and with an Admin account you're not able to revert he changes trough the UI, you'll need to revert them directly on the database.

Refer to the article on How to identify fields with custom Javascript in their description in Jira to identify in which tables the custom Javascript is present and update the respective records to remove the script:

How to identify fields with custom Javascript in their description in Jira
The following query checks the descriptions of custom fields for scripts:
select * from customfield where lower(cast(description as varchar)) like '%<javascript%' or lower(cast(description as varchar)) like '%<script%' or lower(cast(description as varchar)) like '%html%' or lower(cast(description as varchar)) like '%css%';

Custom fields can also have alternate descriptions specified by field configurations:

select * from fieldlayoutitem where lower(cast(description as varchar)) like '%<javascript%' or lower(cast(description as varchar)) like '%<script%' or lower(cast(description as varchar)) like '%html%' or lower(cast(description as varchar)) like '%css%';

The following queries check for any scripts in custom field contexts:

select * from fieldconfigscheme where lower(cast(description as varchar)) like '%<javascript%' or lower(cast(description as varchar)) like '%<script%' or lower(cast(description as varchar)) like '%html%' or lower(cast(description as varchar)) like '%css%';

select * from fieldconfiguration where lower(cast(description as varchar)) like '%<javascript%' or lower(cast(description as varchar)) like '%<script%' or lower(cast(description as varchar)) like '%html%' or lower(cast(description as varchar)) like '%css%';

It can also be worth checking the announcement banner for any scripts, as it's a known potential cause of interference when it contains custom scripts or HTML code:

select * from propertytext where id in (select id from propertyentry where property_key='jira.alertheader');

After updating the specific records, a Jira restart is required.


Last modified on Nov 8, 2023

Was this helpful?

Yes
No
Provide feedback about this article
Powered by Confluence and Scroll Viewport.