diff options
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 ? '+' : '−') + ']<\/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> + + </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 $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" %] |