summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Bugzilla/Bug.pm36
-rw-r--r--Bugzilla/Comment.pm195
-rw-r--r--Bugzilla/Comment/TagWeights.pm74
-rw-r--r--Bugzilla/Config/Common.pm5
-rw-r--r--Bugzilla/Config/GroupSecurity.pm1
-rw-r--r--Bugzilla/Constants.pm7
-rw-r--r--Bugzilla/Field.pm1
-rw-r--r--Bugzilla/User.pm17
-rw-r--r--Bugzilla/WebService/Bug.pm80
-rw-r--r--Bugzilla/WebService/Constants.pm7
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Bug.pm16
-rw-r--r--js/comment-tagging.js385
-rw-r--r--js/comments.js19
-rw-r--r--js/util.js25
-rwxr-xr-xshow_activity.cgi4
-rw-r--r--skins/contrib/Mozilla/global.css29
-rw-r--r--skins/standard/show_bug.css39
-rw-r--r--template/en/default/admin/params/bugfields.html.tmpl6
-rw-r--r--template/en/default/admin/params/groupsecurity.html.tmpl3
-rw-r--r--template/en/default/bug/comments.html.tmpl54
-rw-r--r--template/en/default/bug/edit.html.tmpl25
-rw-r--r--template/en/default/bug/show-header.html.tmpl3
-rw-r--r--template/en/default/filterexceptions.pl1
-rw-r--r--template/en/default/global/user-error.html.tmpl23
24 files changed, 1010 insertions, 45 deletions
diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm
index 1c3b0267c..51035ce5a 100644
--- a/Bugzilla/Bug.pm
+++ b/Bugzilla/Bug.pm
@@ -3458,7 +3458,8 @@ sub comments {
$comment->{count} = $count++;
$comment->{bug} = $self;
}
- Bugzilla::Comment->preload($self->{'comments'});
+ # Some bugs may have no comments when upgrading old installations.
+ Bugzilla::Comment->preload($self->{'comments'}) if $count;
}
return unless defined wantarray;
@@ -3879,7 +3880,7 @@ sub _bugs_in_order {
# Get the activity of a bug, starting from $starttime (if given).
# This routine assumes Bugzilla::Bug->check has been previously called.
sub GetBugActivity {
- my ($bug_id, $attach_id, $starttime) = @_;
+ my ($bug_id, $attach_id, $starttime, $include_comment_tags) = @_;
my $dbh = Bugzilla->dbh;
# Arguments passed to the SQL query.
@@ -3890,7 +3891,7 @@ sub GetBugActivity {
if (defined $starttime) {
trick_taint($starttime);
push (@args, $starttime);
- $datepart = "AND bugs_activity.bug_when > ?";
+ $datepart = "AND bug_when > ?";
}
my $attachpart = "";
@@ -3911,7 +3912,7 @@ sub GetBugActivity {
my $query = "SELECT fielddefs.name, bugs_activity.attach_id, " .
$dbh->sql_date_format('bugs_activity.bug_when', '%Y.%m.%d %H:%i:%s') .
- ", bugs_activity.removed, bugs_activity.added, profiles.login_name,
+ " AS bug_when, bugs_activity.removed, bugs_activity.added, profiles.login_name,
bugs_activity.comment_id
FROM bugs_activity
$suppjoins
@@ -3922,8 +3923,31 @@ sub GetBugActivity {
WHERE bugs_activity.bug_id = ?
$datepart
$attachpart
- $suppwhere
- ORDER BY bugs_activity.bug_when, bugs_activity.id";
+ $suppwhere ";
+
+ if (Bugzilla->params->{'comment_taggers_group'}
+ && $include_comment_tags
+ && !$attach_id)
+ {
+ $query .= "
+ UNION ALL
+ SELECT 'comment_tag' AS name,
+ NULL AS attach_id," .
+ $dbh->sql_date_format('longdescs_tags_activity.bug_when', '%Y.%m.%d %H:%i:%s') . " AS bug_when,
+ longdescs_tags_activity.removed,
+ longdescs_tags_activity.added,
+ profiles.login_name,
+ longdescs_tags_activity.comment_id as comment_id
+ FROM longdescs_tags_activity
+ INNER JOIN profiles ON profiles.userid = longdescs_tags_activity.who
+ WHERE longdescs_tags_activity.bug_id = ?
+ $datepart
+ ";
+ push @args, $bug_id;
+ push @args, $starttime if defined $starttime;
+ }
+
+ $query .= "ORDER BY bug_when, comment_id";
my $list = $dbh->selectall_arrayref($query, undef, @args);
diff --git a/Bugzilla/Comment.pm b/Bugzilla/Comment.pm
index 4559cf1fd..623796142 100644
--- a/Bugzilla/Comment.pm
+++ b/Bugzilla/Comment.pm
@@ -26,11 +26,13 @@ package Bugzilla::Comment;
use base qw(Bugzilla::Object);
use Bugzilla::Attachment;
+use Bugzilla::Comment::TagWeights;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::User;
use Bugzilla::Util;
+use List::Util qw(first);
use Scalar::Util qw(blessed);
###############################
@@ -92,21 +94,90 @@ use constant VALIDATOR_DEPENDENCIES => {
sub update {
my $self = shift;
- my $changes = $self->SUPER::update(@_);
- $self->bug->_sync_fulltext( update_comments => 1);
+ my ($changes, $old_comment) = $self->SUPER::update(@_);
+
+ if (exists $changes->{'thetext'} || exists $changes->{'isprivate'}) {
+ $self->bug->_sync_fulltext( update_comments => 1);
+ }
+
+ my @old_tags = @{ $old_comment->tags };
+ my @new_tags = @{ $self->tags };
+ my ($removed_tags, $added_tags) = diff_arrays(\@old_tags, \@new_tags);
+
+ if (@$removed_tags || @$added_tags) {
+ my $dbh = Bugzilla->dbh;
+ my $when = $dbh->selectrow_array("SELECT LOCALTIMESTAMP(0)");
+ my $sth_delete = $dbh->prepare(
+ "DELETE FROM longdescs_tags WHERE comment_id = ? AND tag = ?"
+ );
+ my $sth_insert = $dbh->prepare(
+ "INSERT INTO longdescs_tags(comment_id, tag) VALUES (?, ?)"
+ );
+ my $sth_activity = $dbh->prepare(
+ "INSERT INTO longdescs_tags_activity
+ (bug_id, comment_id, who, bug_when, added, removed)
+ VALUES (?, ?, ?, ?, ?, ?)"
+ );
+
+ foreach my $tag (@$removed_tags) {
+ my $weighted = Bugzilla::Comment::TagWeights->new({ name => $tag });
+ if ($weighted) {
+ if ($weighted->weight == 1) {
+ $weighted->remove_from_db();
+ } else {
+ $weighted->set_weight($weighted->weight - 1);
+ $weighted->update();
+ }
+ }
+ trick_taint($tag);
+ $sth_delete->execute($self->id, $tag);
+ $sth_activity->execute(
+ $self->bug_id, $self->id, Bugzilla->user->id, $when, '', $tag);
+ }
+
+ foreach my $tag (@$added_tags) {
+ my $weighted = Bugzilla::Comment::TagWeights->new({ name => $tag });
+ if ($weighted) {
+ $weighted->set_weight($weighted->weight + 1);
+ $weighted->update();
+ } else {
+ Bugzilla::Comment::TagWeights->create({ tag => $tag, weight => 1 });
+ }
+ trick_taint($tag);
+ $sth_insert->execute($self->id, $tag);
+ $sth_activity->execute(
+ $self->bug_id, $self->id, Bugzilla->user->id, $when, $tag, '');
+ }
+ }
+
return $changes;
}
-# Speeds up displays of comment lists by loading all ->author objects
-# at once for a whole list.
+# Speeds up displays of comment lists by loading all author objects and tags at
+# once for a whole list.
sub preload {
my ($class, $comments) = @_;
+ # Author
my %user_ids = map { $_->{who} => 1 } @$comments;
my $users = Bugzilla::User->new_from_list([keys %user_ids]);
my %user_map = map { $_->id => $_ } @$users;
foreach my $comment (@$comments) {
$comment->{author} = $user_map{$comment->{who}};
}
+ # Tags
+ if (Bugzilla->params->{'comment_taggers_group'}) {
+ my $dbh = Bugzilla->dbh;
+ my @comment_ids = map { $_->id } @$comments;
+ my %comment_map = map { $_->id => $_ } @$comments;
+ my $rows = $dbh->selectall_arrayref(
+ "SELECT comment_id, " . $dbh->sql_group_concat('tag', "','") . "
+ FROM longdescs_tags
+ WHERE " . $dbh->sql_in('comment_id', \@comment_ids) . "
+ GROUP BY comment_id");
+ foreach my $row (@$rows) {
+ $comment_map{$row->[0]}->{tags} = [ split(/,/, $row->[1]) ];
+ }
+ }
}
###############################
@@ -126,6 +197,39 @@ sub work_time {
sub type { return $_[0]->{'type'}; }
sub extra_data { return $_[0]->{'extra_data'} }
+sub tags {
+ my ($self) = @_;
+ return [] unless Bugzilla->params->{'comment_taggers_group'};
+ $self->{'tags'} ||= Bugzilla->dbh->selectcol_arrayref(
+ "SELECT tag
+ FROM longdescs_tags
+ WHERE comment_id = ?
+ ORDER BY tag",
+ undef, $self->id);
+ return $self->{'tags'};
+}
+
+sub collapsed {
+ my ($self) = @_;
+ return 0 unless Bugzilla->params->{'comment_taggers_group'};
+ return $self->{collapsed} if exists $self->{collapsed};
+ $self->{collapsed} = 0;
+ Bugzilla->request_cache->{comment_tags_collapsed}
+ ||= [ split(/\s*,\s*/, Bugzilla->params->{'collapsed_comment_tags'}) ];
+ my @collapsed_tags = @{ Bugzilla->request_cache->{comment_tags_collapsed} };
+ foreach my $my_tag (@{ $self->tags }) {
+ $my_tag = lc($my_tag);
+ foreach my $collapsed_tag (@collapsed_tags) {
+ if ($my_tag eq lc($collapsed_tag)) {
+ $self->{collapsed} = 1;
+ last;
+ }
+ }
+ last if $self->{collapsed};
+ }
+ return $self->{collapsed};
+}
+
sub bug {
my $self = shift;
require Bugzilla::Bug;
@@ -182,6 +286,26 @@ sub set_is_private { $_[0]->set('isprivate', $_[1]); }
sub set_type { $_[0]->set('type', $_[1]); }
sub set_extra_data { $_[0]->set('extra_data', $_[1]); }
+sub add_tag {
+ my ($self, $tag) = @_;
+ $tag = $self->_check_tag($tag);
+
+ my $tags = $self->tags;
+ return if grep { lc($tag) eq lc($_) } @$tags;
+ push @$tags, $tag;
+ $self->{'tags'} = [ sort @$tags ];
+}
+
+sub remove_tag {
+ my ($self, $tag) = @_;
+ $tag = $self->_check_tag($tag);
+
+ my $tags = $self->tags;
+ my $index = first { lc($tags->[$_]) eq lc($tag) } 0..scalar(@$tags) - 1;
+ return unless defined $index;
+ splice(@$tags, $index, 1);
+}
+
##############
# Validators #
##############
@@ -324,6 +448,17 @@ sub _check_isprivate {
return $isprivate ? 1 : 0;
}
+sub _check_tag {
+ my ($invocant, $tag) = @_;
+ length($tag) < MIN_COMMENT_TAG_LENGTH
+ and ThrowUserError('comment_tag_too_short', { tag => $tag });
+ length($tag) > MAX_COMMENT_TAG_LENGTH
+ and ThrowUserError('comment_tag_too_long', { tag => $tag });
+ $tag =~ /^[\w\d\._-]+$/
+ or ThrowUserError('comment_tag_invalid', { tag => $tag });
+ return $tag;
+}
+
sub count {
my ($self) = @_;
@@ -338,7 +473,7 @@ sub count {
undef, $self->bug_id, $self->creation_ts);
return --$self->{'count'};
-}
+}
1;
@@ -384,7 +519,7 @@ C<string> Time spent as related to this comment.
=item C<is_private>
-C<boolean> Comment is marked as private
+C<boolean> Comment is marked as private.
=item C<already_wrapped>
@@ -399,6 +534,54 @@ L<Bugzilla::User> who created the comment.
C<int> The position this comment is located in the full list of comments for a bug starting from 0.
+=item C<collapsed>
+
+C<boolean> Comment should be displayed as collapsed by default.
+
+=item C<tags>
+
+C<array of strings> The tags attached to the comment.
+
+=item C<add_tag>
+
+=over
+
+=item B<Description>
+
+Attaches the specified tag to the comment.
+
+=item B<Params>
+
+=over
+
+=item C<tag>
+
+C<string> The tag to attach.
+
+=back
+
+=back
+
+=item C<remove_tag>
+
+=over
+
+=item B<Description>
+
+Detaches the specified tag from the comment.
+
+=item B<Params>
+
+=over
+
+=item C<tag>
+
+C<string> The tag to detach.
+
+=back
+
+=back
+
=item C<body_full>
=over
diff --git a/Bugzilla/Comment/TagWeights.pm b/Bugzilla/Comment/TagWeights.pm
new file mode 100644
index 000000000..5835efbc4
--- /dev/null
+++ b/Bugzilla/Comment/TagWeights.pm
@@ -0,0 +1,74 @@
+# 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.
+
+package Bugzilla::Comment::TagWeights;
+
+use 5.10.1;
+use strict;
+
+use parent qw(Bugzilla::Object);
+
+use Bugzilla::Constants;
+
+# No auditing required
+use constant AUDIT_CREATES => 0;
+use constant AUDIT_UPDATES => 0;
+use constant AUDIT_REMOVES => 0;
+
+use constant DB_COLUMNS => qw(
+ id
+ tag
+ weight
+);
+
+use constant UPDATE_COLUMNS => qw(
+ weight
+);
+
+use constant DB_TABLE => 'longdescs_tags_weights';
+use constant ID_FIELD => 'id';
+use constant NAME_FIELD => 'tag';
+use constant LIST_ORDER => 'weight DESC';
+use constant VALIDATORS => { };
+
+sub tag { return $_[0]->{'tag'} }
+sub weight { return $_[0]->{'weight'} }
+
+sub set_weight { $_[0]->set('weight', $_[1]); }
+
+1;
+
+=head1 NAME
+
+Comment::TagWeights - Bugzilla comment weighting class.
+
+=head1 DESCRIPTION
+
+TagWeights.pm represents a Comment::TagWeight object. It is an implementation
+of L<Bugzilla::Object>, and thus provides all methods that L<Bugzilla::Object>
+provides.
+
+TagWeights is used to quickly find tags and order by their usage count.
+
+=head1 PROPERTIES
+
+=over
+
+=item C<tag>
+
+C<getter string> The tag
+
+=item C<weight>
+
+C<getter int> The tag's weight. The value returned corresponds to the number of
+comments with this tag attached.
+
+=item C<set_weight>
+
+C<setter int> Set the tag's weight.
+
+=back
diff --git a/Bugzilla/Config/Common.pm b/Bugzilla/Config/Common.pm
index efbd1a139..edd5872e1 100644
--- a/Bugzilla/Config/Common.pm
+++ b/Bugzilla/Config/Common.pm
@@ -476,4 +476,9 @@ Checks that the value is a valid number
Checks that the value is a valid regexp
+=item C<check_comment_taggers_group>
+
+Checks that the required modules for comment tagging are installed, and that a
+valid group is provided.
+
=back
diff --git a/Bugzilla/Config/GroupSecurity.pm b/Bugzilla/Config/GroupSecurity.pm
index 90932c736..24eddaf6b 100644
--- a/Bugzilla/Config/GroupSecurity.pm
+++ b/Bugzilla/Config/GroupSecurity.pm
@@ -115,4 +115,5 @@ sub _get_all_group_names {
unshift(@group_names, '');
return \@group_names;
}
+
1;
diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm
index b4262f8ab..b7038505a 100644
--- a/Bugzilla/Constants.pm
+++ b/Bugzilla/Constants.pm
@@ -90,6 +90,9 @@ use Memoize;
COMMENT_COLS
MAX_COMMENT_LENGTH
+ MIN_COMMENT_TAG_LENGTH
+ MAX_COMMENT_TAG_LENGTH
+
CMT_NORMAL
CMT_DUPE_OF
CMT_HAS_DUPE
@@ -311,6 +314,10 @@ use constant COMMENT_COLS => 80;
# Used in _check_comment(). Gives the max length allowed for a comment.
use constant MAX_COMMENT_LENGTH => 65535;
+# The minimum and maximum length of comment tags.
+use constant MIN_COMMENT_TAG_LENGTH => 3;
+use constant MAX_COMMENT_TAG_LENGTH => 24;
+
# The type of bug comments.
use constant CMT_NORMAL => 0;
use constant CMT_DUPE_OF => 1;
diff --git a/Bugzilla/Field.pm b/Bugzilla/Field.pm
index f1118da6a..5cd246b8e 100644
--- a/Bugzilla/Field.pm
+++ b/Bugzilla/Field.pm
@@ -264,6 +264,7 @@ use constant DEFAULT_FIELDS => (
{name => 'see_also', desc => "See Also",
type => FIELD_TYPE_BUG_URLS},
{name => 'tag', desc => 'Tags'},
+ {name => 'comment_tag', desc => 'Comment Tag'},
);
################
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm
index 4e4489935..727bb9f2c 100644
--- a/Bugzilla/User.pm
+++ b/Bugzilla/User.pm
@@ -1876,6 +1876,17 @@ sub is_timetracker {
return $self->{'is_timetracker'};
}
+sub can_tag_comments {
+ my $self = shift;
+
+ if (!defined $self->{'can_tag_comments'}) {
+ my $group = Bugzilla->params->{'comment_taggers_group'};
+ $self->{'can_tag_comments'} =
+ ($group && $self->in_group($group)) ? 1 : 0;
+ }
+ return $self->{'can_tag_comments'};
+}
+
sub get_userlist {
my $self = shift;
@@ -2666,6 +2677,12 @@ i.e. if the 'insidergroup' parameter is set and the user belongs to this group.
Returns true if the user is a global watcher,
i.e. if the 'globalwatchers' parameter contains the user.
+=item C<can_tag_comments>
+
+Returns true if the user can attach tags to comments.
+i.e. if the 'comment_taggers_group' parameter is set and the user belongs to
+this group.
+
=back
=head1 CLASS FUNCTIONS
diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm
index 707e0d42d..3efe6aa1f 100644
--- a/Bugzilla/WebService/Bug.pm
+++ b/Bugzilla/WebService/Bug.pm
@@ -26,6 +26,7 @@ use strict;
use base qw(Bugzilla::WebService);
use Bugzilla::Comment;
+use Bugzilla::Comment::TagWeights;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Field;
@@ -33,7 +34,7 @@ use Bugzilla::WebService::Constants;
use Bugzilla::WebService::Util qw(filter filter_wants validate translate);
use Bugzilla::Bug;
use Bugzilla::BugMail;
-use Bugzilla::Util qw(trick_taint trim);
+use Bugzilla::Util qw(trick_taint trim detaint_natural);
use Bugzilla::Version;
use Bugzilla::Milestone;
use Bugzilla::Status;
@@ -327,7 +328,8 @@ sub _translate_comment {
my ($self, $comment, $filters) = @_;
my $attach_id = $comment->is_about_attachment ? $comment->extra_data
: undef;
- return filter $filters, {
+
+ my $comment_hash = {
id => $self->type('int', $comment->id),
bug_id => $self->type('int', $comment->bug_id),
creator => $self->type('email', $comment->author->login),
@@ -338,6 +340,16 @@ sub _translate_comment {
text => $self->type('string', $comment->body_full),
attachment_id => $self->type('int', $attach_id),
};
+
+ # Don't load comment tags unless enabled
+ if (Bugzilla->params->{'comment_taggers_group'}) {
+ $comment_hash->{tags} = [
+ map { $self->type('string', $_) }
+ @{ $comment->tags }
+ ];
+ }
+
+ return filter $filters, $comment_hash;
}
sub get {
@@ -1038,6 +1050,70 @@ sub attachments {
return { bugs => \%bugs, attachments => \%attachments };
}
+sub update_comment_tags {
+ my ($self, $params) = @_;
+
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+ Bugzilla->params->{'comment_taggers_group'}
+ || ThrowUserError("comment_tag_disabled");
+ $user->can_tag_comments
+ || ThrowUserError("auth_failure",
+ { group => Bugzilla->params->{'comment_taggers_group'},
+ action => "update",
+ object => "comment_tags" });
+
+ my $comment_id = $params->{comment_id}
+ // ThrowCodeError('param_required',
+ { function => 'Bug.update_comment_tags',
+ param => 'comment_id' });
+
+ my $comment = Bugzilla::Comment->new($comment_id)
+ || return [];
+ $comment->bug->check_is_visible();
+ if ($comment->is_private && !$user->is_insider) {
+ ThrowUserError('comment_is_private', { id => $comment_id });
+ }
+
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_start_transaction();
+ foreach my $tag (@{ $params->{add} || [] }) {
+ $comment->add_tag($tag) if defined $tag;
+ }
+ foreach my $tag (@{ $params->{remove} || [] }) {
+ $comment->remove_tag($tag) if defined $tag;
+ }
+ $comment->update();
+ $dbh->bz_commit_transaction();
+
+ return $comment->tags;
+}
+
+sub search_comment_tags {
+ my ($self, $params) = @_;
+
+ Bugzilla->login(LOGIN_REQUIRED);
+ Bugzilla->params->{'comment_taggers_group'}
+ || ThrowUserError("comment_tag_disabled");
+ Bugzilla->user->can_tag_comments
+ || ThrowUserError("auth_failure", { group => Bugzilla->params->{'comment_taggers_group'},
+ action => "search",
+ object => "comment_tags"});
+
+ my $query = $params->{query};
+ $query
+ // ThrowCodeError('param_required', { param => 'query' });
+ my $limit = detaint_natural($params->{limit}) || 7;
+
+ my $tags = Bugzilla::Comment::TagWeights->match({
+ WHERE => {
+ 'tag LIKE ?' => "\%$query\%",
+ },
+ LIMIT => $limit,
+ });
+ return [ map { $_->tag } @$tags ];
+}
+
+
##############################
# Private Helper Subroutines #
##############################
diff --git a/Bugzilla/WebService/Constants.pm b/Bugzilla/WebService/Constants.pm
index 0aa6c1bbd..87d890176 100644
--- a/Bugzilla/WebService/Constants.pm
+++ b/Bugzilla/WebService/Constants.pm
@@ -106,7 +106,12 @@ use constant WS_ERROR_CODE => {
comment_is_private => 110,
comment_id_invalid => 111,
comment_too_long => 114,
- comment_invalid_isprivate => 117,
+ comment_invalid_isprivate => 117,
+ # Comment tagging
+ comment_tag_disabled => 125,
+ comment_tag_invalid => 126,
+ comment_tag_too_long => 127,
+ comment_tag_too_short => 128,
# See Also errors
bug_url_invalid => 112,
bug_url_too_long => 112,
diff --git a/Bugzilla/WebService/Server/REST/Resources/Bug.pm b/Bugzilla/WebService/Server/REST/Resources/Bug.pm
index 98ae6049c..ea420b4ed 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Bug.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Bug.pm
@@ -65,6 +65,22 @@ sub _rest_resources {
}
}
},
+ qr{^/bug/comment/tags/([^/]+)$}, {
+ GET => {
+ method => 'search_comment_tags',
+ params => sub {
+ return { query => $_[0] };
+ },
+ },
+ },
+ qr{^/bug/comment/([^/]+)/tags$}, {
+ PUT => {
+ method => 'update_comment_tags',
+ params => sub {
+ return { comment_id => $_[0] };
+ },
+ },
+ },
qr{^/bug/([^/]+)/history$}, {
GET => {
method => 'history',
diff --git a/js/comment-tagging.js b/js/comment-tagging.js
new file mode 100644
index 000000000..94afbb61a
--- /dev/null
+++ b/js/comment-tagging.js
@@ -0,0 +1,385 @@
+/* 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. */
+
+var Dom = YAHOO.util.Dom;
+
+YAHOO.bugzilla.commentTagging = {
+ ctag_div : false,
+ ctag_add : false,
+ counter : 0,
+ min_len : 3,
+ max_len : 24,
+ tags_by_no: {},
+ nos_by_tag: {},
+ current_id: 0,
+ current_no: -1,
+ can_edit : false,
+ pending : {},
+
+ init : function(can_edit) {
+ this.can_edit = can_edit;
+ this.ctag_div = Dom.get('bz_ctag_div');
+ this.ctag_add = Dom.get('bz_ctag_add');
+ YAHOO.util.Event.on(this.ctag_add, 'keypress', this.onKeyPress);
+ YAHOO.util.Event.onDOMReady(function() {
+ YAHOO.bugzilla.commentTagging.updateCollapseControls();
+ });
+ if (!can_edit) return;
+
+ var ds = new YAHOO.util.XHRDataSource("jsonrpc.cgi");
+ ds.connTimeout = 30000;
+ ds.connMethodPost = true;
+ ds.connXhrMode = "cancelStaleRequests";
+ ds.maxCacheEntries = 5;
+ ds.responseSchema = {
+ metaFields : { error: "error", jsonRpcId: "id"},
+ resultsList : "result"
+ };
+
+ var ac = new YAHOO.widget.AutoComplete('bz_ctag_add', 'bz_ctag_autocomp', ds);
+ ac.maxResultsDisplayed = 7;
+ ac.generateRequest = function(query) {
+ query = YAHOO.lang.trim(query);
+ YAHOO.bugzilla.commentTagging.last_query = query;
+ YAHOO.bugzilla.commentTagging.counter = YAHOO.bugzilla.commentTagging.counter + 1;
+ YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
+ return YAHOO.lang.JSON.stringify({
+ method : "Bug.search_comment_tags",
+ id : YAHOO.bugzilla.commentTagging.counter,
+ params : [ { query : query, limit : 10 } ]
+ });
+ };
+ ac.minQueryLength = this.min_len;
+ ac.autoHighlight = false;
+ ac.typeAhead = true;
+ ac.queryDelay = 0.5;
+ ac.dataReturnEvent.subscribe(function(type, args) {
+ args[0].autoHighlight = args[2].length == 1;
+ });
+ },
+
+ toggle : function(comment_id, comment_no) {
+ if (!this.ctag_div) return;
+ var tags_container = Dom.get('ct_' + comment_no);
+
+ if (this.current_id == comment_id) {
+ // hide
+ this.current_id = 0;
+ this.current_no = -1;
+ Dom.addClass(this.ctag_div, 'bz_default_hidden');
+ this.hideError();
+ window.focus();
+
+ } else {
+ // show or move
+ this.rpcRefresh(comment_id, comment_no);
+ this.current_id = comment_id;
+ this.current_no = comment_no;
+ this.ctag_add.value = '';
+ tags_container.parentNode.insertBefore(this.ctag_div, tags_container);
+ Dom.removeClass(this.ctag_div, 'bz_default_hidden');
+ Dom.removeClass(tags_container.parentNode, 'bz_default_hidden');
+ var comment = Dom.get('comment_text_' + comment_no);
+ if (Dom.hasClass(comment, 'collapsed')) {
+ var link = Dom.get('comment_link_' + comment_no);
+ expand_comment(link, comment, comment_no);
+ }
+ window.setTimeout(function() {
+ YAHOO.bugzilla.commentTagging.ctag_add.focus();
+ }, 50);
+ }
+ },
+
+ hideInput : function() {
+ if (this.current_id != 0) {
+ var comment_no = this.current_no;
+ this.toggle(this.current_id, this.current_no);
+ this.hideEmpty(comment_no);
+ }
+ this.hideError();
+ },
+
+ hideEmpty : function(comment_no) {
+ if (Dom.get('ct_' + comment_no).children.length == 0) {
+ Dom.addClass('comment_tag_' + comment_no, 'bz_default_hidden');
+ }
+ },
+
+ showError : function(comment_id, comment_no, error) {
+ var bz_ctag_error = Dom.get('bz_ctag_error');
+ var tags_container = Dom.get('ct_' + comment_no);
+ tags_container.parentNode.appendChild(bz_ctag_error, tags_container);
+ Dom.get('bz_ctag_error_msg').innerHTML = YAHOO.lang.escapeHTML(error);
+ Dom.removeClass(bz_ctag_error, 'bz_default_hidden');
+ },
+
+ hideError : function() {
+ Dom.addClass('bz_ctag_error', 'bz_default_hidden');
+ },
+
+ onKeyPress : function(evt) {
+ evt = evt || window.event;
+ var charCode = evt.charCode || evt.keyCode;
+ if (evt.keyCode == 27) {
+ // escape
+ YAHOO.bugzilla.commentTagging.hideInput();
+ YAHOO.util.Event.stopEvent(evt);
+
+ } else if (evt.keyCode == 13) {
+ // return
+ YAHOO.util.Event.stopEvent(evt);
+ var tags = YAHOO.bugzilla.commentTagging.ctag_add.value.split(/[ ,]/);
+ var comment_id = YAHOO.bugzilla.commentTagging.current_id;
+ var comment_no = YAHOO.bugzilla.commentTagging.current_no;
+ try {
+ YAHOO.bugzilla.commentTagging.add(comment_id, comment_no, tags);
+ YAHOO.bugzilla.commentTagging.hideInput();
+ } catch(e) {
+ YAHOO.bugzilla.commentTagging.showError(comment_id, comment_no, e.message);
+ }
+ }
+ },
+
+ showTags : function(comment_id, comment_no, tags) {
+ // remove existing tags
+ var tags_container = Dom.get('ct_' + comment_no);
+ while (tags_container.hasChildNodes()) {
+ tags_container.removeChild(tags_container.lastChild);
+ }
+ // add tags
+ if (tags != '') {
+ if (typeof(tags) == 'string') {
+ tags = tags.split(',');
+ }
+ for (var i = 0, l = tags.length; i < l; i++) {
+ tags_container.appendChild(this.buildTagHtml(comment_id, comment_no, tags[i]));
+ }
+ }
+ // update tracking array
+ this.tags_by_no['c' + comment_no] = tags;
+ this.updateCollapseControls();
+ },
+
+ updateCollapseControls : function() {
+ var container = Dom.get('comment_tags_collapse_expand_container');
+ if (!container) return;
+ // build list of tags
+ this.nos_by_tag = {};
+ for (var id in this.tags_by_no) {
+ if (this.tags_by_no.hasOwnProperty(id)) {
+ for (var i = 0, l = this.tags_by_no[id].length; i < l; i++) {
+ var tag = this.tags_by_no[id][i].toLowerCase();
+ if (!this.nos_by_tag.hasOwnProperty(tag)) {
+ this.nos_by_tag[tag] = [];
+ }
+ this.nos_by_tag[tag].push(id);
+ }
+ }
+ }
+ var tags = [];
+ for (var tag in this.nos_by_tag) {
+ if (this.nos_by_tag.hasOwnProperty(tag)) {
+ tags.push(tag);
+ }
+ }
+ tags.sort();
+ if (tags.length) {
+ var div = document.createElement('div');
+ div.appendChild(document.createTextNode('Comment Tags:'));
+ var ul = document.createElement('ul');
+ ul.id = 'comment_tags_collapse_expand';
+ div.appendChild(ul);
+ for (var i = 0, l = tags.length; i < l; i++) {
+ var tag = tags[i];
+ var li = document.createElement('li');
+ ul.appendChild(li);
+ var a = document.createElement('a');
+ li.appendChild(a);
+ Dom.setAttribute(a, 'href', '#');
+ YAHOO.util.Event.addListener(a, 'click', function(evt, tag) {
+ YAHOO.bugzilla.commentTagging.toggleCollapse(tag);
+ YAHOO.util.Event.stopEvent(evt);
+ }, tag);
+ li.appendChild(document.createTextNode(' (' + this.nos_by_tag[tag].length + ')'));
+ a.innerHTML = tag;
+ }
+ while (container.hasChildNodes()) {
+ container.removeChild(container.lastChild);
+ }
+ container.appendChild(div);
+ } else {
+ while (container.hasChildNodes()) {
+ container.removeChild(container.lastChild);
+ }
+ }
+ },
+
+ toggleCollapse : function(tag) {
+ var nos = this.nos_by_tag[tag];
+ if (!nos) return;
+ toggle_all_comments('collapse');
+ for (var i = 0, l = nos.length; i < l; i++) {
+ var comment_no = nos[i].match(/\d+$/)[0];
+ var comment = Dom.get('comment_text_' + comment_no);
+ var link = Dom.get('comment_link_' + comment_no);
+ expand_comment(link, comment, comment_no);
+ }
+ },
+
+ buildTagHtml : function(comment_id, comment_no, tag) {
+ var el = document.createElement('span');
+ Dom.setAttribute(el, 'id', 'ct_' + comment_no + '_' + tag);
+ Dom.addClass(el, 'bz_comment_tag');
+ if (this.can_edit) {
+ var a = document.createElement('a');
+ Dom.setAttribute(a, 'href', '#');
+ YAHOO.util.Event.addListener(a, 'click', function(evt, args) {
+ YAHOO.bugzilla.commentTagging.remove(args[0], args[1], args[2])
+ YAHOO.util.Event.stopEvent(evt);
+ }, [comment_id, comment_no, tag]);
+ a.appendChild(document.createTextNode('x'));
+ el.appendChild(a);
+ el.appendChild(document.createTextNode("\u00a0"));
+ }
+ el.appendChild(document.createTextNode(tag));
+ return el;
+ },
+
+ add : function(comment_id, comment_no, add_tags) {
+ // build list of current tags from html
+ var tags = new Array();
+ var spans = Dom.getElementsByClassName('bz_comment_tag', undefined, 'ct_' + comment_no);
+ for (var i = 0, l = spans.length; i < l; i++) {
+ tags.push(spans[i].textContent.substr(2));
+ }
+ // add new tags
+ var new_tags = new Array();
+ for (var i = 0, l = add_tags.length; i < l; i++) {
+ var tag = YAHOO.lang.trim(add_tags[i]);
+ // validation
+ if (tag == '')
+ continue;
+ if (tag.length < YAHOO.bugzilla.commentTagging.min_len)
+ throw new Error("Comment tags must be at least " + this.min_len + " characters.");
+ if (tag.length > YAHOO.bugzilla.commentTagging.max_len)
+ throw new Error("Comment tags cannot be longer than " + this.min_len + " characters.");
+ // append new tag
+ if (bz_isValueInArrayIgnoreCase(tags, tag))
+ continue;
+ new_tags.push(tag);
+ tags.push(tag);
+ }
+ tags.sort();
+ // update
+ this.showTags(comment_id, comment_no, tags);
+ this.rpcUpdate(comment_id, comment_no, new_tags, undefined);
+ },
+
+ remove : function(comment_id, comment_no, tag) {
+ var el = Dom.get('ct_' + comment_no + '_' + tag);
+ if (el) {
+ el.parentNode.removeChild(el);
+ this.rpcUpdate(comment_id, comment_no, undefined, [ tag ]);
+ this.hideEmpty(comment_no);
+ }
+ },
+
+ // If multiple updates are triggered quickly, overlapping refresh events
+ // are generated. We ignore all events except the last one.
+ incPending : function(comment_id) {
+ if (this.pending['c' + comment_id] == undefined) {
+ this.pending['c' + comment_id] = 1;
+ } else {
+ this.pending['c' + comment_id]++;
+ }
+ },
+
+ decPending : function(comment_id) {
+ if (this.pending['c' + comment_id] != undefined)
+ this.pending['c' + comment_id]--;
+ },
+
+ hasPending : function(comment_id) {
+ return this.pending['c' + comment_id] != undefined
+ && this.pending['c' + comment_id] > 0;
+ },
+
+ rpcRefresh : function(comment_id, comment_no, noRefreshOnError) {
+ this.incPending(comment_id);
+ YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
+ YAHOO.util.Connect.asyncRequest('POST', 'jsonrpc.cgi',
+ {
+ success: function(res) {
+ YAHOO.bugzilla.commentTagging.decPending(comment_id);
+ data = YAHOO.lang.JSON.parse(res.responseText);
+ if (data.error) {
+ YAHOO.bugzilla.commentTagging.handleRpcError(
+ comment_id, comment_no, data.error.message, noRefreshOnError);
+ return;
+ }
+
+ if (!YAHOO.bugzilla.commentTagging.hasPending(comment_id))
+ YAHOO.bugzilla.commentTagging.showTags(
+ comment_id, comment_no, data.result.comments[comment_id].tags);
+ },
+ failure: function(res) {
+ YAHOO.bugzilla.commentTagging.decPending(comment_id);
+ YAHOO.bugzilla.commentTagging.handleRpcError(
+ comment_id, comment_no, res.responseText, noRefreshOnError);
+ }
+ },
+ YAHOO.lang.JSON.stringify({
+ version: "1.1",
+ method: 'Bug.comments',
+ params: {
+ comment_ids: [ comment_id ],
+ include_fields: [ 'tags' ]
+ }
+ })
+ );
+ },
+
+ rpcUpdate : function(comment_id, comment_no, add, remove) {
+ this.incPending(comment_id);
+ YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
+ YAHOO.util.Connect.asyncRequest('POST', 'jsonrpc.cgi',
+ {
+ success: function(res) {
+ YAHOO.bugzilla.commentTagging.decPending(comment_id);
+ data = YAHOO.lang.JSON.parse(res.responseText);
+ if (data.error) {
+ YAHOO.bugzilla.commentTagging.handleRpcError(comment_id, comment_no, data.error.message);
+ return;
+ }
+
+ if (!YAHOO.bugzilla.commentTagging.hasPending(comment_id))
+ YAHOO.bugzilla.commentTagging.showTags(comment_id, comment_no, data.result);
+ },
+ failure: function(res) {
+ YAHOO.bugzilla.commentTagging.decPending(comment_id);
+ YAHOO.bugzilla.commentTagging.handleRpcError(comment_id, comment_no, res.responseText);
+ }
+ },
+ YAHOO.lang.JSON.stringify({
+ version: "1.1",
+ method: 'Bug.update_comment_tags',
+ params: {
+ comment_id: comment_id,
+ add: add,
+ remove: remove
+ }
+ })
+ );
+ },
+
+ handleRpcError : function(comment_id, comment_no, message, noRefreshOnError) {
+ YAHOO.bugzilla.commentTagging.showError(comment_id, comment_no, message);
+ if (!noRefreshOnError) {
+ YAHOO.bugzilla.commentTagging.rpcRefresh(comment_id, comment_no, true);
+ }
+ }
+}
diff --git a/js/comments.js b/js/comments.js
index e9a3e209f..2264375eb 100644
--- a/js/comments.js
+++ b/js/comments.js
@@ -40,9 +40,9 @@ function toggle_comment_display(link, comment_id) {
var comment = document.getElementById('comment_text_' + comment_id);
var re = new RegExp(/\bcollapsed\b/);
if (comment.className.match(re))
- expand_comment(link, comment);
+ expand_comment(link, comment, comment_id);
else
- collapse_comment(link, comment);
+ collapse_comment(link, comment, comment_id);
}
function toggle_all_comments(action) {
@@ -59,20 +59,22 @@ function toggle_all_comments(action) {
var id = comments[i].id.match(/\d*$/);
var link = document.getElementById('comment_link_' + id);
if (action == 'collapse')
- collapse_comment(link, comment);
+ collapse_comment(link, comment, id);
else
- expand_comment(link, comment);
+ expand_comment(link, comment, id);
}
}
-function collapse_comment(link, comment) {
+function collapse_comment(link, comment, comment_id) {
link.innerHTML = "[+]";
YAHOO.util.Dom.addClass(comment, 'collapsed');
+ YAHOO.util.Dom.addClass('comment_tag_' + comment_id, 'collapsed');
}
-function expand_comment(link, comment) {
+function expand_comment(link, comment, comment_id) {
link.innerHTML = "[-]";
YAHOO.util.Dom.removeClass(comment, 'collapsed');
+ YAHOO.util.Dom.removeClass('comment_tag_' + comment_id, 'collapsed');
}
function wrapReplyText(text) {
@@ -125,11 +127,12 @@ function wrapReplyText(text) {
/* This way, we are sure that browsers which do not support JS
* won't display this link */
-function addCollapseLink(count, title) {
+function addCollapseLink(count, collapsed, title) {
document.write(' <a href="#" class="bz_collapse_comment"' +
' id="comment_link_' + count +
'" onclick="toggle_comment_display(this, ' + count +
- '); return false;" title="' + title + '">[-]<\/a> ');
+ '); return false;" title="' + title + '">[' +
+ (collapsed ? '+' : '&minus;') + ']<\/a> ');
}
function goto_add_comments( anchor ){
diff --git a/js/util.js b/js/util.js
index e0e87259f..cc4a87081 100644
--- a/js/util.js
+++ b/js/util.js
@@ -143,10 +143,7 @@ function bz_overlayBelow(item, parent) {
*/
function bz_isValueInArray(aArray, aValue)
{
- var run = 0;
- var len = aArray.length;
-
- for ( ; run < len; run++) {
+ for (var run = 0, len = aArray.length ; run < len; run++) {
if (aArray[run] == aValue) {
return true;
}
@@ -156,6 +153,26 @@ function bz_isValueInArray(aArray, aValue)
}
/**
+ * Checks if a specified value is in the specified array by performing a
+ * case-insensitive comparison.
+ *
+ * @param aArray Array to search for the value.
+ * @param aValue Value to search from the array.
+ * @return Boolean; true if value is found in the array and false if not.
+ */
+function bz_isValueInArrayIgnoreCase(aArray, aValue)
+{
+ var re = new RegExp(aValue.replace(/([^A-Za-z0-9])/g, "\\$1"), 'i');
+ for (var run = 0, len = aArray.length ; run < len; run++) {
+ if (aArray[run].match(re)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
* Create wanted options in a select form control.
*
* @param aSelect Select form control to manipulate.
diff --git a/show_activity.cgi b/show_activity.cgi
index 27096018f..b05c1c95a 100755
--- a/show_activity.cgi
+++ b/show_activity.cgi
@@ -54,8 +54,8 @@ my $bug = Bugzilla::Bug->check($id);
# visible immediately due to replication lag.
Bugzilla->switch_to_shadow_db;
-($vars->{'operations'}, $vars->{'incomplete_data'}) =
- Bugzilla::Bug::GetBugActivity($bug->id);
+($vars->{'operations'}, $vars->{'incomplete_data'}) =
+ Bugzilla::Bug::GetBugActivity($bug->id, undef, undef, 1);
$vars->{'bug'} = $bug;
diff --git a/skins/contrib/Mozilla/global.css b/skins/contrib/Mozilla/global.css
index b0ce99cf8..4d6fd7113 100644
--- a/skins/contrib/Mozilla/global.css
+++ b/skins/contrib/Mozilla/global.css
@@ -559,6 +559,35 @@ table.edit_form hr {
padding: 5px !important;
}
+.bz_comment_tags {
+ background: #eee;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
+ padding: 5px;
+}
+
+.bz_comment_tag {
+ background: #fff;
+ color: #444;
+ border: none;
+ padding: 2px 6px;
+}
+
+.bz_comment_tag a {
+ color: #0095DD;
+}
+
+#bz_ctag_error {
+ border: none;
+ background-color: #faa;
+ color: #444;
+ padding: 2px 6px;
+}
+
+#bz_ctag_error a {
+ color: #0095DD;
+}
+
.ih_inlinehistory {
background: #eee;
border-top: 1px solid rgba(0, 0, 0, 0.1);
diff --git a/skins/standard/show_bug.css b/skins/standard/show_bug.css
index 8214ce5f4..656e130a0 100644
--- a/skins/standard/show_bug.css
+++ b/skins/standard/show_bug.css
@@ -116,3 +116,42 @@ table#flags {
.bz_bug .bz_alias_short_desc_container {
width: inherit;
}
+
+.bz_comment_tags {
+ margin-top: 3px;
+}
+
+.bz_comment_tag {
+ border: 1px solid #c8c8ba;
+ padding: 1px 3px;
+ margin-right: 2px;
+ border-radius: 0.5em;
+ background-color: #eee;
+ color: #000;
+}
+
+#bz_ctag_div {
+ display: inline-block;
+}
+
+#bz_ctag_error {
+ border: 1px solid #ff6666;
+ padding: 0px 2px;
+ border-radius: 0.5em;
+ margin: 2px;
+ display: inline-block;
+}
+
+#comment_tags_collapse_expand_container {
+ padding-top: 1em;
+}
+
+#comment_tags_collapse_expand {
+ list-style-type: none;
+ padding-left: 1em;
+}
+
+#comment_tags_collapse_expand li {
+ margin-bottom: 0px;
+}
+
diff --git a/template/en/default/admin/params/bugfields.html.tmpl b/template/en/default/admin/params/bugfields.html.tmpl
index 58b08f615..a0d9664ad 100644
--- a/template/en/default/admin/params/bugfields.html.tmpl
+++ b/template/en/default/admin/params/bugfields.html.tmpl
@@ -57,5 +57,9 @@
"entry form.<br> " _
"You can leave this empty: " _
"$terms.Bugzilla will then use the operating system that the browser " _
- "reports to be running on as the default." }
+ "reports to be running on as the default.",
+
+ collapsed_comment_tags => "A comma separated list of tags which, when applied " _
+ "to comments, will cause them to be collapsed by default",
+ }
%]
diff --git a/template/en/default/admin/params/groupsecurity.html.tmpl b/template/en/default/admin/params/groupsecurity.html.tmpl
index 783099a11..041af6833 100644
--- a/template/en/default/admin/params/groupsecurity.html.tmpl
+++ b/template/en/default/admin/params/groupsecurity.html.tmpl
@@ -42,6 +42,9 @@
querysharegroup => "The name of the group of users who can share their " _
"saved searches with others.",
+ comment_taggers_group => "The name of the group of users who can tag comment." _
+ " Setting this to empty disables comment tagging.",
+
debug_group => "The name of the group of users who can view the actual " _
"SQL query generated when viewing $terms.bug lists and reports.",
diff --git a/template/en/default/bug/comments.html.tmpl b/template/en/default/bug/comments.html.tmpl
index 773e6a485..e0226257e 100644
--- a/template/en/default/bug/comments.html.tmpl
+++ b/template/en/default/bug/comments.html.tmpl
@@ -133,10 +133,13 @@
<td>
[% IF mode == "edit" %]
<ul class="bz_collapse_expand_comments">
- <li><a href="#" onclick="toggle_all_comments('collapse');
+ <li><a href="#" onclick="toggle_all_comments('collapse');
return false;">Collapse All Comments</a></li>
<li><a href="#" onclick="toggle_all_comments('expand');
return false;">Expand All Comments</a></li>
+ [% IF Param('comment_taggers_group') %]
+ <li><div id="comment_tags_collapse_expand_container"></div></li>
+ [% END %]
</ul>
[% END %]
</td>
@@ -151,15 +154,15 @@
[% comment_text = comment.body_full %]
[% RETURN IF comment_text == '' AND (comment.work_time - 0) != 0 AND !user.is_timetracker %]
- <div id="c[% count %]" class="bz_comment[% " bz_private" IF comment.is_private %]
- [% " bz_comment_hilite" IF marks.$count %]
- [% " bz_first_comment" IF count == description %]">
- [% IF count == description %]
+ <div id="c[% comment.count %]" class="bz_comment[% " bz_private" IF comment.is_private %]
+ [% " bz_comment_hilite" IF marks.${comment.count} %]
+ [% " bz_first_comment" IF comment.count == description %]">
+ [% IF comment.count == 0 %]
[% class_name = "bz_first_comment_head" %]
[% comment_label = "Description" %]
[% ELSE %]
[% class_name = "bz_comment_head" %]
- [% comment_label = "Comment " _ count %]
+ [% comment_label = "Comment " _ comment.count %]
[% END %]
<div class="[% class_name FILTER html %]">
@@ -168,17 +171,21 @@
<span class="bz_comment_actions">
[% IF comment_text.search("(?:^>|\n>)") %]
[<a class="bz_wrap_link" href="#"
- onclick="return toggleCommentWrap(this, [% count %])">wrap</a>]
+ onclick="return toggleCommentWrap(this, [% comment.count %])">wrap</a>]
[% END %]
[% IF bug.check_can_change_field('longdesc', 1, 0) %]
+ [% IF user.can_tag_comments %]
+ [<a href="#"
+ onclick="YAHOO.bugzilla.commentTagging.toggle([% comment.id %], [% comment.count %]);return false">tag</a>]
+ [% END %]
[<a class="bz_reply_link" href="#add_comment"
[% IF user.settings.quote_replies.value != 'off' %]
- onclick="replyToComment('[% count %]', '[% comment.id %]', '[% comment.author.name || comment.author.nick FILTER html FILTER js %]'); return false;"
+ onclick="replyToComment('[% comment.count %]', '[% comment.id %]', '[% comment.author.name || comment.author.nick FILTER html FILTER js %]'); return false;"
[% END %]
>reply</a>]
[% END %]
- <script type="text/javascript"><!--
- addCollapseLink([% count %], 'Toggle comment display'); // -->
+ <script type="text/javascript">
+ addCollapseLink([% comment.count %], [% comment.collapsed FILTER js %], 'Toggle comment display');
</script>
</span>
[% END %]
@@ -190,7 +197,7 @@
<input type="checkbox"
name="isprivate_[% comment.id %]" value="1"
id="isprivate_[% comment.id %]"
- onClick="updateCommentPrivacy(this, [% count %])"
+ onClick="updateCommentPrivacy(this, [% comment.count %])"
[% " checked=\"checked\"" IF comment.is_private %]>
<label for="isprivate_[% comment.id %]">Private</label>
</div>
@@ -198,7 +205,7 @@
<span class="bz_comment_number">
<a
- href="show_bug.cgi?id=[% bug.bug_id %]#c[% count %]">
+ href="show_bug.cgi?id=[% bug.bug_id %]#c[% comment.count %]">
[%- comment_label FILTER html %]</a>
</span>
@@ -236,11 +243,30 @@
[% PROCESS formattimeunit time_unit=comment.work_time %]
[% END %]
+ [% IF user.id && Param('comment_taggers_group') %]
+ <div id="comment_tag_[% comment.count FILTER html %]"
+ class="bz_comment_tags[% " collapsed" IF comment.collapsed %]
+ [% " bz_default_hidden" UNLESS comment.tags.size %]">
+ <span id="ct_[% comment.count %]">
+ [% IF comment.tags.size %]
+ <script>
+ YAHOO.bugzilla.commentTagging.showTags([% comment.id FILTER none %],
+ [% comment.count FILTER none %], [
+ [% FOREACH tag = comment.tags %]
+ [%~%]'[% tag FILTER js %]'[% "," UNLESS loop.last %]
+ [% END %]
+ [%~%]]);
+ </script>
+ [% END %]
+ </span>
+ </div>
+ [% END %]
+
[%# Don't indent the <pre> block, since then the spaces are displayed in the
# generated HTML
#%]
-<pre class="bz_comment_text"
- [% ' id="comment_text_' _ count _ '"' IF mode == "edit" %]>
+<pre class="bz_comment_text[% " collapsed" IF comment.collapsed %]"
+ [% ' id="comment_text_' _ comment.count _ '"' IF mode == "edit" %]>
[%- comment_text FILTER quoteUrls(bug, comment) -%]
</pre>
</div>
diff --git a/template/en/default/bug/edit.html.tmpl b/template/en/default/bug/edit.html.tmpl
index 696d0739f..fb596f7a7 100644
--- a/template/en/default/bug/edit.html.tmpl
+++ b/template/en/default/bug/edit.html.tmpl
@@ -30,6 +30,31 @@
[% PROCESS bug/time.html.tmpl %]
+[% IF Param('comment_taggers_group') %]
+ [% IF user.can_tag_comments %]
+ <div id="bz_ctag_div" class="bz_default_hidden">
+ <a href="javascript:void(0)" onclick="YAHOO.bugzilla.commentTagging.hideInput()">x</a>
+ <div>
+ <input id="bz_ctag_add" size="10" placeholder="add tag"
+ maxlength="[% constants.MAX_COMMENT_TAG_LENGTH FILTER html %]">
+ <span id="bz_ctag_autocomp"></span>
+ </div>
+ &nbsp;
+ </div>
+ <div id="bz_ctag_error" class="bz_default_hidden">
+ <a href="javascript:void(0)" onclick="YAHOO.bugzilla.commentTagging.hideError()">x</a>
+ <span id="bz_ctag_error_msg"></span>
+ </div>
+ [% END %]
+ [% IF user.id %]
+ <script type="text/javascript">
+ YAHOO.bugzilla.commentTagging.init([% user.can_tag_comments ? 'true' : 'false' %]);
+ YAHOO.bugzilla.commentTagging.min_len = [% constants.MIN_COMMENT_TAG_LENGTH FILTER js %];
+ YAHOO.bugzilla.commentTagging.max_len = [% constants.MAX_COMMENT_TAG_LENGTH FILTER js %];
+ </script>
+ [% END %]
+[% END %]
+
<script type="text/javascript">
<!--
[% IF user.is_timetracker %]
diff --git a/template/en/default/bug/show-header.html.tmpl b/template/en/default/bug/show-header.html.tmpl
index ee1ecf6d2..9f2127d23 100644
--- a/template/en/default/bug/show-header.html.tmpl
+++ b/template/en/default/bug/show-header.html.tmpl
@@ -39,7 +39,10 @@
[% header = "$terms.Bug&nbsp;$bug.bug_id" %]
[% header_addl_info = "Last modified: $filtered_timestamp" %]
[% yui = ['autocomplete', 'calendar'] %]
+[% yui.push('container') IF user.can_tag_comments %]
[% javascript_urls = [ "js/util.js", "js/field.js" ] %]
+[% javascript_urls.push('js/comment-tagging.js')
+ IF user.id && Param('comment_taggers_group') %]
[% IF bug.defined %]
[% unfiltered_title = "$terms.Bug $bug.bug_id – " %]
[% IF bug.alias != '' %]
diff --git a/template/en/default/filterexceptions.pl b/template/en/default/filterexceptions.pl
index 16a3220e2..84ff45db7 100644
--- a/template/en/default/filterexceptions.pl
+++ b/template/en/default/filterexceptions.pl
@@ -218,6 +218,7 @@
'bug/comments.html.tmpl' => [
'comment.id',
+ 'comment.count',
'bug.bug_id',
],
diff --git a/template/en/default/global/user-error.html.tmpl b/template/en/default/global/user-error.html.tmpl
index 0bd3dd15e..5851d439f 100644
--- a/template/en/default/global/user-error.html.tmpl
+++ b/template/en/default/global/user-error.html.tmpl
@@ -184,6 +184,8 @@
classifications
[% ELSIF object == "components" %]
components
+ [% ELSIF object == "comment_tags" %]
+ comment tags
[% ELSIF object == "custom_fields" %]
custom fields
[% ELSIF object == "field_values" %]
@@ -325,6 +327,25 @@
Comments cannot be longer than
[%+ constants.MAX_COMMENT_LENGTH FILTER html %] characters.
+ [% ELSIF error == "comment_tag_disabled" %]
+ [% title = "Comment Tagging Disabled" %]
+ The comment tagging is not enabled.
+
+ [% ELSIF error == "comment_tag_invalid" %]
+ [% title = "Invalid Comment Tag" %]
+ The comment tag "[% tag FILTER html %]" contains invalid characters or
+ words.
+
+ [% ELSIF error == "comment_tag_too_long" %]
+ [% title = "Comment Tag Too Long" %]
+ Comment tags cannot be longer than
+ [%+ constants.MAX_COMMENT_TAG_LENGTH FILTER html %] characters.
+
+ [% ELSIF error == "comment_tag_too_short" %]
+ [% title = "Comment Tag Too Short" %]
+ Comment tags must be at least
+ [%+ constants.MIN_COMMENT_TAG_LENGTH FILTER html %] characters.
+
[% ELSIF error == "auth_classification_not_enabled" %]
[% title = "Classification Not Enabled" %]
Sorry, classification is not enabled.
@@ -1684,7 +1705,7 @@
[% ELSIF error == "tag_name_too_long" %]
[% title = "Tag Name Too Long" %]
- The tag name must be less than [% constants.MAX_LEN_QUERY_NAME FILTER html %]
+ The tag must be less than [% constants.MAX_LEN_QUERY_NAME FILTER html %]
characters long.
[% ELSIF error == "token_does_not_exist" %]