How to customize Jira with JavaScript and CSS
プラットフォームについて: Server および Data Center のみ。この記事は、Server および Data Center プラットフォームのアトラシアン製品にのみ適用されます。
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.
*Fisheye および 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.
トラブルシューティング
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.
要約
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:
- Make use of comments
- Make use of console log
- Narrow the scope of the change as much as possible
- Try and Catch errors
- Waiting for elements to render (optional, use with caution)
Jira 9+ "module" script attribute
Starting in Jira 9.4, depending on the resources you want to use (like AJS), you may need to add type="module"
to your script tags, like:
<script type="module">
...
</script>
For Jira 8 you don't need the type="module"
, just <script>...</script>
.
環境
Any currently supported version of Jira Core, Software or Service Management.
ソリューション
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
Comments allow for easier troubleshooting, given no expertise on Javascript is needed to suspect a code snippet's doing something to the UI.
"<!-- Custom code to Hide the SLA panel -->
" is easier to understand than "document.getElementById("#sla-web-panel-react").style.display = "none";
", for example.
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.
<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:
<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 type="module">
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 type="module">
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.
例
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 type="module">
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 type="module">
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:
Example 3. Hiding the Clone Issue links
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 type="module">
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>
Example 5. Adding a new stylesheet conditionally
Hiding elements through stylesheet (CSS) is generally better than through Javascript (JS) as the style's calculated by the Browser when creating the element, whereas in Javascript it'd fail if the element's not rendered yet or may delay in some cases until the script's loaded and executed (the element may show up and disappear right next).
This code snippet is an example of how to append a stylesheet dynamically depending on some conditions — in this example, if "/PROJKEY-" is present in the URL:
<!-- COMPANY-NAME custom Javascript banner to hide the SLA panel for some Projects -->
<script>
try {
if (window.location.href.indexOf("/PROJKEY-") > 0) {
let sheetSLA = document.createElement("style");
sheetSLA.id = "COMPANY-NAME-custom-stylesheet-from-announcement-banner-001";
sheetSLA.innerHTML = '#sla-web-panel-react { display: none; }';
document.head.appendChild(sheetSLA);
console.info("[COMPANY-NAME custom JS banner] [INFO] SLA panel hidden");
}
}
catch (error) {
console.info("[COMPANY-NAME custom JS banner] [ERROR] " + error);
}
</script>
When adding new elements in the page it's also very important to give it suggestive Ids to make it easier to troubleshoot.
変更を元に戻す
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.
To clear out the Announcement Banner you can run this DB update and restart Jira:
update propertytext set propertyvalue = '' where id = (select id from propertyentry where property_key='jira.alertheader');
Refer to the article on How to identify fields with custom Javascript in their description in Jira Data Center / Server to identify in which tables the custom Javascript is present and update the respective records to remove the script:
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');
Here are the same queries above all in one block for easier copying and pasting:
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%';
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%';
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%';
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.