Firefox for desktop - list fixed bugs in Mercurial as sortable table

Lists (as sortable table) fixed bugs related to Firefox for desktop in Mozilla Mercurial pushlogs

作者のサイトでサポートを受ける。または、このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name        Firefox for desktop - list fixed bugs in Mercurial as sortable table
// @namespace   darkred
// @version     5.5.9.2
// @date        2020.8.25
// @description Lists (as sortable table) fixed bugs related to Firefox for desktop in Mozilla Mercurial pushlogs
// @author      darkred, johnp
// @license     MIT
// @include     /^https?:\/\/hg\.mozilla\.org.*pushloghtml.*/
// @grant       GM_getResourceURL
// @grant       GM_getResourceText
// @grant       GM_addStyle
// @grant       GM_xmlhttpRequest
// @require     https://code.jquery.com/jquery-2.1.4.min.js
// @require     https://code.jquery.com/ui/1.11.4/jquery-ui.min.js
// @require     https://cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.24.3/js/jquery.tablesorter.min.js
// @require     https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment-with-locales.min.js
// @require     https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.4.1/moment-timezone-with-data.min.js
// @require     https://cdnjs.cloudflare.com/ajax/libs/jstimezonedetect/1.0.6/jstz.min.js
// @require     https://cdnjs.cloudflare.com/ajax/libs/datejs/1.0/date.min.js
// @require     https://cdnjs.cloudflare.com/ajax/libs/keypress/2.1.3/keypress.min.js
// @resource    jqUI_CSS  http://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/redmond/jquery-ui.min.css
// @resource    IconSet1  https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/themes/redmond/images/ui-bg_glass_75_d0e5f5_1x400.png
// @resource    IconSet2  https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/themes/redmond/images/ui-bg_glass_85_dfeffc_1x400.png
// @resource    IconSet3  https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/themes/redmond/images/ui-bg_gloss-wave_55_5c9ccc_500x100.png
// @resource    IconSet4  https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/themes/redmond/images/ui-bg_inset-hard_100_fcfdfd_1x100.png
// @resource    IconSet5  https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/themes/redmond/images/ui-icons_217bc0_256x240.png
// @resource    IconSet6  https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/themes/redmond/images/ui-icons_469bdd_256x240.png
// @resource    IconSet7  https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/themes/redmond/images/ui-icons_6da8d5_256x240.png
// Thanks a lot to: johnp (your contribution is most appreciated!), wOxxOm and Brock Adams.
// @supportURL  https://github.com/darkred/Userscripts/issues
// ==/UserScript==


/* eslint-disable no-console, complexity */
/* global jstz, moment */


var silent = false;
var debug = false;

time('MozillaMercurial');





// CSS rules in order to show 'up' and 'down' arrows in each table header
var stylesheet = `
<style>
thead th {
	background-repeat: no-repeat;
	background-position: right center;
}
thead th.up {
	padding-right: 20px;
	background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAQAAAINjI8Bya2wnINUMopZAQA7);
}
thead th.down {
	padding-right: 20px;
	background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAQAAAINjB+gC+jP2ptn0WskLQA7);
}
</style>`;

$('head').append(stylesheet);



var stylesheet2 =
`<style>


/* in order to highlight hovered table row */
#tbl tr:hover{ background:#F6E6C6 !important;}

/* in order the table headers to be larger and bold */
#tbl th {text-align: -moz-center !important; font-size: larger; font-weight: bold; }

/* in order to remove unnecessairy space between rows */
#dialog > div > table > tbody {line-height: 14px;}


#tbl > thead > tr > th {border-bottom: solid 1px};}


#tbl td:nth-child(1) {text-align: -moz-right;}


/* in order the 'product/component' to be aligned to the left */
#tbl td:nth-child(2) {text-align: -moz-left;}

/* in order the 'Product/Component' cells to appear truncated */
#tbl td:nth-child(2) {white-space: nowrap; text-overflow:ellipsis; overflow: hidden; max-width:280px;}   /* initially it was max-width:1px; */

/* in order the 'Modified' cells to have fixed width (via being displayed truncated but with max-width:100px)  (in order to avoid having to display "Modified__") */
#tbl td:nth-child(3) {white-space: nowrap; text-overflow:ellipsis; overflow: hidden; max-width:700px;}   /* initially it was max-width:1px; */

/* in order the 'Modified' cells to have fixed width (via being displayed truncated but with max-width:100px)  (in order to avoid having to display "Modified__") */
#tbl td:nth-child(4) {white-space: nowrap; text-overflow:ellipsis; overflow: hidden; max-width:100px;}   /* initially it was max-width:1px; */

/* in order the bug list to have width 1500px */
.ui-dialog {width:1200px !important;}

</style>`;
$('head').append(stylesheet2);







// the dialog will only be opened after all these promises have finished
var requests = [];


// theme for the jQuery dialog
if (typeof(GM_getResourceText) !== 'undefined' && typeof(GM_addStyle) !== 'undefined') {


	// https://stackoverflow.com/a/11532646/ , i.e. https://stackoverflow.com/a/11532646/3231411  (By Brock Adams)
	// Themes files URLs: https://cdnjs.com/libraries/jqueryui
	let iconSet1    = GM_getResourceURL ('IconSet1');
	let iconSet2    = GM_getResourceURL ('IconSet2');
	let iconSet3    = GM_getResourceURL ('IconSet3');
	let iconSet4    = GM_getResourceURL ('IconSet4');
	let iconSet5    = GM_getResourceURL ('IconSet5');
	let iconSet6    = GM_getResourceURL ('IconSet6');
	let iconSet7    = GM_getResourceURL ('IconSet7');
	let jqUI_CssSrc = GM_getResourceText ('jqUI_CSS');
	// jqUI_CssSrc     = jqUI_CssSrc.replace (/url\(images\/ui\-bg_.*00\.png\)/g, '');
	jqUI_CssSrc     = jqUI_CssSrc.replace (/images\/ui-bg_glass_75_d0e5f5_1x400\.png/g,         iconSet1);
	jqUI_CssSrc     = jqUI_CssSrc.replace (/images\/ui-bg_glass_85_dfeffc_1x400\.png/g,         iconSet2);
	jqUI_CssSrc     = jqUI_CssSrc.replace (/images\/ui-bg_gloss-wave_55_5c9ccc_500x100\.png/g,  iconSet3);
	jqUI_CssSrc     = jqUI_CssSrc.replace (/images\/ui-bg_inset-hard_100_fcfdfd_1x100\.png/g,   iconSet4);
	jqUI_CssSrc     = jqUI_CssSrc.replace (/images\/ui-icons_217bc0_256x240\.png/g,             iconSet5);
	jqUI_CssSrc     = jqUI_CssSrc.replace (/images\/ui-icons_469bdd_256x240\.png/g,             iconSet6);
	jqUI_CssSrc     = jqUI_CssSrc.replace (/images\/ui-icons_6da8d5_256x240\.png/g,             iconSet7);

	GM_addStyle (jqUI_CssSrc);


} else { // e.g. Greasemonkey: https://github.com/greasemonkey/greasemonkey/issues/2548
	// load jquery-ui css dynamically to bypass Content-Security-Policy restrictions
	let loadCss = $.get('https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/redmond/jquery-ui.min.css', function(css) {
		$('head').append('<style>' + css + '</style>');
	});
	requests.push(loadCss); // prevent a possible race condition where the dialog is opened before the css is loaded
}






// theme for the jQuery dialog
// $('head').append(
//     '<link ' +
//     'href="//ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/redmond/jquery-ui.min.css" ' +
//     // 'href="//ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/smoothness/jquery-ui.min.css" ' +                 // uncomment this line (and comment line 89)  in order to change theme
//     'rel="stylesheet" type="text/css">'
// );
// var newCSS = GM_getResourceText ('customCSS');
// GM_addStyle (newCSS);
$.ajax({
	url:'http://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/redmond/jquery-ui.min.css',
	success:function(data){
		$('<style></style>').appendTo('head').html(data);
	}
});

var regex = /^https:\/\/bugzilla\.mozilla\.org\/show_bug\.cgi\?id=(.*)$/;
var base_url = 'https://bugzilla.mozilla.org/rest/bug?include_fields=id,summary,status,resolution,product,component,op_sys,platform,whiteboard,last_change_time&id=';
var bugIds = [];
var bugsComplete = [];

var table = document.getElementsByTagName('table')[0];
var links = table.getElementsByTagName('a');
var len = links.length;
for (let i = 0; i < len; i++) {
	let n = links[i].href.match(regex);
	if (n !== null && n.length > 0) {
		let id = parseInt(n[1]);
		if (bugIds.indexOf(id) === -1) {
			bugIds.push(id);
		}
	}
}

var numBugs = bugIds.length;
var counter = 0;

var rest_url = base_url + bugIds.join();


String.prototype.escapeHTML = function() {
	var tagsToReplace = {
		'&': '&amp;',
		'<': '&lt;',
		'>': '&gt;'
	};
	return this.replace(/[&<>]/g, function(tag) {
		return tagsToReplace[tag] || tag;
	});
};


time('MozillaMercurial-REST');



GM_xmlhttpRequest({
	method: 'GET',
	url: rest_url,
	onload: function(response) {

		var data = JSON.parse(response.responseText);

		timeEnd('MozillaMercurial-REST');
		$.each(data.bugs, function(index) {
			let bug = data.bugs[index];
			// process bug (let "shorthands" just to simplify things during refactoring)
			let status = bug.status;
			if (bug.resolution !== '') {status += ' ' + bug.resolution;}
			let product = bug.product;
			let component = bug.component;
			let platform = bug.platform;
			if (platform === 'Unspecified') {
				platform = 'Uns';
			}
			if (bug.op_sys !== '' && bug.op_sys !== 'Unspecified') {
				platform += '/' + bug.op_sys;
			}
			let whiteboard = bug.whiteboard === '' ? '[]' : bug.whiteboard;
			// todo: message???





			// 2015-11-09T14:40:41Z
			function toRelativeTime(time, zone) {
				var format2 = ('YYYY-MM-DD HH:mm:ss Z');
				return moment(time, format2).tz(zone).fromNow();
			}


			function getLocalTimezone(){
				var tz = jstz.determine();    // Determines the time zone of the browser client
				return tz.name();             // Returns the name of the time zone eg "Europe/Berlin"
			}




			var changetime;
			var localTimezone = getLocalTimezone();

			if (bug.last_change_time !== '') {
				var temp = toRelativeTime(bug.last_change_time, localTimezone);
				if (temp.match(/(an?) .*/)) {
					changetime = temp.replace(/an?/, 1);
				} else {
					changetime = temp;
				}
			// changetime
			} else {
				changetime = '';
			}








			log('----------------------------------------------------------------------------------------------------------------------------------');
			log((index + 1) + '/' + numBugs); // Progression counter
			log('BugNo: ' + bug.id + '\nTitle: ' + bug.summary + '\nStatus: ' + status + '\nProduct: ' + product + '\nComponent: ' + component + '\nPlatform: ' + platform + '\nWhiteboard: ' + whiteboard);

			if (isRelevant(bug)) {
				// add html code for this bug
				bugsComplete.push('<tr><td><a href="'
							// + 'https://bugzilla.mozilla.org/show_bug.cgi?id='+ bug.id + '">'
							+ 'https://bugzilla.mozilla.org/show_bug.cgi?id='+ bug.id + '"' + ' title="' + bug.id + ' - ' +  bug.summary + '">#'
							+ bug.id
							+ '</a></td>'
							+ '<td nowrap>(' + product + ': ' + component + ') </td>'
							+ '<td>'+bug.summary.escapeHTML() + ' [' + platform + ']' + whiteboard.escapeHTML() + '</td>'
							+ '<td>' + changetime + '</td></tr>');  // previously had a <br> at the end;
			}
			counter++; // increase counter
			// remove processed bug from bugIds
			let i = bugIds.indexOf(bug.id);
			if (i !== -1) {bugIds[i] = null;}
		});
		log('==============\nReceived ' + counter + ' of ' + numBugs + ' bugs.');




		// process remaining bugs one-by-one
		time('MozillaMercurial-missing');
		$.each(bugIds, function(index) {
			let id = bugIds[index];
			if (id !== null) {
				time('Requesting missing bug ' + id);
				let promise = $.getJSON('https://bugzilla.mozilla.org/rest/bug/' + id,
					function(json) {
						// I've not end up here yet, so cry if we do
						console.error('Request for bug ' + id + ' succeeded unexpectedly!');
						timeEnd('Requesting missing bug ' + id);
						console.error(json);
					});
				// Actually, we usually get an '401 Authorization Required' error
				promise.fail(function(req, status, error) {
					timeEnd('Requesting missing bug ' + id);
					if (error === 'Authorization Required') {
						// log("Bug " + id + " requires authorization!");
						log('https://bugzilla.mozilla.org/show_bug.cgi?id=' + id + ' requires authorization!');
						let text = ' requires authorization!<br>';

						bugsComplete.push('<a href="'
							+ 'https://bugzilla.mozilla.org/show_bug.cgi?id='+ id + '">#'
							+ id + '</a>' + text);
					} else {
						console.error('Unexpected error encountered (Bug' + id + '): ' + status + ' ' + error);
					}
				});
				requests.push(promise);
			}
		});
		// wait for all requests to be settled, then join them together
		// Source: https://stackoverflow.com/questions/19177087/deferred-how-to-detect-when-every-promise-has-been-executed
		$.when.apply($, $.map(requests, function(p) {
			return p.then(null, function() {
				return $.Deferred().resolveWith(this, arguments);
			});
		})).always(function() {
			timeEnd('MozillaMercurial-missing');
			// Variable that will contain all values of the bugsComplete array, and will be displayed in the 'dialog' below
			var docu = '';
			docu = bugsComplete.join('');
			docu = ' <table id="tbl" style="width:100%">' +
					'<thead>' +
					'<tr><th>BugNo</th>' +
					'<th>Product/Component</th>' +            // '<th>Product/Component_________</th>' +
					'<th>Summary</th>' +
					'<th>Modified</th></tr>' +              // '<th>Modified__</th></tr>' +
					'</thead>' +
					'<tbody>' + docu + '</tbody></table>';




			var div = document.createElement('div');
			$('div.page_footer').append(div);
			div.id = 'dialog';
			docu = '<div id="dialog_content">' + docu + '</div>';
			div.innerHTML = docu;
			$('#dialog').hide();

			$(function() {
				$('#dialog').dialog({
					title: 'List of fixed bugs of Firefox for desktop (' + bugsComplete.length + ')',
					width: '1350px'
				});
			});






			// THE CUSTOM PARSER MUST BE PUT BEFORE '$('#tbl').tablesorter ( {'' or else it wont work !!!!
			// add parser through the tablesorter addParser method  (for the "Last modified" column)
			$.tablesorter.addParser({
				// set a unique id
				id: 'dates',
				is: function(s) {
					return false;                                // return false so this parser is not auto detected
				},
				format: function(s) {
					// format your data for normalization
					if (s !== ''){
						var number1, number2;

						// format your data for normalization
						number1 = Number((/(.{1,2}) .*/).exec(s)[1]);


						if (s.match(/A few seconds ago/)) { number2 = 0;}
						else if (s.match(/(.*)seconds?.*/)) { number2 = 1;}
						else if (s.match(/(.*)minutes?.*/)) {number2 = 60;}
						else if (s.match(/(.*)hours?.*/)) { number2 = 3600;}
						else if (s.match(/(.*)days?.*/)) { number2 = 86400;}
						else if (s.match(/(.*)months?.*/)) { number2 = 30 * 86400;}
						else if (s.match(/(.*)years?.*/)) {number2 = 365 * 30 * 86400;}
						return number1 * number2;

					}
				},
				// set type, either numeric or text
				type: 'numeric'
			});



			// make table sortable
			$('#tbl').tablesorter({
				cssAsc: 'up',
				cssDesc: 'down',
				sortList: [[3, 0],[1, 0],[2, 0]], // in order the table to be sorted by default by column 3 'Modified', then by column 1 'Product/Component' and then by column 2 'Summary'
				headers: {3: {sorter: 'dates'}},
				initialized: function() {
					var mytable = document.getElementById('tbl');
					for (let i = 2, j = mytable.rows.length + 1; i < j; i++) {
						if (mytable.rows[i].cells[3].innerHTML !== mytable.rows[i - 1].cells[3].innerHTML) {
							for (let k = 0; k < 4; k++) {
								mytable.rows[i - 1].cells[k].style.borderBottom = '1px black dotted';
							}
						}
					}
				}
			});






			log('ALL IS DONE');
			timeEnd('MozillaMercurial');





		});

	}
});


var flag = 1;

// bind keypress of ` so that when pressed, the separators between groups of the same timestamps to be removed, in order to sort manually
var listener = new window.keypress.Listener();
listener.simple_combo('`', function() {
	// console.log('You pressed `');
	if (flag === 1) {
		flag = 0;
		// remove seperators
		var mytable = document.getElementById('tbl');
		for (let i = 2, j = mytable.rows.length + 1; i < j; i++) {
			for (let k = 0; k < 4; k++) {
				mytable.rows[i - 1].cells[k].style.borderBottom = 'none';
			}
		}
		var sorting = [[1, 0], [2, 0]]; // sort by column 1 'Product/Component' and then by column 2 'Summary'
		$('#tbl').trigger('sorton', [sorting]);
	} else {
		if (flag === 0) {
			flag = 1;
			// console.log('You pressed ~');
			sorting = [[3, 0], [1, 0], [2, 0]]; // sort by column 3 'Modified Date, then by '1 'Product/Component' and then by column 2 'Summary'
			$('#tbl').trigger('sorton', [sorting]);
			mytable = document.getElementById('tbl');
			for (let i = 2, j = mytable.rows.length + 1; i < j; i++) {
				if (mytable.rows[i].cells[3].innerHTML !== mytable.rows[i - 1].cells[3].innerHTML) {
					for (let k = 0; k < 4; k++) {
						mytable.rows[i - 1].cells[k].style.borderBottom = '1px black dotted';
					}
				}
			}
		}
	}
});






function isRelevant(bug) {
	if (!bug.id) {return false;}
	if (bug.status && bug.status !== 'RESOLVED' && bug.status !== 'VERIFIED') {
		log('    IRRELEVANT because of it\'s Status --> ' + bug.status);
		return false;
	}
	if (bug.component && bug.product && bug.component === 'Build Config' && (bug.product === 'Toolkit' || bug.product === 'Firefox')) {
		log('    IRRELEVANT because of it\'s Product --> ' + bug.product + 'having component --> ' + bug.component);
		return false;
	}
	if (bug.product &&
		bug.product !== 'Add-on SDK'      &&
		bug.product !== 'Cloud Services'  &&
		bug.product !== 'Core'            &&
		bug.product !== 'Firefox'         &&
		bug.product !== 'Hello (Loop)'    &&
		bug.product !== 'Toolkit') {
		log('    IRRELEVANT because of it\'s Product --> ' + bug.product);
		return false;
	}
	if (bug.component &&
		bug.component === 'AutoConfig'                 ||
		bug.component === 'Build Config'               ||
		bug.component === 'DMD'                        ||
		bug.component === 'Embedding: GRE Core'        ||
		bug.component === 'Embedding: Mac'             ||
		bug.component === 'Embedding: MFC Embed'       ||
		bug.component === 'Embedding: Packaging'       ||
		bug.component === 'Hardware Abstraction Layer' ||
		bug.component === 'mach'                       ||
		bug.component === 'Nanojit'                    ||
		bug.component === 'QuickLaunch'                ||
		bug.component === 'Widget: Gonk') {
		log('    IRRELEVANT because of it\'s Component --> ' + bug.component);
		return false;
	}

	log('    OK  ' + 'https://bugzilla.mozilla.org/show_bug.cgi?id=' + bug.id);
	return true;
}




function log(str) {
	if (!silent) {
		console.log(str);
	}
}

function time(str) {
	if (debug) {
		console.time(str);
	}
}

function timeEnd(str) {
	if (debug) {
		console.timeEnd(str);
	}
}


$('#dialog').dialog({
	modal: false,
	title: 'Draggable, sizeable dialog',
	position: {
		my: 'top',
		at: 'top',
		of: document,
		collision: 'none'
	},
	// width: 1500,               // not working
	zIndex: 3666
}).dialog('widget').draggable('option', 'containment', 'none');

//-- Fix crazy bug in FF! ...
$('#dialog').parent().css({
	position: 'fixed',
	top: 0,
	left: '4em',
	width: '75ex'
});