summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorByron Jones <glob@mozilla.com>2015-06-02 07:25:33 +0200
committerByron Jones <glob@mozilla.com>2015-06-02 07:25:33 +0200
commit646d6199a644a1e6d65706c400163d00fa310bfe (patch)
tree4ae677220b8464dc99c29b5ee7609db15fc1befc
parent4c751704c9644faf357adeea13584b08a359593d (diff)
downloadbugzilla-646d6199a644a1e6d65706c400163d00fa310bfe.tar.gz
bugzilla-646d6199a644a1e6d65706c400163d00fa310bfe.tar.xz
Bug 1146771: implement comment tagging
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl45
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/header.html.tmpl4
-rw-r--r--extensions/BugModal/web/bug_modal.css32
-rw-r--r--extensions/BugModal/web/bug_modal.js47
-rw-r--r--extensions/BugModal/web/comments.js262
-rw-r--r--extensions/ProdCompSearch/web/js/prod_comp_search.js6
-rw-r--r--js/field.js11
-rw-r--r--js/global.js23
8 files changed, 394 insertions, 36 deletions
diff --git a/extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl
index 2e7b3c87b..11442985c 100644
--- a/extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl
+++ b/extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl
@@ -36,8 +36,21 @@
END;
%]
+[% IF user.can_tag_comments %]
+ <div id="ctag" style="display:none">
+ <input id="ctag-add" size="10" placeholder="add tag"
+ maxlength="[% constants.MAX_COMMENT_TAG_LENGTH FILTER html %]">
+ <button type="button" id="ctag-close" class="minor">X</button>
+ <a href="https://wiki.mozilla.org/BMO/comment_tagging" target="_blank" title="About Comment Tagging">Help</a>
+ </div>
+ <div id="ctag-error" style="display:none">
+ <a href="#" class="close-btn" data-for="ctag-error">x</a>
+ <span id="ctag-error-message"></span>
+ </div>
+[% END %]
+
[% BLOCK comment_header %]
- <div class="comment">
+ <div class="comment" data-id="[% comment.id FILTER none %]" data-no="[% comment.count FILTER none %]">
[%# normal comment header %]
<table class="layout-table change-head [% extra_class FILTER none %]" id="ch-[% comment.count FILTER none %]"
[% IF comment.collapsed +%] style="display:none"[% END %]>
@@ -49,6 +62,7 @@
gravatar_only = 1
%]
</td>
+
<td class="change-author">
[% INCLUDE bug_modal/user.html.tmpl
u = comment.author
@@ -58,6 +72,7 @@
[% END %]
[% Hook.process('user', 'bug/comments.html.tmpl') %]
</td>
+
<td class="comment-actions">
[% IF user.is_insider && bug.check_can_change_field('longdesc', 0, 1) %]
[% IF comment.is_private %]
@@ -74,7 +89,13 @@
</div>
[% END %]
[% IF user.id %]
- <button class="reply-btn minor"
+ [% IF user.can_tag_comments %]
+ <button class="tag-btn minor" type="button"
+ data-id="[% comment.id FILTER none %]"
+ data-no="[% comment.count FILTER none %]"
+ >Tag</button>
+ [% END %]
+ <button class="reply-btn minor" type="button"
data-reply-id="[% comment.count FILTER none %]"
data-reply-name="[% comment.author.name || comment.author.moz_nick FILTER html %]"
>Reply</button>
@@ -82,6 +103,7 @@
<button class="comment-spinner minor" id="cs-[% comment.count FILTER none%]">-</button>
</td>
</tr>
+
<tr>
<td colspan="2">
<div class="change-name">
@@ -95,15 +117,16 @@
</div>
</td>
</tr>
- [% IF comment.tags.size %]
- <tr>
- <td colspan="2" class="comment-tags">
- [% FOREACH tag IN comment.tags %]
- <span class="comment-tag">[% tag FILTER html %]</span>
- [% END %]
- </td>
- </tr>
- [% END %]
+
+ <tr id="ctag-[% comment.count FILTER none %]">
+ <td colspan="2" class="comment-tags">
+ [% FOREACH tag IN comment.tags ~%]
+ <span class="comment-tag">
+ [%~ "<a>x</a>" IF user.can_tag_comments %][% tag FILTER html ~%]
+ </span>
+ [%~ END %]
+ </td>
+ </tr>
</table>
[%# default-collapsed comment header %]
diff --git a/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl
index 13c146ed5..a1aee3cfd 100644
--- a/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl
+++ b/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl
@@ -50,6 +50,7 @@
javascript_urls.push(
"extensions/ProdCompSearch/web/js/prod_comp_search.js",
"extensions/BugModal/web/bug_modal.js",
+ "extensions/BugModal/web/comments.js",
"extensions/BugModal/web/ZeroClipboard/ZeroClipboard.min.js",
"js/field.js",
"js/comments.js",
@@ -93,6 +94,7 @@
id: [% user.id FILTER none %],
login: '[% user.login FILTER js %]',
is_insider: [% user.is_insider ? "true" : "false" %],
+ can_tag: [% user.can_tag_comments ? "true" : "false" %],
settings: {
quote_replies: '[% user.settings.quote_replies.value FILTER js %]',
zoom_textareas: [% user.settings.zoom_textareas.value == "on" ? "true" : "false" %]
@@ -101,6 +103,8 @@
[% IF user.id %]
BUGZILLA.default_assignee = '[% bug.component_obj.default_assignee.login FILTER js %]';
BUGZILLA.default_qa_contact = '[% bug.component_obj.default_qa_contact.login FILTER js %]';
+ BUGZILLA.constant.min_comment_tag_length = [% constants.MIN_COMMENT_TAG_LENGTH FILTER none %];
+ BUGZILLA.constant.max_comment_tag_length = [% constants.MAX_COMMENT_TAG_LENGTH FILTER none %];
[% END %]
[% END %]
diff --git a/extensions/BugModal/web/bug_modal.css b/extensions/BugModal/web/bug_modal.css
index afa4719c4..b02fc6067 100644
--- a/extensions/BugModal/web/bug_modal.css
+++ b/extensions/BugModal/web/bug_modal.css
@@ -545,13 +545,37 @@ td.flag-requestee {
}
.comment-tag {
- border: 1px solid #ccc;
- padding: 2px 4px;
+ border: 1px solid #eee;
+ padding: 2px 6px 2px 4px;
margin-right: 2px;
- border-radius: 0.5em;
+ border-radius: 4px;
background-color: #fff;
color: #000;
- font-size: 12px;
+}
+
+.comment-tag a {
+ padding-right: 4px;
+ cursor: pointer;
+}
+
+#ctag {
+ margin-bottom: 4px;
+}
+
+#ctag button {
+ margin-top: 2px;
+}
+
+#ctag a {
+ margin-left: 8px;
+}
+
+#ctag-error {
+ padding-left: 5px;
+ background-color: #faa;
+ color: #444;
+ border-radius: 2px;
+ margin-top: 2px;
}
.comment-collapse-reason {
diff --git a/extensions/BugModal/web/bug_modal.js b/extensions/BugModal/web/bug_modal.js
index a9ff51452..9d4cd83d0 100644
--- a/extensions/BugModal/web/bug_modal.js
+++ b/extensions/BugModal/web/bug_modal.js
@@ -111,7 +111,7 @@ $(function() {
$('#cc-' + id).hide();
$('#ch-' + id).show();
}
- $('#ct-' + id).slideToggle('fast', function() {
+ $('#ct-' + id + ', #ctag-' + id).slideToggle('fast', function() {
$('#c' + id).find('.activity').toggle();
spinner.text($('#ct-' + id + ':visible').length ? '-' : '+');
});
@@ -345,11 +345,7 @@ $(function() {
autoSelectFirst: true,
formatResult: function(suggestion, currentValue) {
// disable <b> wrapping of matched substring
- return suggestion.value
- .replace(/&/g, '&amp;')
- .replace(/</g, '&lt;')
- .replace(/>/g, '&gt;')
- .replace(/"/g, '&quot;');
+ return suggestion.value.htmlEncode();
},
onSelect: function() {
this.focus();
@@ -1067,7 +1063,7 @@ function bugzilla_ajax(request, done_fn, error_fn) {
request.data = JSON.stringify(request.data);
}
}
- $.ajax(request)
+ return $.ajax(request)
.done(function(data) {
if (data.error) {
$('#xhr-error').html(data.message);
@@ -1080,6 +1076,8 @@ function bugzilla_ajax(request, done_fn, error_fn) {
}
})
.error(function(data) {
+ if (data.statusText === 'abort')
+ return;
var message = data.responseJSON ? data.responseJSON.message : 'Unexpected Error'; // all errors are unexpected :)
if (!request.hideError) {
$('#xhr-error').html(message);
@@ -1137,6 +1135,41 @@ function lb_close(event) {
$('#lb_overlay, #lb_overlay2, #lb_close_btn, #lb_img, #lb_text').remove();
}
+// extensions
+
+(function($) {
+ $.extend({
+ // Case insensative $.inArray (http://api.jquery.com/jquery.inarray/)
+ // $.inArrayIn(value, array [, fromIndex])
+ // value (type: String)
+ // The value to search for
+ // array (type: Array)
+ // An array through which to search.
+ // fromIndex (type: Number)
+ // The index of the array at which to begin the search.
+ // The default is 0, which will search the whole array.
+ inArrayIn: function(elem, arr, i) {
+ // not looking for a string anyways, use default method
+ if (typeof elem !== 'string') {
+ return $.inArray.apply(this, arguments);
+ }
+ // confirm array is populated
+ if (arr) {
+ var len = arr.length;
+ i = i ? (i < 0 ? Math.max(0, len + i) : i) : 0;
+ elem = elem.toLowerCase();
+ for (; i < len; i++) {
+ if (i in arr && arr[i].toLowerCase() == elem) {
+ return i;
+ }
+ }
+ }
+ // stick with inArray/indexOf and return -1 on no match
+ return -1;
+ }
+ });
+})(jQuery);
+
// no-ops
function initHidingOptionsForIE() {}
function showFieldWhen() {}
diff --git a/extensions/BugModal/web/comments.js b/extensions/BugModal/web/comments.js
new file mode 100644
index 000000000..1f0b18696
--- /dev/null
+++ b/extensions/BugModal/web/comments.js
@@ -0,0 +1,262 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+$(function() {
+ 'use strict';
+
+ // comment collapse/expand
+ $('.comment-spinner')
+ .click(function(event) {
+ event.preventDefault();
+ var spinner = $(event.target);
+ var id = spinner.attr('id').match(/\d+$/)[0];
+ // switch to full header for initially collapsed comments
+ if (spinner.attr('id').match(/^ccs-/)) {
+ $('#cc-' + id).hide();
+ $('#ch-' + id).show();
+ }
+ $('#ct-' + id + ', #ctag-' + id).slideToggle('fast', function() {
+ $('#c' + id).find('.activity').toggle();
+ spinner.text($('#ct-' + id + ':visible').length ? '-' : '+');
+ });
+ });
+
+ //
+ // anything after this point is only executed for logged in users
+ //
+
+ if (BUGZILLA.user.id === 0) return;
+
+ // comment tagging
+
+ function taggingError(commentNo, message) {
+ $('#ctag-' + commentNo + ' .comment-tags').append($('#ctag-error'));
+ $('#ctag-error-message').text(message);
+ $('#ctag-error').show();
+ }
+
+ function deleteTag(event) {
+ event.preventDefault();
+ $('#ctag-error').hide();
+
+ var that = $(this);
+ var comment = that.parents('.comment');
+ var commentNo = comment.data('no');
+ var commentID = comment.data('id');
+ var tag = that.parent('.comment-tag').contents().filter(function() {
+ return this.nodeType === 3;
+ }).text();
+ var container = that.parents('.comment-tags');
+
+ // update ui
+ that.parent('.comment-tag').remove();
+ renderTags(commentNo, tagsFromDom(container));
+
+ // update bugzilla
+ bugzilla_ajax(
+ {
+ url: 'rest/bug/comment/' + commentID + '/tags',
+ type: 'PUT',
+ data: { remove: [ tag ] },
+ hideError: true
+ },
+ function(data) {
+ renderTags(commentNo, data);
+ },
+ function(message) {
+ taggingError(commentNo, message);
+ }
+ );
+ }
+ $('.comment-tag a').click(deleteTag);
+
+ function tagsFromDom(commentTagsDiv) {
+ return commentTagsDiv
+ .find('.comment-tag')
+ .contents()
+ .filter(function() { return this.nodeType === 3; })
+ .map(function() { return $(this).text(); })
+ .toArray();
+ }
+
+ function renderTags(commentNo, tags) {
+ cancelRefresh();
+ var root = $('#ctag-' + commentNo + ' .comment-tags');
+ root.find('.comment-tag').remove();
+ $.each(tags, function() {
+ var span = $('<span/>').addClass('comment-tag').text(this);
+ if (BUGZILLA.user.can_tag) {
+ span.prepend($('<a>x</a>').click(deleteTag));
+ }
+ root.append(span);
+ });
+ $('#ctag-' + commentNo + ' .comment-tags').append($('#ctag-error'));
+ }
+
+ var refreshXHR;
+
+ function refreshTags(commentNo, commentID) {
+ cancelRefresh();
+ refreshXHR = bugzilla_ajax(
+ {
+ url: 'rest/bug/comment/' + commentID + '?include_fields=tags',
+ hideError: true
+ },
+ function(data) {
+ refreshXHR = false;
+ renderTags(commentNo, data.comments[commentID].tags);
+ },
+ function(message) {
+ refreshXHR = false;
+ taggingError(commentNo, message);
+ }
+ );
+ }
+
+ function cancelRefresh() {
+ if (refreshXHR) {
+ refreshXHR.abort();
+ refreshXHR = false;
+ }
+ }
+
+ $('#ctag-add')
+ .devbridgeAutocomplete({
+ serviceUrl: function(query) {
+ return 'rest/bug/comment/tags/' + encodeURIComponent(query);
+ },
+ params: {
+ Bugzilla_api_token: (BUGZILLA.api_token ? BUGZILLA.api_token : '')
+ },
+ deferRequestBy: 250,
+ minChars: 3,
+ tabDisabled: true,
+ autoSelectFirst: true,
+ triggerSelectOnValidInput: false,
+ transformResult: function(response) {
+ response = $.parseJSON(response);
+ return {
+ suggestions: $.map(response, function(tag) {
+ return { value: tag };
+ })
+ };
+ },
+ formatResult: function(suggestion, currentValue) {
+ // disable <b> wrapping of matched substring
+ return suggestion.value.htmlEncode();
+ }
+ })
+ .keydown(function(event) {
+ if (event.which === 27) {
+ event.preventDefault();
+ $('#ctag-close').click();
+ }
+ else if (event.which === 13) {
+ event.preventDefault();
+ $('#ctag-error').hide();
+
+ var ctag = $('#ctag');
+ var newTags = $('#ctag-add').val().trim().split(/[ ,]/);
+ var commentNo = ctag.data('commentNo');
+ var commentID = ctag.data('commentID');
+
+ $('#ctag-close').click();
+
+ // update ui
+ var tags = tagsFromDom($(this).parents('.comment-tags'));
+ var dirty = false;
+ var addTags = [];
+ $.each(newTags, function(index, value) {
+ if ($.inArrayIn(value, tags) == -1)
+ addTags.push(value);
+ });
+ if (addTags.length === 0)
+ return;
+
+ // validate
+ try {
+ $.each(addTags, function(index, value) {
+ if (value.length < BUGZILLA.constant.min_comment_tag_length) {
+ throw 'Comment tags must be at least ' +
+ BUGZILLA.constant.min_comment_tag_length + ' characters.';
+ }
+ if (value.length > BUGZILLA.constant.max_comment_tag_length) {
+ throw 'Comment tags cannot be longer than ' +
+ BUGZILLA.constant.min_comment_tag_length + ' characters.';
+ }
+ });
+ } catch(ex) {
+ taggingError(commentNo, ex);
+ return;
+ }
+
+ Array.prototype.push.apply(tags, addTags);
+ tags.sort();
+ renderTags(commentNo, tags);
+
+ // update bugzilla
+ bugzilla_ajax(
+ {
+ url: 'rest/bug/comment/' + commentID + '/tags',
+ type: 'PUT',
+ data: { add: addTags },
+ hideError: true
+ },
+ function(data) {
+ renderTags(commentNo, data);
+ },
+ function(message) {
+ taggingError(commentNo, message);
+ refreshTags(commentNo, commentID);
+ }
+ );
+ }
+ });
+
+ $('#ctag-close')
+ .click(function(event) {
+ event.preventDefault();
+ $('#ctag').hide().data('commentNo', '');
+ });
+
+ $('.tag-btn')
+ .click(function(event) {
+ event.preventDefault();
+ var that = $(this);
+ var commentNo = that.data('no');
+ var commentID = that.data('id');
+ var ctag = $('#ctag');
+ $('#ctag-error').hide();
+
+ // toggle -> hide
+ if (ctag.data('commentNo') === commentNo) {
+ ctag.hide().data('commentNo', '');
+ window.focus();
+ return;
+ }
+ ctag.data('commentNo', commentNo);
+ ctag.data('commentID', commentID);
+
+ // kick off a refresh of the tags
+ refreshTags(commentNo, commentID);
+
+ // expand collapsed comments
+ if ($('#ct-' + commentNo + ':visible').length === 0) {
+ $('#cs-' + commentNo + ', #ccs-' + commentNo).click();
+ }
+
+ // move, show, and focus tagging ui
+ ctag.prependTo('#ctag-' + commentNo + ' .comment-tags').show();
+ $('#ctag-add').val('').focus();
+ });
+
+ $('.close-btn')
+ .click(function(event) {
+ event.preventDefault();
+ $('#' + $(this).data('for')).hide();
+ });
+});
diff --git a/extensions/ProdCompSearch/web/js/prod_comp_search.js b/extensions/ProdCompSearch/web/js/prod_comp_search.js
index 69cc7cc0b..efbaab81b 100644
--- a/extensions/ProdCompSearch/web/js/prod_comp_search.js
+++ b/extensions/ProdCompSearch/web/js/prod_comp_search.js
@@ -99,11 +99,7 @@ $(function() {
},
formatResult: function(suggestion, currentValue) {
var value = (suggestion.data.component ? suggestion.data.component : suggestion.data.product);
- var escaped = value
- .replace(/&/g, '&amp;')
- .replace(/</g, '&lt;')
- .replace(/>/g, '&gt;')
- .replace(/"/g, '&quot;');
+ var escaped = value.htmlEncode();
if (suggestion.data.component) {
return '-&nbsp;' + escaped;
}
diff --git a/js/field.js b/js/field.js
index 778451daf..fdacd4728 100644
--- a/js/field.js
+++ b/js/field.js
@@ -738,10 +738,7 @@ $(function() {
formatResult: function(suggestion, currentValue) {
return (suggestion.data.name === '' ?
suggestion.data.login : suggestion.data.name + ' (' + suggestion.data.login + ')')
- .replace(/&/g, '&amp;')
- .replace(/</g, '&lt;')
- .replace(/>/g, '&gt;')
- .replace(/"/g, '&quot;');
+ .htmlEncode();
},
onSearchStart: function(params) {
var that = $(this);
@@ -800,11 +797,7 @@ $(function() {
autoSelectFirst: true,
formatResult: function(suggestion, currentValue) {
// disable <b> wrapping of matched substring
- return suggestion.value
- .replace(/&/g, '&amp;')
- .replace(/</g, '&lt;')
- .replace(/>/g, '&gt;')
- .replace(/"/g, '&quot;');
+ return suggestion.value.htmlEncode();
},
onSelect: function() {
this.focus();
diff --git a/js/global.js b/js/global.js
index 7675fd98a..7ecd3d901 100644
--- a/js/global.js
+++ b/js/global.js
@@ -87,3 +87,26 @@ function display_value(field, value) {
if (translated) return translated;
return value;
}
+
+// polyfill .trim
+if (!String.prototype.trim) {
+ (function() {
+ // Make sure we trim BOM and NBSP
+ var rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;
+ String.prototype.trim = function() {
+ return this.replace(rtrim, '');
+ };
+ })();
+}
+
+// html encoding
+if (!String.prototype.htmlEncode) {
+ (function() {
+ String.prototype.htmlEncode = function() {
+ return this.replace(/&/g, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;')
+ .replace(/"/g, '&quot;');
+ };
+ })();
+}