Jira Issue Links & Files Tablification

Makes Links and Files sections of Issues into tables that can be sorted

目前為 2014-05-17 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name           Jira Issue Links & Files Tablification
// @namespace      http://home.comcast.net/~mailerdaemon
// @description    Makes Links and Files sections of Issues into tables that can be sorted
// @include        *jira*/browse/*-*
// @version        1.9
// ==/UserScript==

//Test URLS
//http://jira.atlassian.com/browse/JRA-16868
//http://jira.activemath.org/browse/IG-370
//http://jira.atlassian.com/browse/JRA-5959
//http://jira.secondlife.com/browse/WEB-856
//http://jira.atlassian.com/browse/JRA-2509

init();

GM_addStyle([
		"table.jilft {}",
		"table.jilft tr.row > td:nth-child(n+2) { border-left:1px dotted rgba(0, 0, 0, 0.133);}",
		"table.jilft tr.header td { font-size:9px; border-bottom:1px solid rgba(0,0,0,0.25);  cursor:pointer; -moz-user-select: none;}",

		"table.links_table { width:100%; }",
		"table.links_table td { padding-left: 1px; padding-right: 1px; }",
		
		"table.files_table { empty-cells:show; width:auto; }",
		"table.files_table td { padding: 1px 3px; }",// text-shadow: 0 0 2px white;
		"#attachment_thumbnails li.latest, table.files_table tr.latest { "+CSSmulti("box-shadow", "0 0 8px rgba(0, 255, 0, 0.20)") +"}", //0.025
		"#attachment_thumbnails li.latest, table.files_table tr.latest td { background-color:rgba(0, 255, 0, 0.08); text-shadow: 0 0 2px white, 0 0 2px white, 0 0 2px white, 0 0 1px white;}",
		"table.files_table tr.stale td { color: rgb(153, 153, 153); }",
		"table.files_table tr.stale td:hover { color: rgb(138, 138, 138); }",
		
		"table.links_table tr.header, table.links_table td.key { white-space: nowrap; }",
		
		"#attachment_thumbnails span.blender {display:none;}",
		
		".t_links tbody td { width: auto; }",

		"#attachment_thumbnails .attachment-content { height: 256px;}",
		"#attachment_thumbnails .attachment-content > td { display:none; }",
		"#attachment_thumbnails .attachment-size { top:auto; float:none;}",
		"#attachment_thumbnails .attachment-title, #attachment_thumbnails dl { height:auto; }",
		"#attachment_thumbnails dt, #attachment_thumbnails dd { line-height:1.1em; font-size:90%; text-align:center; float:none;}",
		//copied from atlassian css
		"a.user-no-hover.user-avatar, span.user-no-hover.user-avatar { background-position: left center; background-repeat: no-repeat; padding: 2px 0 2px 19px; }",
	].join("\n\n"));

//update these!!!
var priorities = ["Showstopper","Critical","Major","Normal","Low","Trivial","Nice to have"];

var statuses = {
	"Open":1,
	"In Progress":50,
	"Reopened":100,
	"Fix Pending":150,
	"Resolved":200,
	"Closed":250,
	"Ready for Translation":-1,
	"Translation In Progress":-1,
	"Translated":-1,
	"In QA":-1,
	"Passed QA":-1,
	"Translation Final":-1,
	"Published":-1,
	"On Hold":-1,
	"Editing In Progress":-1,
	"Edited":-1,
	"Testing":-1,
	"Verified":-1,
	"Verified: Fixed":-1,
	"Verified: Fix Needed":-1,
	"Republished":-1,
	large:10000,
}

if($X("//div[contains(concat(' ', @class, ' '), ' content ') and div[contains(concat(' ', @class, ' '), ' module ')]]"))
{//V4.1.1
	$Z("//div[contains(concat(' ', @class, ' '), ' content ')]/div[@id='attachmentmodule']", function(module){
			$Z(".//ol[@id='file_attachments']", function(r, i){
						var table = document.createElement("table");
						table.cellPadding = 1; 
						table.cellSpacing = 0;
						table.className = "files_table jilft";
						table.setAttribute("order", [-1]);
						tbody = document.createElement("tbody");
						table.appendChild(tbody);
							
						var tr = document.createElement("tr");
						tr.align="center"
						tr.style.whiteSpace="nowrap";
						tr.style.boarderBottom="1px;"
						tr.className="header row";
							
						tr.appendChild(NewHeader(1, "#", "Initial Ordering"));
						tr.appendChild(NewHeader(2, "T", "Type"));
						tr.appendChild(NewHeader(3, "Filename"));
						tr.appendChild(NewHeader(4, "Size"));
						tr.appendChild(NewHeader(5, "Date"));
						tr.appendChild(NewHeader(6, "Uploader"));
						tr.appendChild(NewHeader(7, "Latest"));
						
						tbody.appendChild(tr);
						
						$Z("li[contains(concat(' ', @class, ' '), ' attachment-content ')]", function(li, i){
								tbody.appendChild(File(i + 1, 
										$X(".//div[contains(concat(' ', @class, ' '), ' attachment-thumb ')]/a", li), 
										$X(".//*[contains(concat(' ', @class, ' '), ' attachment-title ')]/descendant-or-self::a", li), 
										$X(".//dd[contains(concat(' ', @class, ' '), ' attachment-size ')]", li)
									))
							}, r);
						
						var click = generic_click(1, FileSort);//FileShifter
						click.apply(tr.cells[0]);//we cheat!
					
						replace(r, table);
						//r.parentNode.appendChild(table);
						
						for(j = 0; j < tr.cells.length; j++)
							tr.cells[j].addEventListener('click', click, false);
					}, module);
			$Z(".//ol[@id='attachment_thumbnails']", function(ol, i){
				$Z("li[contains(concat(' ', @class, ' '), ' attachment-content ')]", function(li, i){
						//just provide the missing info
						var dl = $X("dl", li);
						var thumb = $X(".//div[contains(concat(' ', @class, ' '), ' attachment-thumb ')]/a", li);
						var link = $X(".//*[contains(concat(' ', @class, ' '), ' attachment-title ')]/descendant-or-self::a", li);
						var size = $X(".//dd[contains(concat(' ', @class, ' '), ' attachment-size ')]", li)
						obj = FileObj(i + 1, null, link, size)
						
						FileSetAttributes(li, obj)
						
						if(!(date = $X("dd[contains(concat(' ', @class, ' '), ' attachment-date ')]", dl)))
							dl.insertBefore(date = document.createElement("dd"), size).appendChild(obj.date[0]);
						dl.insertBefore(creator = document.createElement("dd"), size).appendChild(obj.creator[0]);
	//					dl.appendChild(latest = document.createElement("dd")).appendChild($X("td[7]/text()", tr));
						
						//GM_log(tr.className);
						
						li.title = link.title;
					}, ol);
				
				var table = document.createElement("table");
				table.cellPadding =1; 
				table.cellSpacing = 0;
				table.className = "files_table jilft";
				ol.setAttribute("order", [-1]);
				tbody = document.createElement("tbody");
				table.appendChild(tbody);
					
				var tr = document.createElement("tr");
				tr.align="center"
				tr.style.whiteSpace="nowrap";
				tr.style.boarderBottom="1px;"
				tr.className="header row";
					
				tr.appendChild(NewHeader(1, "Initial Ordering"));
				tr.appendChild(NewHeader(2, "Filename"));
				tr.appendChild(NewHeader(3, "Size"));
				tr.appendChild(NewHeader(4, "Date"));
				tr.appendChild(NewHeader(5, "Uploader"));
				tr.appendChild(NewHeader(6, "Latest"));
				
				tbody.appendChild(tr);
				
				var click = generic_list_click(ol, ImageSort);
				click.apply(tr.cells[0]);//we cheat!
			
				ol.insertBefore(table, ol.firstChild);
				
				for(j = 0; j < tr.cells.length; j++)
					tr.cells[j].addEventListener('click', click, false);
				}, module);
		});
	{//Link Tables
		var click = generic_click(2, LinkSort)//, LinkShifter);
		
		$Z("//table[@id = 'outwardLinks_table' or @id = 'inwardLinks_table']", function L1(table){
			table.className = (table.className + " links_table jilft").trim();
			var top = $X("thead/tr/th", table);
			top.colSpan=5;
			table.setAttribute("order", [1]);
			
			$Z("tbody/tr", function(tr, i){
					var summary = $X("td/div[contains(concat(' ', @class, ' '), ' flooded ')]", tr);
					var wrapper = $X(".//div[contains(concat(' ', @class, ' '), ' attribute-wrap ')]", summary);
					var priority = $X(".//span[contains(concat(' ', @class, ' '), ' priority ')]", wrapper);
					var status = $X(".//span[contains(concat(' ', @class, ' '), ' status ')]", wrapper);
					var link = $X(".//a", summary);
					
					tr.className = tr.className.split(" ").concat("row").join(" ");
					tr.setAttribute("number", i + 1);
					replace(summary,  document.createTextNode(i + 1));
					
					tr.appendChild(key = document.createElement("td")).appendChild(link);
					tr.appendChild(sum = document.createElement("td")).appendChild(summary);
					
					if(priority)
					{
						var s = $X(".//img", priority).alt;
						var e = priorities.indexOf(s);
						if(e == null || e == -1) e = priorities.length;
						tr.setAttribute("priority", e + 1);
						tr.appendChild(document.createElement("td")).appendChild(priority);
					}
					else
					{
						tr.appendChild(document.createElement("td"));
						tr.setAttribute("priority", priorities.length + 1);
					}
					tr.appendChild(document.createElement("td")).appendChild(status);
					
					key.className = "key";
					sum.title = link.title && link.title.trim();
					
					var s = link.textContent.trim();
					var e = s.lastIndexOf("-") + 1;
					tr.setAttribute("key", s.slice(0, e).concat(Array(9 + e - s.length).join("0"), s.slice(e)));
					
					var s = $X(".//img", status).alt;
					var e = statuses[s];
					if(e == null || e == -1) e = statuses.large;
					tr.setAttribute("status", e);
					
					if(!$X("*", wrapper))
						remove(wrapper);
					else
						GM_log("unhandled attribute!")
				}, table);
			
			var tr = document.createElement("tr");
			tr.align="center"
			tr.className="header row";
			
			tr.appendChild(number = NewHeader(1, "#", "Initial Ordering", {status:true}));
			tr.appendChild(key = NewHeader(2, "Key", null, {status:true}));
			tr.appendChild(summary = NewHeader(3, "Summary", null, {status:true}));
			tr.appendChild(priority = NewHeader(4, "P", "Priority", {status:true}));
			tr.appendChild(status = NewHeader(5, "S", "Status", {status:true}));

			summary.width="100%";
			
			insertAfter(tr, top.parentNode);
			
			for(j = 0; j < tr.cells.length; j++)
				tr.cells[j].addEventListener('click', click, false);
		});
	}
}
else
{//Old V3 to V4.1.0
	if((files = $X("//td[@id='file_attachments']")) && (files.textContent.trim() != "None")) {
		table = document.createElement("table");
		table.cellPadding =1; 
		table.cellSpacing = 0;
		table.className = "files_table jilft";
		table.setAttribute("order", [-1]);
		tbody = document.createElement("tbody");
		table.appendChild(tbody);
			
		var tr = document.createElement("tr");
		tr.align="center"
		tr.style.whiteSpace="nowrap";
		tr.style.boarderBottom="1px;"
		tr.className="header row";
			
		tr.appendChild(NewHeader(1, "#", "Initial Ordering"));
		tr.appendChild(NewHeader(2, "T", "Type"));
		tr.appendChild(NewHeader(3, "Filename"));
		tr.appendChild(NewHeader(4, "Size"));
		tr.appendChild(NewHeader(5, "Date"));
		tr.appendChild(NewHeader(6, "Uploader"));
		tr.appendChild(NewHeader(7, "Latest"));
		
		tbody.appendChild(tr);

		var items = [];
		var nodes = [files];
		while(nodes.length > 0)
		{
			var node = nodes.shift();
			var offset = 0;
			do
			{
				if(offset + 7 < node.childNodes.length)
				{
					while(e = /^[\d]+\.$/.exec(node.childNodes[offset].nodeValue.trim()))
					{
						tbody.appendChild(File(e[0], node.childNodes[offset + 1], node.childNodes[offset + 3], node.childNodes[offset + 5]));
						offset += 7;
					}
				}
				if((node.childNodes.length > ++offset) && node.childNodes[offset].nodeName == "SPAN")
					nodes.push(node.childNodes[offset++]);
				else
					break;
			}while(true);
		}
		
		var click = generic_click(1, FileSort);//FileShifter
		click.apply(tr.cells[0]);//we cheat!
		
		files.innerHTML="";
		files.appendChild(table);
		
		for(j = 0; j < tr.cells.length; j++)
			tr.cells[j].addEventListener('click', click, false);
	}

	{//Link Tables
		var click = generic_click(2, LinkSort, LinkShifter);
		
		$Z("//table[@id = 'outwardLinks_table' or @id = 'inwardLinks_table']", function L1(r, i, p){
			r.className = (r.className + " links_table jilft").trim();
			top = $X(".//tr[1]/td[@colspan]", r);
			top.colspan=5;
			r.setAttribute("order", [1]);
			
			$Z(".//tr[position()>1 and td[@width]]", L2, r);
			
			var tr = document.createElement("tr");
			tr.align="center"
			tr.className="header row";
			
			tr.appendChild(number = NewHeader(1, "#", "Initial Ordering", {status:true}));
			tr.appendChild(key = NewHeader(2, "Key", null, {status:true}));
			tr.appendChild(summary = NewHeader(3, "Summary", null, {status:true}));
			tr.appendChild(priority = NewHeader(4, "P", "Priority", {status:true}));
			tr.appendChild(status = NewHeader(5, "S", "Status", {status:true}));

			insertAfter(tr, top.parentNode);
			
			for(j = 0; j < tr.cells.length; j++)
				tr.cells[j].addEventListener('click', click, false);
		});
	}
}

/*
{//Sub-Tasks
	//https://jira.secondlife.com/browse/SVC-5165
	$Z("//form[@id='stqcform']/table[@id='issuetable']", function(r, i){
			r.className = (r.className + " links_table jilft").trim();
			r.setAttribute("order", [1]);
			
			$Z("./tbody/tr", function(tr, i){
					tr.setAttribute("sequence", Number(tr.cells[0]).textContent))
					var sum = $X(".//a", tr.cells[1]);
					tr.cells[1].colSpan="2"
					var key = /\/browse\/([A-Z]{3,})-([\d]+)/.match(sum.getAttribute("href"))
					tr.setAttribute("key", key[1] + key[2].leftPad(7))
					tr.setAttribute("summary", sum.textContent)
					tr.setAttribute("issuetype", $X(".//img", tr.cells[2])).alt)
					tr.setAttribute("status", statuses[$X(".//img", tr.cells[3])).alt])
					
					var assignee = $X("./td[contains(concat(concat(' ', @class), ' '), ' assignee ')]//img", tr);
					
					tr.setAttribute("assignee", )
				}, r);
			
			var tr = document.createElement("tr");
			tr.align="center"
			tr.className="header row";
			
			tr.appendChild(number = NewHeader(1, "#", "Initial Ordering", {status:true}));
			tr.appendChild(key = NewHeader(2, "Key", null, {status:true}));
			tr.appendChild(summary = NewHeader(3, "Summary", null, {status:true}));
			tr.appendChild(priority = NewHeader(4, "P", "Priority", {status:true}));
			tr.appendChild(status = NewHeader(5, "S", "Status", {status:true}));

			insertAfter(tr, top.parentNode);
			
			for(j = 0; j < tr.cells.length; j++)
				tr.cells[j].addEventListener('click', click, false);
		});
	var click = generic_click(2, TaskSort);
}
//*/
function TaskSort(by, a, b){
	by = Math.abs(by) - 1;
	var name = ["sequence", "key", "summary", "issuetype", "status", "assignee"][by];
	var left, right, ret = 0;
	if(name)
	{
		left = a.getAttribute(name);
		right = b.getAttribute(name);
	}
	switch(by)
	{
		case 2:
			left = a.cells[2].title;
			right = b.cells[2].title;
		case 1:
			ret = (left==right)?0:(left<right)?-1:1;
			break;
		default:
			ret = left - right;
			break;
	}
	return ret;
	
}

function TaskShifter(row){
	return $X("./td[1]/div", row);
}

function LinkSort(by, a, b){
	by = Math.abs(by) - 1;
	var name = ["number", "key", null, "priority", "status"][by];
	var left, right, ret = 0;
	if(name)
	{
		left = a.getAttribute(name);
		right = b.getAttribute(name);
	}
	switch(by)
	{
		case 2:
			left = a.cells[2].title;
			right = b.cells[2].title;
		case 1:
			ret = (left==right)?0:(left<right)?-1:1;
			break;
		default:
			ret = left - right;
			break;
	}
	//log({by:by, name:name, left:left, right:right, ret:ret});
	return ret;
}

function LinkShifter(row){
	return $X("./td[1]/img", row);
}

function FileShifter(row){
	return row.cells[0];
}

function ImageSort(by, a, b){
	by = Math.abs(by) - 1;
	var name = ["number", "name", "size", "date", "creator", "latest"][by];
	var left = a.getAttribute(name), right = b.getAttribute(name);
	var ret = 0;
	switch(by)
	{
		case 5:
			left = (left=="false");
			right = (right=="false");
		case 0:
		case 2:
		case 3:
			ret = left - right;
			break;
//		case 1:
//			left = left.toLowerCase();
//			right = right.toLowerCase();
		default:
			ret = (left==right)?0:(left<right)?-1:1;
			break;
	}
//	log({by:by, name:name, left:left, right:right});
	return ret;
}

function FileSort(by, a, b){
	by = Math.abs(by) - 1;
	var name = ["number", "type", "name", "size", "date", "creator", "latest"][by];
	var left = a.getAttribute(name), right = b.getAttribute(name);
	var ret = 0;
	switch(by)
	{
		case 0:
		case 3:
		case 4:
			ret = left - right;
			break;
		case 6:
			left = (left=="false");
			right = (right=="false");
		default:
			ret = (left==right)?0:(left<right)?-1:1;
			break;
	}
//	log({by:by, name:name, left:left, right:right});
	return ret;
}

function NewHeader(num, text, name, obj){
	td = document.createElement("td")
	td.appendChild(document.createTextNode(text));
	td.title = "Sort by " + (name?name:text);
	td.setAttribute("type", num);
	if(obj)
		for(var a in obj)
			td.setAttribute(a, obj[a]);
	return td;
}

function FileObj(num, icon, link, size)
{
	var m = /^(?:(.+?) - |)(?:(Latest) |)(([^-]+)|([\d]{4})-([\d]{2})-([\d]{2}) ([\d]{2}):([\d]{2})) - ([^-]+)$/.exec(link.title);
	var latest = m[2] != null;
	var date = m[3];
	var creator = m[10];
	var nice_date = (m[4] && JiraTimeStringToDate(m[4])) || (m[5] && new Date(m[5],m[6],m[7],m[8],m[9]))
	var name = m[1] || link.textContent.trim()
	
	m = /([\d]+(?:\.[\d]+)?) ([kTGM]B)/.exec(size.textContent);
	var bytes = parseFloat(m[1]);
	switch(m[2])
	{
		case "TB":
			bytes *= 1024;
		case "GB":
			bytes *= 1024;
		case "MB":
			bytes *= 1024;
		case "kB":
			bytes *= 1024;
			break;
	}
	
	//GM_log([num,icon,link,size,m[3],m[2]].join(", "));
	var crl = $X("//a[text()=\""+creator+"\" and @style]") || $X("//a[text()=\""+creator+"\"]");
	var cre = document.createElement("a")
	cre.appendChild(document.createTextNode(creator));
	//*
	if(crl)
	{
		cre.href = crl.getAttribute("href");
		//cre.className = crl.className;
		cre.className = (" "+crl.className+" ").replace(" user-hover "," user-no-hover ").trim();//hover doesn't work!
		if(crl.style.cssText)
			cre.style.cssText = crl.style.cssText;
	}
	//*/
	//unsafeWindow.console.log(crl)
	//unsafeWindow.console.log(cre)
	var simple_size = size.cloneNode(false);
		simple_size.appendChild(document.createTextNode(m[0]));
	
	obj = {
			number: [document.createTextNode(num), parseInt(num)],
			name:[link.cloneNode(true), name.toLowerCase()],
			size:[simple_size, bytes],
			date:[document.createTextNode(date), (new Date()) - nice_date],
			creator:[cre, creator.toLowerCase()],
			latest:[document.createTextNode(latest?"Latest":" "), latest],
		}
	if(icon)
		obj.type = [icon.cloneNode(true), $X("./img",icon).alt];
	
	return obj;
}

function FileSetAttributes(file, obj)
{
	for(var a in obj)
		file.setAttribute(a, obj[a][1]);
	
	if(typeof(obj.latest) !== "undefined")
		file.className = (file.className + (obj.latest[1]?" latest":" stale")).trim();
}

function File(num, icon, link, size){
	obj = FileObj(num, icon, link, size);
	
	tr =document.createElement("tr");

	tr.className="row";
	[obj.number, obj.type, obj.name, obj.size, obj.date, obj.creator, obj.latest].forEach(function (i){
			tr.appendChild(td = document.createElement("td"));
			td.appendChild(i[0]);
		})
	
	FileSetAttributes(tr, obj);
	
	return tr;
}

function generic_list_click(ol, sorter){
	return function (event) {
		var type = parseInt(this.getAttribute("type"));
		var a_type = Math.abs(type);

		var order = ol.getAttribute("order");
		if(order == "" || order == null)
			order = [1];
		else
			order = order.split(",").map(function(f){return parseInt(f);});
		var k;
		
	//	GM_log("------------------------------------------------");
		
		if(Math.abs(order[0]) == a_type)
			order[0] = -order[0];
		else if(type == 1)
			order = [1];//numbers are unique a good opertunity to clear the list.
		else
			(order = order.filter( function(r){return (Math.abs(r) != a_type);})).unshift(type);
		
		ol.setAttribute("order", order.join(","));
		
		//we cheated <.<
		var rows = [];
		$Z("li", function(r,i){ rows[i]=r; }, ol);
		
		rows.sort(function(a, b){
			var i = 0, j = 0, neg = (order[0] < 0);
			//we don't bounds check this because every entry is unique.
			//if we overflow order than there is something SERIOUSLY wrong with the data.
			while(!(j = sorter(order[i], a, b)))
			{
				if(order[i] == null)
					break;
				neg ^= (order[++i] < 0);
			}
			return neg ? -j : j;
		});
		
		for(k = 0; k < rows.length; ++k)
			ol.appendChild(remove(rows[k]));
		//	if(n = $X("li["+(k+2)+"]", ol))
		//		if(rows[k].getAttribute("number") != n.getAttribute("number"))
		//			insertAfter(rows[k], n);
		
		//[2,4,1,3] -> [1,2,3,4]
		//if(a != b)
			
		/*
		log({rows:rows.map(function(f){return f.getAttribute("number");}), order:order, 
				"order-names":order.map(function(by){return tbody.rows[RS-1].cells[Math.abs(by) - 1].textContent;})});
		//*/
		delete rows;
	}
}

function generic_click(RS, sorter, deshifter){
	return function (event) {
		var type = parseInt(this.getAttribute("type"));
		var a_type = Math.abs(type);
		var table = $X(".//ancestor::table[1]", this);
		var tbody = $X("tbody", table);
		var order = table.getAttribute("order");
		if(order == "" || order == null)
			order = [1];
		else
			order = order.split(",").map(function(f){return parseInt(f);});
		var k;
		
	//	GM_log("------------------------------------------------");
		
		if(Math.abs(order[0]) == a_type)
			order[0] = -order[0];
		else if(type == 1)
			order = [1];//numbers are unique a good opertunity to clear the list.
		else
			(order = order.filter( function(r){return (Math.abs(r) != a_type);})).unshift(type);
		
		table.setAttribute("order", order.join(","));
		
		//we cheated <.<
		var rows = new Array(table.rows.length - RS);
		for(k = 0; k < rows.length; ++k)
			rows[k] = table.rows[k + RS];
		
		rows.sort(function(a, b){
			var i = 0, j = 0, neg = (order[0] < 0);
			//we don't bounds check this because every entry is unique.
			//if we overflow order than there is something SERIOUSLY wrong with the data.
			while(!(j = sorter(order[i], a, b)))
			{
				if(order[i] == null)
					break;
				neg ^= (order[++i] < 0);
			}
			return neg ? -j : j;
		});
		
		if(deshifter)
		{
			//keep the numbers in place
			if(rows[0] != table.rows[RS])
				swap(deshifter(rows[0]), deshifter(table.rows[RS]), "n[0]<->o[0]");
			if(k = (rows.length - 1))
			{
				if(rows[0] == table.rows[k+RS]) //new[0] == old[last]
				{
					if(rows[k] != table.rows[RS])
						swap(deshifter(rows[k]), deshifter(table.rows[RS]), "n[k]<->o[0]");
				}
				else
				{
					if(rows[k] != table.rows[k+RS])
						swap(deshifter(rows[k]), deshifter(table.rows[k+RS]), "n[k]<->o[k]");
				}
			}
		}
		
		for(k = 0; k < rows.length; ++k)
			if(rows[k] != table.rows[k+1])
				insertAfter(rows[k], table.rows[k+1]);
		/*
		log({rows:rows.map(function(f){return f.getAttribute("number");}), order:order, 
				"order-names":order.map(function(by){return tbody.rows[RS-1].cells[Math.abs(by) - 1].textContent;})});
		//*/
		delete rows;
	}
}

function CSSmulti(name, value){
	var m = name+": "+value+"; ";
	return ["-moz-", "-webkit-", "-ie-", ""].map(function(f){return f + m;}).join("");
}

function L2(r, i, p){
	var summary = r.cells[1];
	var key = document.createElement("td");
	var priority = r.cells[2];
	var status = r.cells[3];
	insertBefore(key, summary);
	r.className = r.className.split(" ").concat("row").join(" ");
	
	r.setAttribute("number", i + 1);
	
	var link = $X(".//font/a", summary);
	var font = link.parentNode.cloneNode(false);
	font.appendChild(link);
	key.appendChild(font);
	key.className = "key"

	summary.title = link.title && link.title.trim();
	
	var s = link.textContent.trim();
	var e = s.lastIndexOf("-") + 1;
	r.setAttribute("key", s.slice(0, e).concat(Array(9 + e - s.length).join("0"), s.slice(e)));
	
	var s = $X(".//img", r.cells[3]).title.split(" - ", 1)[0];
	var e = priorities.indexOf(s);
	if(e == null || e == -1) e = priorities.length;
	r.setAttribute("priority", e + 1);
	
	var s = $X(".//img", r.cells[4]).title.split(" - ", 1)[0];
	var e = statuses[s];
	if(e == null || e == -1) e = statuses.large;
	r.setAttribute("status", e);
}

function log()
{
	var arg;
	switch(arguments.length)
	{
		case 1:
			arg = arguments[0];
			break;
		case 0:
			arg = null;
			break;
		default:
			arg = arguments;
			break;
	}
	
	var f = JSON.stringify(arg);
	if(typeof(unsafeWindow.console) != "undefined" && typeof(unsafeWindow.console.log) != "undefined")
		unsafeWindow.console.log(f);
	else
		GM_log(f);
	return arg;
}

//*/

function $X(_xpath, node){//to search in a frame, you must traverse it's .contentDocument attribute.
    var doc = (node)?(node.ownerDocument || node):(node = document);
    return doc.evaluate(_xpath, node, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotItem(0);
}
function $Y(_xpath, node){
    var doc = (node)?(node.ownerDocument || node):(node = document);
    return doc.evaluate(_xpath, node, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
}
function $Z(_xpath, func, node){
    var doc = (node)?(node.ownerDocument || node):(node = document);
    var res = doc.evaluate(_xpath, node, null,	XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    var args = Array.prototype.slice.call(arguments, 3);
    var i = 0;
    for (; i < res.snapshotLength; ++i)
        func.apply(func, [res.snapshotItem(i), i].concat(args));
    return i;
}

function insertAfter(insert, after){return after.parentNode.insertBefore(insert, after.nextSibling);}
function insertBefore(insert, before){return before.parentNode.insertBefore(insert, before);}
function remove(r){return r.parentNode.removeChild(r);}
function replace(old, New){return old.parentNode.replaceChild(New,old);}
function swap(a, b, comment){
	if(a != b)
	{
		var c = a.nextSibling; 
		var ap = a.parentNode;
		var bp = b.parentNode;
		bp.insertBefore(ap.removeChild(a), b);
		ap.insertBefore(bp.removeChild(b), c);
//		GM_log([comment, a.src?a.src:a, b.src?b.src:b].join("\n"));
	}
}
function findParent(node, parent){
	return $X(".//ancestor::"+parent+"[1]", node);
}

function init(){
	if(typeof(String.prototype.trim) == "undefined")
		String.prototype.trim = function() {return this.replace(/^\s+|\s+$/g, "");}
	if(typeof(String.prototype.leftPad) == "undefined")
		String.prototype.leftPad = function (len, text) { return new Array(len - this.length + 1).join(text || '0') + this; }
}

function JiraTimeStringToDate(str){
	return new Date(str.replace(/^(\d{2})\/([A-Z][a-z]{2})\/(\d{2})/, "$2 $1 20$3"));
}