// ==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.8
// @homepage https://greasyfork.org/en/scripts/462479-jira-issue-page-updated-notification
// @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
///
/// * 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()`)?
///
/// * 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 `escapeHTML()` 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 escapeHTML(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 regularlyChecking = true;
function regularlyCheckForUpdates() {
if (!regularlyChecking) {
console.log(`ERROR: internal inconsistency, somewhere we forgot to set 'regularlyChecking = true'`);
regularlyChecking = true;
}
var issueNumber = JIRA.Issue.getIssueKey(); // TODO: avoid unnecessary failing call below if the result is `null`.
console.log(`Checking whether issue ${issueNumber} has been recently updated`);
$.ajax({url:`/rest/api/latest/issue/${issueNumber}?fields=updated`, type:"GET", dataType:"json", contenType: "application/json",
success: function(response) {
// 'last updated' according to the Jira REST API
var issueLastUpdated = luxon.DateTime.fromISO(response.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 ${escapeHTML(issueLastUpdatedText)})</a>
</li>
`);
updateButtonElement.addEventListener("click", updateThisPage);
document.getElementsByClassName("aui-nav")[0].appendChild(updateButtonElement);
document.title = `\u21BB ${document.title}`;
// From now on leave the page alone, the `ISSUE_REFRESHED` handler (below) will re-enable the regular check
regularlyChecking = false;
return;
}
console.log(`Concluding that there was no recent issue ${issueNumber} update, will check again in a little while.`);
setTimeout(regularlyCheckForUpdates, timeBetweenChecksInSeconds*1000);
},
error: function(xhr, textStatus, errorThrown) {
console.log(`Something went wrong checking for updates of issue ${issueNumber}, will retry in a little while: ${textStatus} : ${errorThrown}`);
setTimeout(regularlyCheckForUpdates, timeBetweenChecksInSeconds*1000);
}
});
}
function updateThisPage() {
console.log(`Let this page update its information (which also updates 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 (!regularlyChecking) {
console.log(`We will start to look for issue updates again in a little while`);
setTimeout(regularlyCheckForUpdates, timeBetweenChecksInSeconds*1000);
regularlyChecking = true;
}
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
}
});
JIRA.bind(JIRA.Events.INLINE_EDIT_SAVE_COMPLETE, function (e, context) {
console.log(`INLINE_EDIT_SAVE_COMPLETE (e.g., inline Description change), force page update`);
updateThisPage();
});
regularlyCheckForUpdates();
})();