Roundup Tracker

Select 2 is a jquery/javascript based tool that makes using select boxes less painful when there are many options. You can search available options and get a much better display of the options compared to a usual select box. One of the capabilities is to use the REST interface in roundup to allow searching of the options.

This page provides an example of using select2 to replace the usual text box (e.g. superceder field in the classic template) used to edit multilinks to other issues. The tracker it came from is the sysadmin demo tracker.

By making this change replaces

normal_multilink_field.png

with

initial_display.png

When the user selects the widget, they get prompted to enter a couple of characters

start_search.png

Typing a few characters generates a scrollbox list with matching options. If there are more than 100 options, new options are loaded when the scrollbar hits the bottom.

during_search.png

when the user clicks on the matching entry, it is added to the select2 widget

search_done.png

Implementation

To follow along with this, create a jslibraries subdirectory in your tracker's html directory. After downloading the select2 files, expand the zip file into the jslibraries directory. (Note you can also use a CDN. You must change the url's appropriately and should include a subresource integrity attribute in the script tag and css link.) Also download jquery libraries and install them in the same subdirectory.

html page changes

Edit page.html to load select2's css and javascript library. Add:

   1   <link rel="stylesheet" href="@@file/jslibraries/select2.css">

inside the head tag and add:

   1   <script tal:attributes="nonce request/client/client_nonce"
   2           src="@@file/jslibraries/jquery-3.4.1.js"></script>
   3   <script tal:attributes="nonce request/client/client_nonce"
   4           src="@@file/jslibraries/select2.js"></script>
   5   <script tal:attributes="nonce request/client/client_nonce"
   6           src="@@file/tracker.js"></script>

at the end of the body tag so that loading the javascript libraries doesn't delay the display of the page.

The contents of tracker.js will be discussed later. It provides the trigger that activates select2 on the select boxes on the page.

The example below is done using a dependson multilink to issues. This is the same relationship as the superseder field in the classic tracker. Translate the field names as appropriate for your tracker.

In issue.item.html look for the tag with the field call for your multilink relationship. For example:

   1   <span tal:content="structure
   2                      python:context.dependson.field(showid=1,
   3                      size=20, id='dependsrelation') or default">
   4     No dependencies
   5   </span>

Replace this with:

   1   <span tal:condition="not:context/dependson/is_edit_ok"
   2         tal:content="structure
   3                      python:context.dependson.field(showid=1,
   4                      size=20, id='dependsrelation') or default">
   5     No dependencies
   6   </span>
   7   <input tal:condition="context/dependson/is_edit_ok"
   8          type="hidden"   name="dependson" value="" />
   9   <select tal:condition="context/dependson/is_edit_ok"
  10           id="dependson" name="dependson" multiple>
  11       <tal:block tal:repeat="issue context/dependson">
  12           <option selected tal:attributes="value issue/id"
  13                   tal:content="string:${issue/title}
  14                   ([issue${issue/id}] by ${issue/requestedby/username})">
  15           </option>
  16       </tal:block>
  17   </select>
  18   <noscript>Enable javascript to add items to the list.</noscript>

A few things to note:

  1. The hidden input type is required to allow the field to be emptied. If you don't have it removing all dependson items from the list will have no effect.
  2. The value for the name attribute is the same one you will see if you use debugging tools on the original html generated by the field call.
  3. The use of tal:condition="context/dependson/is_edit_ok" or it's negated form displays a select box (that appears editable but is not within a form) only if the user has edit rights. IF the user doesn't have edit rights, a list of issues is generated by the field() call.

When the user has edit rights, this creates a select tag with an option for each issue currently linked using dependson. The displayed value for the issue consists of:

javascript code

Now create html/trigger.js and add this to the file:

   1 jQuery(document).ready(function () {
   2   jQuery("select#dependson").select2({
   3     width: "100%", // adjust to set width of replacement
   4     minimumInputLength: 2, // type two chars before searching happens
   5     delay: 250,  // wait for 1/4 second typing pause before searching
   6     placeholder: "Select issues", // text to show in empty box
   7     ajax: {  // configure the remote call
   8       url: 'https://roundup.example.net/demo/rest/data/issue', // Change Me
   9       dataType: 'json',
  10       data: function (params) {
  11           var query = {
  12             'title': params.term, // search by title
  13             '@fields': "title,requestedby", // get title and requestedby
  14             '@verbose': "2", // get username in requestedby dict
  15             '@page_size': "100", // number of entries to return
  16             '@page_index': params.page, // which page to return
  17             '@sort': "-id" // return most recent issues first
  18           }
  19           // Query parameters will be:
  20           // ?title=[term]&@fields=title,requesteby&@verbose=2&
  21           //       @page_size=100&@page_index=...@sort="-id"
  22           // @verbose=2 gives me the requestedby username in the
  23           // response.
  24           return query
  25       },
  26       processResults: function (data) {
  27           // select2 expects a pagination: more item in the dict
  28           // indicating that there are more pages of results to get.
  29           // If we have a next link in data.data['@links'] we have
  30           // more data
  31           var more = (data.data["@links"].next) ? true: false
  32 
  33           // Transforms the top-level key of the response object
  34           // extract title into text field and annotate with
  35           // issue number and requestor
  36           var data = jQuery.map(data.data.collection, function (obj) {
  37               obj.text = obj.text ||
  38                   obj.title +
  39                   ` ([issue${obj.id}] by ${obj.requestedby.username})`;
  40               return obj;
  41             });
  42 
  43           return {
  44             results: data,
  45             pagination: { "more": more }
  46           }
  47       },
  48     }
  49   });
  50 
  51   /* hide the sibling list link for enhanced multilselects */
  52   jQuery("select[multiple]").parent().siblings("a.classhelp").hide();
  53 });

edit the URL at line 8 replacing it with the rest endpoint of your roundup instance.

This code configures a function to call when jquery is fully loaded and the page is ready to be displayed. This function is called on the select tag with the id dependson (as specified by "select#dependson" at line 2). The jQuery command at line 52 hides the link for the javascript selector popup.

Open issues

If the user does not have javascript enabled but has edit rights, they will be presented with a standard multi-select box showing all the issues. The user can remove items by control-clicking, but can't add any new items to the list. New items are only available by ajax calls requiring javascript. The noscript tag lets the user know that the standard select box needs javascript on order to add items. It would be nice to have a better solution. See IsJavascriptAvailable for a way to emit a standard text input box with a list of item id's when javascript is disabled.

The template code to differentiate between having edit rights and not having edit rights is not beautiful but effective. It should be easier/more beautiful.


CategoryInterfaceRest CategoryInterfaceWeb CategoryJavascript