Marcin Kuzminski avatar Marcin Kuzminski committed b61e540

#415: Adding comment to changeset causes reload
- comments are now added via ajax and doesn't reload the page

Comments (0)

Files changed (8)

rhodecode/controllers/changeset.py

 
         return render('changeset/raw_changeset.html')
 
+    @jsonify
     def comment(self, repo_name, revision):
-        ChangesetCommentsModel().create(text=request.POST.get('text'),
-                                        repo_id=c.rhodecode_db_repo.repo_id,
-                                        user_id=c.rhodecode_user.user_id,
-                                        revision=revision,
-                                        f_path=request.POST.get('f_path'),
-                                        line_no=request.POST.get('line'))
+        comm = ChangesetCommentsModel().create(
+            text=request.POST.get('text'),
+            repo_id=c.rhodecode_db_repo.repo_id,
+            user_id=c.rhodecode_user.user_id,
+            revision=revision,
+            f_path=request.POST.get('f_path'),
+            line_no=request.POST.get('line')
+        )
         Session.commit()
-        return redirect(h.url('changeset_home', repo_name=repo_name,
-                              revision=revision))
+        data = {
+           'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
+        }
+        if comm:
+            c.co = comm
+            data.update(comm.get_dict())
+            data.update({'rendered_text': render('changeset/changeset_comment_block.html')})
+        return data
 
     @jsonify
     def delete_comment(self, repo_name, comment_id):

rhodecode/model/comment.py

             .filter(ChangesetComment.repo_id == repo_id)\
             .filter(ChangesetComment.revision == revision)\
             .filter(ChangesetComment.line_no != None)\
-            .filter(ChangesetComment.f_path != None).all()
+            .filter(ChangesetComment.f_path != None)\
+            .order_by(ChangesetComment.comment_id.asc())\
+            .all()
 
         paths = defaultdict(lambda: defaultdict(list))
 

rhodecode/public/css/style.css

 
 .comment .buttons {
 	float: right;
+	padding:2px 2px 0px 0px;
 }
 
 
 }
 
 /** comment inline form **/
+.comment-inline-form .overlay{
+	display: none;
+}
+.comment-inline-form .overlay.submitting{
+	display:block;
+    background: none repeat scroll 0 0 white;
+    font-size: 16px;
+    opacity: 0.5;
+    position: absolute;
+    text-align: center;
+    vertical-align: top;
+
+}
+.comment-inline-form .overlay.submitting .overlay-text{
+	width:100%;
+	margin-top:5%;
+}
 
 .comment-inline-form .clearfix{
     background: #EEE;
 div.comment-inline-form {
     margin-top: 5px;
     padding:2px 6px 8px 6px;
+
 }
 
 .comment-inline-form strong {
     margin: 3px 3px 5px 5px;
     background-color: #FAFAFA;
 }
+.inline-comments .add-comment {
+	padding: 2px 4px 8px 5px;
+}
+
 .inline-comments .comment-wrapp{
 	padding:1px;
 }
     font-size: 16px;
 }
 .inline-comments-button .add-comment{
-	margin:10px 5px !important;
+	margin:2px 0px 8px 5px !important
 }
 .notifications{
     border-radius: 4px 4px 4px 4px;

rhodecode/public/js/rhodecode.js

 	
 };
 
+var ajaxPOST = function(url,postData,success) {
+	var toQueryString = function(o) {
+	    if(typeof o !== 'object') {
+	        return false;
+	    }
+	    var _p, _qs = [];
+	    for(_p in o) {
+	        _qs.push(encodeURIComponent(_p) + '=' + encodeURIComponent(o[_p]));
+	    }
+	    return _qs.join('&');
+	};
+	
+    var sUrl = url;
+    var callback = {
+        success: success,
+        failure: function (o) {
+            alert("error");
+        },
+    };
+    var postData = toQueryString(postData);
+    var request = YAHOO.util.Connect.asyncRequest('POST', sUrl, callback, postData);
+    return request;
+};
+
+
 /**
  * tooltip activate
  */
 	}	
 };
 
-var ajaxPOST = function(url,postData,success) {
-    var sUrl = url;
-    var callback = {
-        success: success,
-        failure: function (o) {
-            alert("error");
-        },
-    };
-    var postData = postData;
-    var request = YAHOO.util.Connect.asyncRequest('POST', sUrl, callback, postData);
+var tableTr = function(cls,body){
+	var tr = document.createElement('tr');
+	YUD.addClass(tr, cls);
+	
+	
+	var cont = new YAHOO.util.Element(body);
+	var comment_id = fromHTML(body).children[0].id.split('comment-')[1];
+	tr.id = 'comment-tr-{0}'.format(comment_id);
+	tr.innerHTML = '<td class="lineno-inline new-inline"></td>'+
+    				 '<td class="lineno-inline old-inline"></td>'+ 
+                     '<td>{0}</td>'.format(body);
+	return tr;
 };
 
-
 /** comments **/
 var removeInlineForm = function(form) {
 	form.parentNode.removeChild(form);
 };
 
-var tableTr = function(cls,body){
-	var form = document.createElement('tr');
-	YUD.addClass(form, cls);
-	form.innerHTML = '<td class="lineno-inline new-inline"></td>'+
-    				 '<td class="lineno-inline old-inline"></td>'+ 
-                     '<td>{0}</td>'.format(body);
-	return form;
-};
-
 var createInlineForm = function(parent_tr, f_path, line) {
 	var tmpl = YUD.get('comment-inline-form-template').innerHTML;
 	tmpl = tmpl.format(f_path, line);
 	var form_hide_button = new YAHOO.util.Element(form.getElementsByClassName('hide-inline-form')[0]);
 	form_hide_button.on('click', function(e) {
 		var newtr = e.currentTarget.parentNode.parentNode.parentNode.parentNode.parentNode;
+		if(YUD.hasClass(newtr.nextElementSibling,'inline-comments-button')){
+			YUD.setStyle(newtr.nextElementSibling,'display','');
+		}
 		removeInlineForm(newtr);
 		YUD.removeClass(parent_tr, 'form-open');
+		
 	});
+	
 	return form
 };
+
+/**
+ * Inject inline comment for on given TR this tr should be always an .line
+ * tr containing the line. Code will detect comment, and always put the comment
+ * block at the very bottom
+ */
 var injectInlineForm = function(tr){
+	  if(!YUD.hasClass(tr, 'line')){
+		  return
+	  }
+	  var submit_url = AJAX_COMMENT_URL;
 	  if(YUD.hasClass(tr,'form-open') || YUD.hasClass(tr,'context') || YUD.hasClass(tr,'no-comment')){
 		  return
 	  }	
 	  var node = tr.parentNode.parentNode.parentNode.getElementsByClassName('full_f_path')[0];
 	  var f_path = YUD.getAttribute(node,'path');
 	  var lineno = getLineNo(tr);
-	  var form = createInlineForm(tr, f_path, lineno);
-	  var target_tr = tr;
-	  if(YUD.hasClass(YUD.getNextSibling(tr),'inline-comments')){
-		  target_tr = YUD.getNextSibling(tr);
-	  }
-	  YUD.insertAfter(form,target_tr);
+	  var form = createInlineForm(tr, f_path, lineno, submit_url);
+	  
+	  var parent = tr;
+	  while (1){
+		  var n = parent.nextElementSibling;
+		  // next element are comments !
+		  if(YUD.hasClass(n,'inline-comments')){
+			  parent = n;
+		  }
+		  else{
+			  break;
+		  }
+	  }	  
+	  YUD.insertAfter(form,parent);
+	  
 	  YUD.get('text_'+lineno).focus();
+	  var f = YUD.get(form);
+	  
+	  var overlay = f.getElementsByClassName('overlay')[0];
+	  var _form = f.getElementsByClassName('inline-form')[0];
+	  
+	  form.on('submit',function(e){
+		  YUE.preventDefault(e);
+		  
+		  //ajax submit
+		  var text = YUD.get('text_'+lineno).value;
+		  var postData = {
+	            'text':text,
+	            'f_path':f_path,
+	            'line':lineno
+		  };
+		  
+		  if(lineno === undefined){
+			  alert('missing line !');
+			  return
+		  }
+		  if(f_path === undefined){
+			  alert('missing file path !');
+			  return
+		  }
+		  
+		  var success = function(o){
+			  YUD.removeClass(tr, 'form-open');
+			  removeInlineForm(f);			  
+			  var json_data = JSON.parse(o.responseText);
+	          renderInlineComment(json_data);
+		  };
+
+		  if (YUD.hasClass(overlay,'overlay')){
+			  var w = _form.offsetWidth;
+			  var h = _form.offsetHeight;
+			  YUD.setStyle(overlay,'width',w+'px');
+			  YUD.setStyle(overlay,'height',h+'px');
+		  }		  
+		  YUD.addClass(overlay, 'submitting');		  
+		  
+		  ajaxPOST(submit_url, postData, success);
+	  });
+	  
 	  tooltip_activate();
 };
 
-var createInlineAddButton = function(tr,label){
-	var html = '<div class="add-comment"><span class="ui-btn">{0}</span></div>'.format(label);
-        
-	var add = new YAHOO.util.Element(tableTr('inline-comments-button',html));
+var deleteComment = function(comment_id){
+	var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__',comment_id);
+    var postData = {'_method':'delete'};
+    var success = function(o){
+        var n = YUD.get('comment-tr-'+comment_id);
+        var root = n.previousElementSibling.previousElementSibling;
+        n.parentNode.removeChild(n);
+
+        // scann nodes, and attach add button to last one
+        placeAddButton(root);        
+    }
+    ajaxPOST(url,postData,success);
+}
+
+
+var createInlineAddButton = function(tr){
+
+	var label = TRANSLATION_MAP['add another comment'];
+	
+	var html_el = document.createElement('div');
+	YUD.addClass(html_el, 'add-comment');
+	html_el.innerHTML = '<span class="ui-btn">{0}</span>'.format(label);
+	
+	var add = new YAHOO.util.Element(html_el);
 	add.on('click', function(e) {
 		injectInlineForm(tr);
 	});
 	return line
 };
 
+var placeAddButton = function(target_tr){
+	if(!target_tr){
+		return
+	}
+	var last_node = target_tr;
+    //scann	
+	  while (1){
+		  var n = last_node.nextElementSibling;
+		  // next element are comments !
+		  if(YUD.hasClass(n,'inline-comments')){
+			  last_node = n;
+			  //also remove the comment button from previos
+			  var comment_add_buttons = last_node.getElementsByClassName('add-comment');
+			  for(var i=0;i<comment_add_buttons.length;i++){
+				  var b = comment_add_buttons[i];
+				  b.parentNode.removeChild(b);
+			  }
+		  }
+		  else{
+			  break;
+		  }
+	  }
+	  
+    var add = createInlineAddButton(target_tr);
+    // get the comment div
+    var comment_block = last_node.getElementsByClassName('comment')[0];
+    // attach add button
+    YUD.insertAfter(add,comment_block);	
+}
+
+/**
+ * Places the inline comment into the changeset block in proper line position
+ */
+var placeInline = function(target_container,lineno,html){
+	  var lineid = "{0}_{1}".format(target_container,lineno);
+	  var target_line = YUD.get(lineid);
+	  var comment = new YAHOO.util.Element(tableTr('inline-comments',html))
+	  
+	  // check if there are comments already !
+	  var parent = target_line.parentNode;
+	  var root_parent = parent;
+	  while (1){
+		  var n = parent.nextElementSibling;
+		  // next element are comments !
+		  if(YUD.hasClass(n,'inline-comments')){
+			  parent = n;
+		  }
+		  else{
+			  break;
+		  }
+	  }
+	  // put in the comment at the bottom
+	  YUD.insertAfter(comment,parent);
+	  
+	  // scann nodes, and attach add button to last one
+      placeAddButton(root_parent);
+
+	  return target_line;
+}
+
+/**
+ * make a single inline comment and place it inside
+ */
+var renderInlineComment = function(json_data){
+    try{
+	  var html =  json_data['rendered_text'];
+	  var lineno = json_data['line_no'];
+	  var target_id = json_data['target_id'];
+	  placeInline(target_id, lineno, html);
+
+    }catch(e){
+  	  console.log(e);
+    }
+}
+
+/**
+ * Iterates over all the inlines, and places them inside proper blocks of data
+ */
+var renderInlineComments = function(file_comments){
+	for (f in file_comments){
+        // holding all comments for a FILE
+		var box = file_comments[f];
+
+		var target_id = YUD.getAttribute(box,'target_id');
+		// actually comments with line numbers
+        var comments = box.children;
+        for(var i=0; i<comments.length; i++){
+        	var data = {
+        		'rendered_text': comments[i].outerHTML,
+        		'line_no': YUD.getAttribute(comments[i],'line'),
+        		'target_id': target_id
+        	}
+        	renderInlineComment(data);
+        }
+    }	
+}
+
 
 var fileBrowserListeners = function(current_url, node_list_url, url_base,
 									truncated_lbl, nomatch_lbl){

rhodecode/templates/base/root.html

 
             <script type="text/javascript">
             var follow_base_url  = "${h.url('toggle_following')}";
-            var stop_follow_text = "${_('Stop following this repository')}";
-            var start_follow_text = "${_('Start following this repository')}";
-
+            
+            //JS translations map
+            var TRANSLATION_MAP = {
+            	'add another comment':'${_("add another comment")}',
+                'Stop following this repository':"${_('Stop following this repository')}",
+                'Start following this repository':"${_('Start following this repository')}",
+            };
 
             var onSuccessFollow = function(target){
                 var f = YUD.get(target.id);
 
                 if(f.getAttribute('class')=='follow'){
                     f.setAttribute('class','following');
-                    f.setAttribute('title',stop_follow_text);
+                    f.setAttribute('title',TRANSLATION_MAP['Stop following this repository']);
 
                     if(f_cnt){
                         var cnt = Number(f_cnt.innerHTML)+1;
                 }
                 else{
                     f.setAttribute('class','follow');
-                    f.setAttribute('title',start_follow_text);
+                    f.setAttribute('title',TRANSLATION_MAP['Start following this repository']);
                     if(f_cnt){
                         var cnt = Number(f_cnt.innerHTML)+1;
                         f_cnt.innerHTML = cnt;

rhodecode/templates/changeset/changeset.html

     <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
     ${comment.comment_inline_form(c.changeset)}
 
+    ## render comments
     ${comment.comments(c.changeset)}
-
     <script type="text/javascript">
-      var deleteComment = function(comment_id){
-
-          var url = "${url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}".replace('__COMMENT_ID__',comment_id);
-          var postData = '_method=delete';
-          var success = function(o){
-              var n = YUD.get('comment-'+comment_id);
-              n.parentNode.removeChild(n);
-          }
-          ajaxPOST(url,postData,success);
-      }
-
       YUE.onDOMReady(function(){
-
+    	  AJAX_COMMENT_URL = "${url('changeset_comment',repo_name=c.repo_name,revision=c.changeset.raw_id)}";
+    	  AJAX_COMMENT_DELETE_URL = "${url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}"
           YUE.on(YUQ('.show-inline-comments'),'change',function(e){
               var show = 'none';
               var target = e.currentTarget;
 
           // inject comments into they proper positions
           var file_comments = YUQ('.inline-comment-placeholder');
-
-          for (f in file_comments){
-              var box = file_comments[f];
-              var inlines = box.children;
-              for(var i=0; i<inlines.length; i++){
-                  try{
-
-                    var inline = inlines[i];
-                    var lineno = YUD.getAttribute(inlines[i],'line');
-                    var lineid = "{0}_{1}".format(YUD.getAttribute(inline,'target_id'),lineno);
-                    var target_line = YUD.get(lineid);
-
-                    var add = createInlineAddButton(target_line.parentNode,'${_("add another comment")}');
-                    YUD.insertAfter(add,target_line.parentNode);
-
-                    var comment = new YAHOO.util.Element(tableTr('inline-comments',inline.innerHTML))
-                    YUD.insertAfter(comment,target_line.parentNode);
-                  }catch(e){
-                	  console.log(e);
-                  }
-              }
-          }
+          renderInlineComments(file_comments);
       })
 
     </script>

rhodecode/templates/changeset/changeset_comment_block.html

+<%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
+${comment.comment_block(c.co)}

rhodecode/templates/changeset/changeset_file_comment.html

 ## ${comment.comment_block(co)}
 ##
 <%def name="comment_block(co)">
-  <div class="comment" id="comment-${co.comment_id}">
+  <div class="comment" id="comment-${co.comment_id}" line="${co.line_no}">
     <div class="comment-wrapp">
   	<div class="meta">
   		<span class="user">
 <div id='comment-inline-form-template' style="display:none">
   <div class="comment-inline-form">
   %if c.rhodecode_user.username != 'default':
-      ${h.form(h.url('changeset_comment', repo_name=c.repo_name, revision=changeset.raw_id))}
+    <div class="overlay"><div class="overlay-text">${_('Submitting...')}</div></div>
+      ${h.form(h.url('changeset_comment', repo_name=c.repo_name, revision=changeset.raw_id),class_='inline-form')}
       <div class="clearfix">
           <div class="comment-help">${_('Commenting on line')} {1}. ${_('Comments parsed using')}
           <a href="${h.url('rst_help')}">RST</a> ${_('syntax')} ${_('with')}
       <div class="comment-button">
       <input type="hidden" name="f_path" value="{0}">
       <input type="hidden" name="line" value="{1}">
-      ${h.submit('save', _('Comment'), class_='ui-btn')}
+      ${h.submit('save', _('Comment'), class_='ui-btn save-inline-form')}
       ${h.reset('hide-inline-form', _('Hide'), class_='ui-btn hide-inline-form')}
       </div>
       ${h.end_form()}
 </%def>
 
 
-<%def name="comments(changeset)">
-
-<div class="comments">
+<%def name="inlines(changeset)">
     <div class="comments-number">${len(c.comments)} comment(s) (${c.inline_cnt} ${_('inline')})</div>
-
     %for path, lines in c.inline_comments:
-        <div style="display:none" class="inline-comment-placeholder" path="${path}" target_id="${h.FID(changeset.raw_id,path)}">
         % for line,comments in lines.iteritems():
-            <div class="inline-comment-placeholder-line" line="${line}" target_id="${h.safeid(h.safe_unicode(path))}">
+            <div style="display:none" class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
             %for co in comments:
                 ${comment_block(co)}
             %endfor
             </div>
         %endfor
-        </div>
     %endfor
+    
+</%def>
 
+<%def name="comments(changeset)">
+
+<div class="comments">
+    <div id="inline-comments-container">
+     ${inlines(changeset)}
+    </div>
+    
     %for co in c.comments:
         ${comment_block(co)}
     %endfor
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.