|
// ==UserScript== |
|
// @name Jira issue page updated notification |
|
// @namespace https://greasyfork.org/users/1047370 |
|
// @description Give Jira issue page an update button, in the top navigation bar, when the issue is updated while the page is shown. Also update the title. |
|
// @author Marnix Klooster <[email protected]> |
|
// @copyright public domain |
|
// @license public domain |
|
// @version 0.9 |
|
// @homepage https://greasyfork.org/scripts/462479 |
|
// @include /^https?://(jira\.[^/]*|[^/]*\.atlassian\.net)/(browse|projects/[^/]+/issues)// |
|
// @require https://cdn.jsdelivr.net/npm/[email protected]/build/global/luxon.min.js |
|
// @grant none |
|
// ==/UserScript== |
|
|
|
/// TODO list |
|
/// |
|
/// * Bug report in Edge: After the button shows, and it has been clicked, then using browser 'back', |
|
/// which goes from a URL ending in a '#' to a URL without it, |
|
/// results in the 'refresh icon' shown again in the tab title bar. (Edge bug?) |
|
/// |
|
/// * Also support URLs like https://jira.example.com/secure/RapidBoard.jspa?...&selectedIssue=MYPROJ-98765&... |
|
/// |
|
/// * Usability issue. On 'Jira issue in search result' .../browse/SOMEPROJ-12345?jql=... pages, |
|
/// the top navigation bar scrolls out of view when scrolling down, |
|
/// making the 'update' button inaccessible, which is not helpful. |
|
/// |
|
/// Consider moving the button into the issue header (<header id="stalker" class="issue-header"/>), |
|
/// either in the primary toolbar on the left (`<div class="aui-toolbar2-primary">`) |
|
/// or in the secondary toolbar on the right (`<div class="aui-toolbar2-secondary">`). |
|
/// |
|
/// EXCEPT that on .../projects/SOMEPROJ/issues/... pages, it is the other way around: |
|
/// There the top navigation bar remains visible, and the issue header scrolls out of view... |
|
/// |
|
/// Options: |
|
/// - Do nothing, the 'search result' user will see the 'refresh' circular arrow in the title, |
|
/// and that triggers them to scroll up. |
|
/// - Show two buttons always, both in top bar and in issue header. Ugly. |
|
/// - If the top bar button scrolls out of view, then show that button e.g. floating. |
|
/// (Or add a second button in the issue header?) |
|
/// |
|
/// * If the 'updated' time on the button is not today anymore, add 'yesterday' or other relative date. |
|
/// This presumably requires triggering an event at local midnight. |
|
/// Alternatively, especially if we keep updating the tooltip regularly (see other TODO item), |
|
/// update the button regularly with the relative time (using Luxon's `.toRelative()`)? |
|
/// |
|
/// * Try to get rid of `href="#"`, since the URL-changing when clicking the button is not nice. |
|
/// |
|
/// * Perhaps: Also show the _type_ of changes, in the 'updated' button's tooltip. |
|
/// (It seems this needs both its ...?field=&expand=changelog |
|
/// and the issue's full .../comment list, and perhaps more.) |
|
/// If this is built, it also would make sense to keep watching for issue updates, |
|
/// and keep updating the tooltip (and perhaps the time, see other TODO item). |
|
/// |
|
/// Additionally, if (at least) the Description changed, make the button red/bold/highlighted |
|
/// since that other Description change will be lost. |
|
/// (And/or make that a separate userscript, that only looks at Description collisions.) |
|
/// |
|
/// * Perhaps: If another update is done, update the time on the button. |
|
/// |
|
/// Or show a time range even. Idea from elsewhere: |
|
/// - Split start and end time string using `s.split(/\s+(?!(?:AM|PM))\b/)`, giving two arrays. |
|
/// - Show the start, but leave out the suffix common to start and end. |
|
/// - Show an `&mdash`. |
|
/// - Show the end, but leave out the prefix common to start and end. |
|
/// That would result in something like |
|
/// |
|
/// issue changed 10:12:11 AM — 12:39:55 PM GMT+2 |
|
/// |
|
/// But I'm not sure how this would interact with the desire to show relative times |
|
/// (see other TODO item). |
|
/// |
|
/// * Bug/limitation: From a query result page (https://jira.infor.com/issues/?jql=...) |
|
/// clicking on a specific issue, the page is updated 'in place', |
|
/// so even though the address bar URL changes, this userscript is not activated. |
|
/// See if there is a way to fix that. |
|
/// |
|
/// * Robustness in case `aui-nav` element does not exist. |
|
/// |
|
/// * Perhaps: Wait longer after an error response, to reduce server load? |
|
|
|
/// END of TODO list |
|
|
|
|
|
"use strict"; |
|
|
|
/// Configuration settings |
|
/// (also look at @include and @match in the manifest above, |
|
/// which you can usually override in your userscript browser extension configuration) |
|
|
|
/// Setting the following too high will overload the Jira server; |
|
/// setting it too low will make this userscript less useful. |
|
var timeBetweenChecksInSeconds = 10; |
|
|
|
/// END of Configuration settings |
|
|
|
|
|
/// Helper functions |
|
|
|
/// From https://stackoverflow.com/a/35385518/223837: |
|
/// Construct an DOM element from the given HTML string. |
|
/// The caller must ensure no injection occurs, e.g. using `stringToHTML()` below. |
|
function htmlToElement(html) { |
|
var template = document.createElement('template'); |
|
html = html.trim(); // Never return a text node of whitespace as the result |
|
template.innerHTML = html; |
|
return template.content.firstChild; |
|
} |
|
|
|
/// From https://stackoverflow.com/a/22706073/223837: |
|
/// Convert a string to the equivalent HTML source code. |
|
function stringToHTML(str){ |
|
return new Option(str).innerHTML; |
|
} |
|
|
|
/// END of Helper functions |
|
|
|
|
|
// Documentation about the Jira REST API that is used here can be found at |
|
// https://docs.atlassian.com/software/jira/docs/api/REST/latest/ |
|
// (currently https://docs.atlassian.com/software/jira/docs/api/REST/9.7.0/), |
|
// specifically |
|
// |
|
// - https://docs.atlassian.com/software/jira/docs/api/REST/latest/#api/2/issue-getIssue |
|
// - https://docs.atlassian.com/software/jira/docs/api/REST/latest/#api/2/issue-getComments |
|
// |
|
// Documentation about the Javascript JIRA object's API doesn't seem to exist. |
|
// There are mostly fragments floating around in forum questions and answers. |
|
|
|
(function () { |
|
|
|
/// 'global' variables |
|
var theInterval = null; |
|
|
|
function start() { |
|
if (theInterval) { |
|
console.log(`SOMETHING WENT WRONG. Ignoring this call to start().`); |
|
return; |
|
} |
|
console.log(`STARTING regular check for issue updates`); |
|
theInterval = setInterval(checkForUpdates, timeBetweenChecksInSeconds * 1000); |
|
} |
|
|
|
function stop() { |
|
if (!theInterval) { |
|
console.log(`SOMETHING WENT WRONG. Ignoring this call to stop().`); |
|
return; |
|
} |
|
console.log(`STOPPING regular check for issue updates`); |
|
clearInterval(theInterval); |
|
theInterval = null; |
|
} |
|
|
|
function checkForUpdates() { |
|
var issueNumber = JIRA.Issue.getIssueKey(); |
|
if (issueNumber == null) { |
|
console.log(`No issue number found (yet?)... trying again in a little while`); |
|
return; |
|
} |
|
|
|
console.log(`Checking whether issue ${issueNumber} has been recently updated`); |
|
fetch(new Request(`/rest/api/latest/issue/${issueNumber}?fields=updated`, { |
|
headers: {'Content-Type': 'application/json'} |
|
})).then((response) => { |
|
// technical handling of the response |
|
if (!response.ok) { |
|
console.log(`Something went wrong checking for updates of issue ${issueNumber}, will retry in a little while: ${response.statusText}`); |
|
return Promise.reject(response); // not handled in any way, no need |
|
} |
|
return response.json(); |
|
}).then((responseJSON) => { |
|
// functional handling of the response |
|
|
|
// 'last updated' according to the Jira REST API |
|
var issueLastUpdated = luxon.DateTime.fromISO(responseJSON.fields.updated); |
|
// 'last updated' according to this page; could perhaps also be read via the JIRA object? could not find any documentation |
|
var pageLastUpdated = luxon.DateTime.fromISO(document.getElementById("updated-val").getElementsByTagName("time")[0].getAttribute("datetime")); |
|
console.log(`Issue ${issueNumber} was last updated ${issueLastUpdated}`); |
|
console.log(`This page says its data is from ${pageLastUpdated}' (+/- 1 second)`); |
|
if (pageLastUpdated.toMillis() + 1000 < issueLastUpdated.toMillis()) { // + 1000 since 'page last updated' has no millisecond information |
|
// issue was updated after the page information was refreshed |
|
var issueLastUpdatedText = issueLastUpdated.toLocaleString(luxon.DateTime.TIME_WITH_SHORT_OFFSET, {}); |
|
console.log(`Concluding that issue was updated after last page refresh, just now at ${issueLastUpdatedText}: showing update button and updating title`); |
|
|
|
// We put the button as the last in the <ul class="aui-nav"> top navigation bar. |
|
// (The button is the same as the 'Create' button; `href="#"` is needed for the correct hover color.) |
|
var updateButtonElement = htmlToElement(` |
|
<li id="marnix_update_page_button"> |
|
<a |
|
href="#" |
|
class="aui-button aui-button-primary aui-style" |
|
title="Update page" |
|
>Update page (issue changed ${stringToHTML(issueLastUpdatedText)})</a> |
|
</li> |
|
`); |
|
updateButtonElement.addEventListener("click", updateThisPage); |
|
document.getElementsByClassName("aui-nav")[0].appendChild(updateButtonElement); |
|
|
|
document.title = `\u21BB ${document.title}`; // prepend 'Clockwise Open Circle Arrow' character |
|
|
|
// From now on leave the page alone, the `ISSUE_REFRESHED` handler (below) will re-enable the regular check |
|
stop(); |
|
return; |
|
} |
|
|
|
console.log(`Concluding that there was no recent issue ${issueNumber} update, will check again in a little while.`); |
|
}); |
|
} |
|
|
|
function updateThisPage() { |
|
console.log(`Let this page update its information (which also reverts the title)`); |
|
JIRA.trigger(JIRA.Events.REFRESH_ISSUE_PAGE, [JIRA.Issue.getIssueId()]); |
|
// this event triggers an update, which raises an ISSUE_REFRESHED event, |
|
// which is caught by the event handler below, which will re-enable the regular check for issue updates |
|
// (or it triggers a full page reload, sometimes, it seems, e.g. if there is a network issue) |
|
} |
|
|
|
JIRA.bind(JIRA.Events.ISSUE_REFRESHED, function (e, context) { |
|
console.log(`Something triggered a refresh of this page`); |
|
|
|
if (!theInterval) { |
|
console.log(`We will start to look for issue updates again in a little while`); |
|
start(); |
|
} |
|
|
|
var updateButtonElement = document.getElementById('marnix_update_page_button') |
|
if (updateButtonElement) { |
|
console.log(`We can remove the update button again`); |
|
updateButtonElement.remove(); |
|
|
|
// no need to revert the document.title, the refresh that just happened has already done that |
|
} |
|
}); |
|
|
|
// No need to updateThisPage() on JIRA.Events.INLINE_EDIT_SAVE_COMPLETE, |
|
// because every inline edit also updates the `Updated:` (id="updated-val") field, |
|
// and we already trigger on that. |
|
|
|
start(); |
|
})(); |