changeset 2612:9364776d1331 beta

Added autocomplete widget for pull request reviewers, in exchange of 90s style multi select widget
author Marcin Kuzminski <marcin@python-works.com>
date Sun, 15 Jul 2012 18:49:11 +0200
parents e83be26bb8d8
children ad3573d744ef
files rhodecode/controllers/pullrequests.py rhodecode/public/css/style.css rhodecode/public/js/rhodecode.js rhodecode/templates/pullrequests/pullrequest.html
diffstat 4 files changed, 226 insertions(+), 48 deletions(-) [+]
line wrap: on
line diff
--- a/rhodecode/controllers/pullrequests.py	Sun Jul 15 17:01:31 2012 +0200
+++ b/rhodecode/controllers/pullrequests.py	Sun Jul 15 18:49:11 2012 +0200
@@ -36,7 +36,8 @@
 
 from rhodecode.lib.compat import json
 from rhodecode.lib.base import BaseRepoController, render
-from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
+from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
+    NotAnonymous
 from rhodecode.lib import helpers as h
 from rhodecode.lib import diffs
 from rhodecode.lib.utils import action_logger
@@ -58,6 +59,9 @@
                                    'repository.admin')
     def __before__(self):
         super(PullrequestsController, self).__before__()
+        repo_model = RepoModel()
+        c.users_array = repo_model.get_users_js()
+        c.users_groups_array = repo_model.get_users_groups_js()
 
     def _get_repo_refs(self, repo):
         hist_l = []
@@ -128,17 +132,10 @@
             }
 
         c.other_repos_info = json.dumps(other_repos_info)
-        c.review_members = []
-        c.available_members = []
-        for u in User.query().filter(User.username != 'default').all():
-            uname = u.username
-            if org_repo.user == u:
-                uname = _('%s (owner)') % u.username
-                # auto add owner to pull-request recipients
-                c.review_members.append([u.user_id, uname])
-            c.available_members.append([u.user_id, uname])
+        c.review_members = [org_repo.user]
         return render('/pullrequests/pullrequest.html')
 
+    @NotAnonymous()
     def create(self, repo_name):
         req_p = request.POST
         org_repo = req_p['org_repo']
@@ -147,6 +144,7 @@
         other_ref = req_p['other_ref']
         revisions = req_p.getall('revisions')
         reviewers = req_p.getall('review_members')
+
         #TODO: wrap this into a FORM !!!
 
         title = req_p['pullrequest_title']
--- a/rhodecode/public/css/style.css	Sun Jul 15 17:01:31 2012 +0200
+++ b/rhodecode/public/css/style.css	Sun Jul 15 18:49:11 2012 +0200
@@ -1429,7 +1429,8 @@
 	margin: 0 0 0 0px;
 }
 
-#content div.box div.form div.fields div.field div.input input {
+#content div.box div.form div.fields div.field div.input input,
+.reviewer_ac input {
 	background: #FFF;
 	border-top: 1px solid #b3b3b3;
 	border-left: 1px solid #b3b3b3;
@@ -1549,12 +1550,21 @@
 	padding: 5px 5px 5px 0;
 }
 
-#content div.box div.form div.fields div.field input[type=text]:focus,#content div.box div.form div.fields div.field input[type=password]:focus,#content div.box div.form div.fields div.field input[type=file]:focus,#content div.box div.form div.fields div.field textarea:focus,#content div.box div.form div.fields div.field select:focus
+#content div.box div.form div.fields div.field input[type=text]:focus,
+#content div.box div.form div.fields div.field input[type=password]:focus,
+#content div.box div.form div.fields div.field input[type=file]:focus,
+#content div.box div.form div.fields div.field textarea:focus,
+#content div.box div.form div.fields div.field select:focus,
+.reviewer_ac input:focus
 	{
 	background: #f6f6f6;
 	border-color: #666;
 }
 
+.reviewer_ac {
+	padding:10px
+}
+
 div.form div.fields div.field div.button {
 	margin: 0;
 	padding: 0 0 0 8px;
@@ -3783,6 +3793,11 @@
 	padding:0px 0px 0px 10px;
 }
 
+.reviewers_member{
+    height: 15px;
+    padding:0px 0px 0px 10px;	
+}
+
 .emails_wrap{
 	padding: 0px 20px;
 }
--- a/rhodecode/public/js/rhodecode.js	Sun Jul 15 17:01:31 2012 +0200
+++ b/rhodecode/public/js/rhodecode.js	Sun Jul 15 18:49:11 2012 +0200
@@ -63,6 +63,18 @@
 	return this.replace(new RegExp(''+char+'+$'),'');
 }
 
+
+if(!Array.prototype.indexOf) {
+    Array.prototype.indexOf = function(needle) {
+        for(var i = 0; i < this.length; i++) {
+            if(this[i] === needle) {
+                return i;
+            }
+        }
+        return -1;
+    };
+}
+
 /**
  * SmartColorGenerator
  *
@@ -1204,7 +1216,8 @@
 			return [unam, chunks];
 		}
 		return [null, null];
-    };    
+    };
+    
 	ownerAC.textboxKeyUpEvent.subscribe(function(type, args){
 		
 		var ac_obj = args[0];
@@ -1229,6 +1242,167 @@
 }
 
 
+var PullRequestAutoComplete = function (divid, cont, users_list, groups_list) {
+    var myUsers = users_list;
+    var myGroups = groups_list;
+
+    // Define a custom search function for the DataSource of users
+    var matchUsers = function (sQuery) {
+            // Case insensitive matching
+            var query = sQuery.toLowerCase();
+            var i = 0;
+            var l = myUsers.length;
+            var matches = [];
+
+            // Match against each name of each contact
+            for (; i < l; i++) {
+                contact = myUsers[i];
+                if (((contact.fname+"").toLowerCase().indexOf(query) > -1) || 
+                   	 ((contact.lname+"").toLowerCase().indexOf(query) > -1) || 
+                   	 ((contact.nname) && ((contact.nname).toLowerCase().indexOf(query) > -1))) {
+                       matches[matches.length] = contact;
+                   }
+            }
+            return matches;
+        };
+
+    // Define a custom search function for the DataSource of usersGroups
+    var matchGroups = function (sQuery) {
+            // Case insensitive matching
+            var query = sQuery.toLowerCase();
+            var i = 0;
+            var l = myGroups.length;
+            var matches = [];
+
+            // Match against each name of each contact
+            for (; i < l; i++) {
+                matched_group = myGroups[i];
+                if (matched_group.grname.toLowerCase().indexOf(query) > -1) {
+                    matches[matches.length] = matched_group;
+                }
+            }
+            return matches;
+        };
+
+    //match all
+    var matchAll = function (sQuery) {
+            u = matchUsers(sQuery);
+            return u
+        };
+
+    // DataScheme for owner
+    var ownerDS = new YAHOO.util.FunctionDataSource(matchUsers);
+
+    ownerDS.responseSchema = {
+        fields: ["id", "fname", "lname", "nname", "gravatar_lnk"]
+    };
+
+    // Instantiate AutoComplete for mentions
+    var reviewerAC = new YAHOO.widget.AutoComplete(divid, cont, ownerDS);
+    reviewerAC.useShadow = false;
+    reviewerAC.resultTypeList = false;
+    reviewerAC.suppressInputUpdate = true;
+    reviewerAC.animVert = false;
+    reviewerAC.animHoriz = false;    
+    reviewerAC.animSpeed = 0.1;
+    
+    // Helper highlight function for the formatter
+    var highlightMatch = function (full, snippet, matchindex) {
+            return full.substring(0, matchindex) 
+            + "<span class='match'>" 
+            + full.substr(matchindex, snippet.length) 
+            + "</span>" + full.substring(matchindex + snippet.length);
+        };
+
+    // Custom formatter to highlight the matching letters
+    reviewerAC.formatResult = function (oResultData, sQuery, sResultMatch) {
+		    var org_sQuery = sQuery;
+		    if(this.dataSource.mentionQuery != null){
+		    	sQuery = this.dataSource.mentionQuery;		    	
+		    }
+
+            var query = sQuery.toLowerCase();
+            var _gravatar = function(res, em, group){
+            	if (group !== undefined){
+            		em = '/images/icons/group.png'
+            	}
+            	tmpl = '<div class="ac-container-wrap"><img class="perm-gravatar-ac" src="{0}"/>{1}</div>'
+            	return tmpl.format(em,res)
+            }
+            if (oResultData.nname != undefined) {
+                var fname = oResultData.fname || "";
+                var lname = oResultData.lname || "";
+                var nname = oResultData.nname;
+                
+                // Guard against null value
+                var fnameMatchIndex = fname.toLowerCase().indexOf(query),
+                    lnameMatchIndex = lname.toLowerCase().indexOf(query),
+                    nnameMatchIndex = nname.toLowerCase().indexOf(query),
+                    displayfname, displaylname, displaynname;
+
+                if (fnameMatchIndex > -1) {
+                    displayfname = highlightMatch(fname, query, fnameMatchIndex);
+                } else {
+                    displayfname = fname;
+                }
+
+                if (lnameMatchIndex > -1) {
+                    displaylname = highlightMatch(lname, query, lnameMatchIndex);
+                } else {
+                    displaylname = lname;
+                }
+
+                if (nnameMatchIndex > -1) {
+                    displaynname = "(" + highlightMatch(nname, query, nnameMatchIndex) + ")";
+                } else {
+                    displaynname = nname ? "(" + nname + ")" : "";
+                }
+
+                return _gravatar(displayfname + " " + displaylname + " " + displaynname, oResultData.gravatar_lnk);
+            } else {
+                return '';
+            }
+        };
+        
+    //members cache to catch duplicates
+    reviewerAC.dataSource.cache = [];
+    // hack into select event
+    if(reviewerAC.itemSelectEvent){
+    	reviewerAC.itemSelectEvent.subscribe(function (sType, aArgs) {
+
+            var myAC = aArgs[0]; // reference back to the AC instance
+            var elLI = aArgs[1]; // reference to the selected LI element
+            var oData = aArgs[2]; // object literal of selected item's result data
+            var members  = YUD.get('review_members');
+            //fill the autocomplete with value
+
+            if (oData.nname != undefined) {
+            	if (myAC.dataSource.cache.indexOf(oData.id) != -1){
+            		return
+            	}
+
+            	var tmpl = '<li>'+
+		                      '<div class="reviewers_member">'+
+		                        '<div class="gravatar"><img alt="gravatar" src="{0}"/> </div>'+
+		                        '<div style="float:left">{1}</div>'+
+		                        '<input type="hidden" value="{2}" name="review_members" />'+
+		                      '</div>'+
+		                   '</li>'
+
+		        var displayname = "{0} {1} ({2})".format(oData.fname,oData.lname,oData.nname);
+            	var element = tmpl.format(oData.gravatar_lnk,displayname,oData.id);
+            	members.innerHTML += element;
+            	myAC.dataSource.cache.push(oData.id);
+            }
+    	});        
+    }
+    return {
+        ownerDS: ownerDS,
+        reviewerAC: reviewerAC,
+    };
+}
+
+
 /**
  * QUICK REPO MENU
  */
--- a/rhodecode/templates/pullrequests/pullrequest.html	Sun Jul 15 17:01:31 2012 +0200
+++ b/rhodecode/templates/pullrequests/pullrequest.html	Sun Jul 15 18:49:11 2012 +0200
@@ -69,40 +69,28 @@
     <div style="float:left; border-left:1px dashed #eee">
         <h4>${_('Pull request reviewers')}</h4>
         <div id="reviewers" style="padding:0px 0px 0px 15px">
-        ##TODO: make this nicer :)
-          <table class="table noborder">
-                  <tr>
-                      <td>
-                          <div>
-                              <div style="float:left">
-                                  <div class="text" style="padding: 0px 0px 6px;">${_('Chosen reviewers')}</div>
-                                  ${h.select('review_members',[x[0] for x in c.review_members],c.review_members,multiple=True,size=8,style="min-width:210px")}
-                                 <div  id="remove_all_elements" style="cursor:pointer;text-align:center">
-                                     ${_('Remove all elements')}
-                                     <img alt="remove" style="vertical-align:text-bottom" src="${h.url('/images/icons/arrow_right.png')}"/>
-                                 </div>
-                              </div>
-                              <div style="float:left;width:20px;padding-top:50px">
-                                  <img alt="add" id="add_element"
-                                      style="padding:2px;cursor:pointer"
-                                      src="${h.url('/images/icons/arrow_left.png')}"/>
-                                  <br />
-                                  <img alt="remove" id="remove_element"
-                                      style="padding:2px;cursor:pointer"
-                                      src="${h.url('/images/icons/arrow_right.png')}"/>
-                              </div>
-                              <div style="float:left">
-                                   <div class="text" style="padding: 0px 0px 6px;">${_('Available reviewers')}</div>
-                                   ${h.select('available_members',[],c.available_members,multiple=True,size=8,style="min-width:210px")}
-                                   <div id="add_all_elements" style="cursor:pointer;text-align:center">
-                                         <img alt="add" style="vertical-align:text-bottom" src="${h.url('/images/icons/arrow_left.png')}"/>
-                                          ${_('Add all elements')}
-                                   </div>
-                              </div>
-                          </div>
-                      </td>
-                  </tr>
-          </table>
+          ## members goes here !
+          <div class="group_members_wrap">
+            <ul id="review_members" class="group_members">
+            %for member in c.review_members:
+                  <li>
+                    <div class="reviewers_member">
+                      <div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(member.email,14)}"/> </div>
+                      <div style="float:left">${member.full_name} (${_('owner')})</div>
+                      <input type="hidden" value="${member.user_id}" name="review_members" />
+                    </div>
+                 </li>
+            %endfor
+            </ul>
+          </div>                
+          
+          <div class='ac'>
+            <div class="reviewer_ac">
+               ${h.text('user', class_='yui-ac-input')}
+               <span class="help-block">${_('Add reviewer to this pull request.')}</span>
+               <div id="reviewers_container"></div>           
+            </div>
+          </div>
         </div>
     </div>
     <h3>${_('Create new pull request')}</h3>
@@ -141,7 +129,10 @@
 </div>
 
 <script type="text/javascript">
-  MultiSelectWidget('review_members','available_members','pull_request_form');
+  var _USERS_AC_DATA = ${c.users_array|n};
+  var _GROUPS_AC_DATA = ${c.users_groups_array|n};
+  PullRequestAutoComplete('user', 'reviewers_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
+
   var other_repos_info = ${c.other_repos_info|n};
   var loadPreview = function(){
 	  YUD.setStyle(YUD.get('pull_request_overview_url').parentElement,'display','none');