WIP mozreview: add inline-comments js, html, css (bug 1115707); r!davidwalsh,smacleod draft
authorbyron jones <glob@mozilla.com>
Thu, 25 Aug 2016 14:12:33 +0800
changeset 9468 9b745b9a4e464165f848855ddcd26ec018d0ed32
parent 9467 f63bc4097a9ff6e528236007d1f823460dca4706
child 9469 ff48d7ea8908901dee9c04e32e61e5fe26af9a8a
push id1188
push userbjones@mozilla.com
push dateThu, 01 Sep 2016 13:14:49 +0000
bugs1115707
WIP mozreview: add inline-comments js, html, css (bug 1115707); r!davidwalsh,smacleod Add core support for showing comments inline on the diff view. MozReview-Commit-ID: HWeg6ZAEt8f
reviewboard/reviewboard/static/rb/css/pages/diffviewer.less
reviewboard/reviewboard/static/rb/js/views/inlineCommentView.js
reviewboard/reviewboard/staticbundles.py
reviewboard/reviewboard/templates/diffviewer/inline_comments.html
--- a/reviewboard/reviewboard/static/rb/css/pages/diffviewer.less
+++ b/reviewboard/reviewboard/static/rb/css/pages/diffviewer.less
@@ -828,9 +828,72 @@
   color: @paginate-text-color;
   font-weight: bold;
 }
 
 #pagination2 {
   margin-top: 1em;
 }
 
+
+/****************************************************************************
+ * Inline Comments
+ ****************************************************************************/
+.inlineCommentRow {
+  .inlineComments {
+    border-top: 1px solid #888;
+    border-bottom: 1px solid #888;
+
+    &:hover {
+      outline: 1px solid #000;
+    }
+
+    .inlineComment {
+      cursor: pointer;
+      background: #f0f0f0;
+      padding: 2px;
+      border-right: 0;
+      font-size: 8pt;
+
+      .meta  {
+        display: inline-block;
+        overflow: hidden;
+        vertical-align: bottom;
+        height: 16px;  // icon height
+      }
+
+      .comment {
+        display: inline-block;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        height: 16px;  // icon height
+        width: 250px;  // calculated on window.resize
+        color: #888;
+        vertical-align: bottom;
+        margin-left: 4px;
+
+        p {
+          margin: 0;
+        }
+      }
+    }
+  }
+}
+
+.sidebyside {
+  tbody {
+    tr.inlineCommentRow {
+      th, td {
+        padding: 0;
+        cursor: default;
+      }
+    }
+  }
+}
+
+tr.selected-draft {
+  th:nth-child(3) {
+    outline: 1px dotted #000;
+  }
+}
+
 // vim: set et ts=2 sw=2:
new file mode 100644
--- /dev/null
+++ b/reviewboard/reviewboard/static/rb/js/views/inlineCommentView.js
@@ -0,0 +1,207 @@
+RB.InlineCommentView = RB.AbstractCommentBlockView.extend({
+    tagName: 'tr',
+    className: 'inlineCommentRow',
+
+    initialize: function() {
+        var view = this;
+        this.rowTemplate = _.template($('#inlineComments-row').html());
+        this.commentTemplate = _.template($('#inlineComments-comment').html());
+        this.draftTemplate = _.template($('#inlineComments-draft').html());
+        this.$beginRow = null;
+        this.$endRow = null;
+        this.isDraft = false;
+        this.contextRows = [];
+        _.bindAll(this, '_updateSize');
+
+        // Detect when chunks are expanded/collapsed to update visible rows.
+        $.each(RB.PageManager.getPage()._diffReviewableViews, function() {
+            this.on('chunkExpansionChanged', function() {
+                view._updateContextRows();
+                view._updateSize();
+            });
+        });
+
+        // Update icon when issue statuses change.
+        RB.PageManager.getPage().commentIssueManager.on('issueStatusUpdated',
+            function(comment) {
+                $('#c' + comment.id + ' img')[0].className =
+                    view._commentIconClass(
+                        comment.get('issueOpened'), comment.get('issueStatus'));
+            });
+
+        // Don't recalculate the sizes on _every_ window.resize event.
+        this.lazyUpdateSize = _.debounce(this._updateSize, 100);
+    },
+
+    renderContent: function() {
+        var view = this;
+        var commentHtml = [];
+        this.isDraft = !!this.model.get('draftComment');
+        console.log(this);  // XXX debugging
+
+        // Render row with each comment.
+        if (this.isDraft) {
+            commentHtml.push(view.draftTemplate({
+                user_name: RB.UserSession.instance.get('username')
+            }));
+        } else {
+            _.each(this.model.get('serializedComments'), function(comment) {
+                commentHtml.push(view.commentTemplate({
+                    id: comment.comment_id,
+                    user_name: comment.user.username,
+                    comment_html: comment.html,
+                    icon_class: view._commentIconClass(
+                        comment.issue_opened, comment.issue_status)
+                }));
+            });
+        }
+        this.$el
+            .html(this.rowTemplate({
+                commentHtml: commentHtml.join('')
+            }))
+            .prop('id', this.cid);
+
+        this.$('.inlineComments')
+            .click(function() {
+                view.trigger('clicked');
+            })
+            .hover(
+                function() {
+                    _.each(view.contextRows, function($row) {
+                        $row.addClass('selected');
+                    });
+                },
+                function() {
+                    _.each(view.contextRows, function($row) {
+                        $row.removeClass('selected');
+                    });
+                }
+            );
+
+        if (this.isDraft) {
+            this.$el.find('.comment')
+                .bindProperty('text', this.model.get('draftComment'), 'text', {
+                    elementToModel: false
+                });
+        }
+
+        $(window).on('resize', this.lazyUpdateSize);
+        this.lazyUpdateSize();
+    },
+
+    remove: function() {
+        Backbone.View.prototype.remove.call(this);
+        $(window).off('resize', this.lazyUpdateSize);
+    },
+
+    setRows: function($beginRow, $endRow) {
+        var view = this;
+        this.$beginRow = $beginRow;
+        this.$endRow = $endRow;
+        this._updateContextRows();
+
+        // Highlight selected rows on drafts.
+        if (this.isDraft) {
+            _.each(view.contextRows, function($row) {
+                $row.addClass('selected-draft');
+            });
+        }
+    },
+
+    _commentIconClass: function(issueOpened, issueStatus) {
+        return 'rb-icon ' +
+            (issueOpened
+                ? 'rb-icon-issue-' + issueStatus
+                : 'rb-icon-datagrid-comment');
+    },
+
+    positionCommentDlg: function(commentDlg) {
+        commentDlg.$el.css({
+            left: $(document).scrollLeft() +
+                    ($(window).width() - commentDlg.$el.width()) / 2,
+            top: this.$endRow.offset().top + this.$endRow.height()
+        });
+    },
+
+    buildAnchorName: function() {
+        return 'file' + this.model.get('fileDiffID') + 'line' +
+            this.model.get('beginLineNum');
+    },
+
+    _updateContextRows: function () {
+        if (!(this.$beginRow && this.$endRow)) {
+            return;
+        }
+        // Keep track of the rows this comment refers to.
+        var beginLineNum = this.$beginRow.attr('line');
+        var endLineNum = this.$endRow.attr('line');
+        this.contextRows = [];
+        for (var lineNum = beginLineNum; lineNum <= endLineNum; lineNum++) {
+            var $tr = $('tr[line="' + lineNum + '"]');
+            if ($tr.length) {
+                this.contextRows.push($tr);
+            }
+        }
+    },
+
+    _updateSize: function() {
+        if (!(this.$beginRow && this.$endRow)) {
+            return;
+        }
+        // Show as much comment context as possible.
+        // Half the row, minus line number columns, minus name.
+        var colWidth = $('tr[line]').first().width() / 2 - (25 + 25 + 100);
+        this.$el
+            .find('.inlineComment .comment')
+            .width(colWidth - 20);
+    },
+
+    dispose: function() {
+        if (this.isDraft) {
+            _.each(this.contextRows, function($row) {
+                $row.removeClass('selected-draft');
+            });
+        }
+        this.remove();
+    },
+
+    render: function() {
+        this.renderContent();
+        this.model.on('change:draftComment', this._onDraftCommentChanged, this);
+        this._onDraftCommentChanged();
+        return this;
+    },
+
+    notify: function() {
+        // Ignore requests to show a notification bubble.
+    },
+
+    _onDraftCommentChanged: function() {
+        var self = this,
+            $el = this.$el,
+            comment = this.model.get('draftComment');
+
+        if (!comment) {
+            $el.removeClass('draft');
+            return;
+        }
+
+        comment.on('destroy', function() {
+            // Discard the comment block if empty.
+            if (this.model.isEmpty()) {
+                $el.fadeOut(150, function() { self.dispose(); });
+            } else {
+                $el.removeClass('draft');
+            }
+        }, this);
+
+        comment.on('saved', function() {
+            _.each(self.contextRows, function($row) {
+                $row.removeClass('selected-draft');
+            });
+            RB.DraftReviewBannerView.instance.show();
+        }, this);
+
+        $el.addClass('draft');
+    }
+});
--- a/reviewboard/reviewboard/staticbundles.py
+++ b/reviewboard/reviewboard/staticbundles.py
@@ -230,16 +230,17 @@ PIPELINE_JS = dict({
             'rb/js/views/reviewReplyDraftBannerView.js',
             'rb/js/views/reviewReplyEditorView.js',
             'rb/js/views/reviewRequestEditorView.js',
             'rb/js/views/screenshotThumbnailView.js',
             'rb/js/views/imageReviewableView.js',
             'rb/js/views/dummyReviewableView.js',
             'rb/js/views/textBasedCommentBlockView.js',
             'rb/js/views/textBasedReviewableView.js',
+            'rb/js/views/inlineCommentView.js',
             'rb/js/views/textCommentRowSelector.js',
             'rb/js/views/markdownReviewableView.js',
             'rb/js/views/uploadDiffView.js',
             'rb/js/views/updateDiffView.js',
             'rb/js/diffviewer/models/diffCommentBlockModel.js',
             'rb/js/diffviewer/models/diffCommentsHintModel.js',
             'rb/js/diffviewer/models/diffFileModel.js',
             'rb/js/diffviewer/models/diffReviewableModel.js',
new file mode 100644
--- /dev/null
+++ b/reviewboard/reviewboard/templates/diffviewer/inline_comments.html
@@ -0,0 +1,27 @@
+
+<script type="text/template" id="inlineComments-row">
+  <th></th>
+  <td></td>
+  <th></th>
+  <td class="inlineComments"><%= commentHtml %></td>
+</script>
+
+<script type="text/template" id="inlineComments-comment">
+  <div class="inlineComment" id="c<%- id %>">
+    <div class="meta">
+      <span class="icon"><img class="<%- icon_class %>" width="16" height="16"></span>
+      <span class="user"><%- user_name %></span>
+    </div>
+    <div class="comment"><%= comment_html %></div>
+  </div>
+</script>
+
+<script type="text/template" id="inlineComments-draft">
+  <div class="inlineComment">
+    <div class="meta">
+      <span class="icon"><img class="rb-icon rb-icon-datagrid-comment-draft" width="16" height="16"></span>
+      <span class="user"><%- user_name %></span>
+    </div>
+    <div class="comment"></div>
+  </div>
+</script>