From Request Tracker Wiki
Jump to navigation Jump to search

Popup menu shortcut for Ticket list


I don't know about you but i think merging tickets together in RT is a pain so I wanted to give myself a shortcut menu while at the same time not learn perl or mason and this is the result.

It is very rough but it works for me so I've decided to share it. Use at your own risk.


I used JQuery to dynamically add a right column to any list with the class "ticket-list". The column only contains a # sign which opens a small popup menu when clicked.

The popup menu currently contains the following function:

* View * Reply * Resolve * Delete * Merge

View opens the Display page Reply, Resolve and Delete open the Update page with the appropriate action and defaultstatus. Same link RT uses to do the same thing.

Merge works in two steps. Select the Merge option on the ticket your want to get rid of, then select the Merge option on the Ticket you want to update. The Merge options should display:

* Merge with #22

There is a confirmation request and the merge is done via an xmlhttp request which loads the ModifyLink page in the background. If the merge is successful the Ticket row is removed from the table.

If it works it should display a yellow information bar with "Merge Success" and an error message if it fails or "Unknown Result" if the screen scraping barfed.


It uses JQuery. Because I don't like prototype. :)

The script has some serious limitation. First it assumes that the first column of the Ticket list contains the ticket id.

It only really works on the "Home" page ticket list. It seems to work on the Ticket search result page but I haven't really tried so expect bugs.

It uses dirty regexp based HTML scraping regexp to retrieve the merge result info and it assume that you speak English or at least that you use an English UI. The script doesn't really care what you speak it only look for the string "Success". ;)

It's ugly.. so it blends perfectly with RT.. joke

I use dirty magic number to position the popup. I remember thinking I would fix that later but apparently I forgot.


1) Save the script below as "rthack.js" somewhere on your webserver and edit *rtbase: "http://....../rt" to point to your rt installation. I've put mine in /var/www so I won't accidentally delete it when and if I upgrade RT.

2) Download JQuery from their website and put it on your webserver..

3) I don't know if there is a better way but I added the link to the JS directly in /opt/rt3/share/html/Elements/HeaderJavascript as shown below. (Remember to rm -rf mason_cache/obj/* and restart apache for it to take effect)

<script type="text/javascript" src="http://MY_DOMAIN/jquery.js"></script> <script type="text/javascript" src="http://MY_DOMAIN/rthack.js?r=<%rand()%>"></script>

<script> var $jq = jQuery.noConflict(); </script>

The line var $jq = jQuery.noConflict(); is important otherwise JQuery will override the $ function of prototype and break RT's interface.


License? seriously? ;)

Whatever is required to comply with whatever the people at bestpractical have decided for stuff published on their wiki or public domain or "i-dont-care-but-you-cant-sue-me" :)


var rthack = { rtbase: "http://MY_DOMAIN/rt", mergeTicket: -1, targetTicket: -1, <code><pre>ticket_actions: { view: "@RT@/Ticket/Display.html?id=@TICKET_ID@", reply: "@RT@/Ticket/Update.html?Action=Respond&amp;id=@TICKET_ID@", resolve: "@RT@/Ticket/Update.html?Action=Comment&amp;DefaultStatus=resolved&amp;id=@TICKET_ID@", delete: "@RT@/Ticket/Update.html?Action=Comment&amp;DefaultStatus=deleted&amp;id=@TICKET_ID@", merge : function() { if (rthack.mergeTicket &lt;= 0) // we don't have a target ticket yet { rthack.mergeTicket = rthack.targetTicket; $jq("#rthack-merge").text("Merge with #" + rthack.mergeTicket); } else // we have a target ticket for the merge { if (confirm("Do you want to merge #" + rthack.mergeTicket + " into #" + rthack.targetTicket)) { rthack.merge_ticket(rthack.mergeTicket, rthack.targetTicket); } rthack.mergeTicket = 0; $jq("#rthack-merge").text("Merge"); } }, '*': function() { alert('not implemented (' + rthack.targetTicket + ')'); } }, // Display an info-box at the top of the screen display_message : function (message) { $jq("#rthack-infobox").remove() if ($jq("#rthack-infobox").length == 0) { infobox = $jq('&lt;div id="rthack-infobox"&gt;Yo!&lt;/div&gt;').prependTo("#body") infobox.css( { background: "#FFFF99", fontWeight: "bold", padding: "4px", margin: "5px", border: "1px solid black", "-moz-border-radius-bottomleft":"0.5em", "-moz-border-radius-bottomright":"0.5em", "-moz-border-radius-topleft":"0.5em", "-moz-border-radius-topright":"0.5em" } ) } $jq("#rthack-infobox").text(message); }, merge_ticket : function(mergeTicket, targetTicket ){ self = this; // Build an Ajax request: $jq.ajax({ type: "POST", url: this.rtbase + "/Ticket/ModifyLinks.html", data: "id=" + mergeTicket + "&amp;" + mergeTicket + "-MergeInto=" + targetTicket, success: function(data) { // This is a hack so I can play with the result in firebug = data; // Attempt to extract the result -&gt; regexp+HTML = bad... res = data.match(/&lt;ul\sclass="action-results"&gt;.*\s*.*/gim)[0]; if (res.length&gt;0) { res = res.match(/&lt;li&gt;(.*)&lt;\/li&gt;/)[1]; self.display_message(res); if (res.indexOf("Success") &gt; 0) // HACK:So much for non english speaker... { // HACK: once again we assume id is the first column $jq("table.ticket-list td:first-child").each( function(i) { var c = $jq(this); if (c.text() == mergeTicket){ c.parent()[0].remove() } }); //each } } else { self.display_message("Unknown result for merge"); } } // success });//ajax }, // return the ticket function popup show_ticket_function:function(target) { self = this; var fnaction = function(e) { var action_name = + ''; action_name = action_name.substring(action_name.indexOf("#")+1); var action = self.ticket_actions[action_name]; if (action == null) action = self.ticket_actions['*']; console.log(typeof(action)); switch(typeof(action)) { case "string": // Assume it's an URL, replace some parameters and open it action = action.replace("@RT@", self.rtbase); action = action.replace("@TICKET_ID@", self.targetTicket); document.location.href = action; break; case "function": action(); break; } console.log("action:" + action); } // if the popup does not already exist create it if ($jq('#rth-tapopup').length == 0) { $jq('&lt;div id="rth-tapopup" style="display:none"&gt;&lt;ul&gt;&lt;/ul&gt;&lt;/div&gt;').appendTo("body"); // ADD NEW ACTION IN POPUP MENU HERE // the action name is defined by href="#ACTION_NAME // which maps to a key in ticket_action: { .... } $jq('#rth-tapopup ul').append('&lt;li&gt;&lt;a href="#view"&gt;View&lt;/a&gt;&lt;/li&gt;'); $jq('#rth-tapopup ul').append('&lt;li&gt;&lt;a href="#reply"&gt;Reply&lt;/a&gt;&lt;/li&gt;'); $jq('#rth-tapopup ul').append('&lt;li&gt;&lt;a href="#resolve"&gt;Resolve&lt;/a&gt;&lt;/li&gt;'); $jq('#rth-tapopup ul').append('&lt;li&gt;&lt;a href="#delete"&gt;Delete&lt;/a&gt;&lt;/li&gt;'); $jq('#rth-tapopup ul').append('&lt;li&gt;&lt;a id="rthack-merge" href="#merge"&gt;Merge&lt;/a&gt;&lt;/li&gt;'); $jq('#rth-tapopup ul li a').bind('click', fnaction); // Hide popup when body is clicked $jq('body').bind('click', function(e) { $jq("#rth-tapopup").hide(); } ); } // relocate position = $jq(target).position() console.log(parseInt( + "px") $jq("#rth-tapopup").css( { zIndex:1000, width:"150px", background:"white", border: "1px solid black", position: 'absolute', left: parseInt(position.left) + 150 + "px", // HACK: position() does not work properly top: parseInt( + 100 +"px" }); $jq('#rth-tapopup').show(); }, // init the hack init: function() { self = this; // // Add ticket function menu in ticket table clickh = function(e) { // show ticket option popup // get the ticket id by assuming it is displayed in the first column // tr // td // (text id) // ... // td // a &lt;-- click target self.targetTicket = $jq($jq("td",[0]).text() self.show_ticket_function(; return false; } // Find table and add function column var table = $jq("table.ticket-list") $jq("tr th:last-child, td:last-child", table).each( function(i) { if($jq(this).is("th")) { $jq(this).after('&lt;th&gt;&amp;nbsp;&lt;/th&gt;'); } else if ($jq(this).is("td")) { $jq(this).after('&lt;td&gt;&lt;a href="#" class="rthack-action-button"&gt;#&lt;/a&gt;&lt;/td&gt;'); } } ); $jq("a.rthack-action-button").bind("click", clickh); } </pre></code> } $(rthack.init);