Jira Issue Links & Files Tablification

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

当前为 2014-05-17 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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"));
}