]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 1484892 - Modify EditComments extension to let anyone use it conditionally and...
authorDylan William Hardison <dylan@hardison.net>
Tue, 20 Nov 2018 21:30:56 +0000 (16:30 -0500)
committerGitHub <noreply@github.com>
Tue, 20 Nov 2018 21:30:56 +0000 (16:30 -0500)
23 files changed:
Bugzilla/Template.pm
extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl
extensions/BugModal/web/bug_modal.css
extensions/BugModal/web/comments.js
extensions/EditComments/Extension.pm
extensions/EditComments/lib/WebService.pm
extensions/EditComments/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl
extensions/EditComments/template/en/default/hook/bug_modal/activity_stream-comment_action.html.tmpl
extensions/EditComments/template/en/default/hook/bug_modal/activity_stream-comment_meta.html.tmpl [new file with mode: 0644]
extensions/EditComments/template/en/default/hook/bug_modal/header-end.html.tmpl
extensions/EditComments/template/en/default/hook/global/header-start.html.tmpl [new file with mode: 0644]
extensions/EditComments/template/en/default/pages/comment-revisions.html.tmpl [new file with mode: 0644]
extensions/EditComments/template/en/default/pages/editcomments.html.tmpl [deleted file]
extensions/EditComments/web/js/editcomments.js [deleted file]
extensions/EditComments/web/js/inline-editor.js [new file with mode: 0644]
extensions/EditComments/web/js/revisions.js [new file with mode: 0644]
extensions/EditComments/web/styles/editcomments.css [deleted file]
extensions/EditComments/web/styles/inline-editor.css [new file with mode: 0644]
extensions/EditComments/web/styles/revisions.css [new file with mode: 0644]
qa/t/test_bug_edit.t
scripts/generate_bmo_data.pl
skins/standard/global.css
template/en/default/global/code-error.html.tmpl

index a72a440f93ad1bd97aded5769735527e00ef7acb..827ffa9105f57dccaf48b7ef4d5c924a8d21bd71 100644 (file)
@@ -39,7 +39,8 @@ use File::Spec;
 use IO::Dir;
 use List::MoreUtils qw(firstidx);
 use Scalar::Util qw(blessed);
-use JSON::XS qw(encode_json);
+use Mojo::JSON qw(encode_json);
+use Encode qw(encode decode);
 
 use parent qw(Template);
 
@@ -888,7 +889,7 @@ sub create {
             },
 
             json_encode => sub {
-                return encode_json($_[0]);
+                return decode('UTF-8', encode_json($_[0]), Encode::FB_DEFAULT);
             },
 
             # Function to create date strings
index 33a38209fee3654d97fc78c4a3f531e61b942039..05e8833f1f53538dca555b0224b89ac374771b94 100644 (file)
           [% Hook.process('comment_action', 'bug_modal/activity_stream.html.tmpl') %]
           [% IF user.id %]
             [% IF user.can_tag_comments %]
-              <button class="tag-btn minor" type="button"
-                data-id="[% comment.id FILTER none %]"
-                data-no="[% comment.count FILTER none %]"
-              >Tag</button>
+              <button type="button" class="tag-btn minor iconic" title="Add a tag to this comment" aria-label="Tag"
+                      data-id="[% comment.id FILTER none %]" data-no="[% comment.count FILTER none %]">
+                <span class="icon" aria-hidden="true"></span>
+              </button>
             [% END %]
-            <button type="button" class="reply-btn minor"
-              data-reply-id="[% comment.count FILTER none %]"
-              data-reply-name="[% comment.author.name || comment.author.nick FILTER html %]"
-              >Reply</button>
+            <button type="button" class="reply-btn minor iconic" title="Reply to this comment" aria-label="Reply"
+                    data-reply-id="[% comment.count FILTER none %]"
+                    data-reply-name="[% comment.author.name || comment.author.nick FILTER html %]">
+              <span class="icon" aria-hidden="true"></span>
+            </button>
           [% END %]
-          <button type="button" class="change-spinner minor" id="cs-[% comment.count FILTER none %]">-</button>
+          <button type="button" class="change-spinner minor iconic" id="cs-[% comment.count FILTER none %]"
+                  title="Collapse this comment" aria-label="Collapse" aria-expanded="true"
+                  data-strings='{ "collapse_label": "Collapse", "collapse_tooltip": "Collapse this comment",
+                                  "expand_label": "Expanded", "expand_tooltip": "Expanded this comment" }'>
+            <span class="icon" aria-hidden="true"></span>
+          </button>
         </td>
       </tr>
 
           <div class="change-time">
             [% INCLUDE bug_modal/rel_time.html.tmpl ts=comment.creation_ts %]
           </div>
+          [% Hook.process('comment_meta', 'bug_modal/activity_stream.html.tmpl') %]
         </td>
       </tr>
 
             Comment hidden ([% comment.collapsed_reason FILTER html %])
         </td>
         <td class="comment-actions">
-          <button type="button" class="change-spinner minor" id="ccs-[% comment.count FILTER none %]">
-            [%~ comment.collapsed ? "+" : "-" ~%]
+          <button type="button" class="change-spinner minor iconic" id="ccs-[% comment.count FILTER none %]"
+                  title="[% comment.collapsed ? 'Expand this comment' : 'Collapse this comment' %]"
+                  aria-label="[% comment.collapsed ? 'Expand' : 'Collapse' %]"
+                  aria-expanded="[% comment.collapsed ? 'false' : 'true' %]"
+                  data-strings='{ "collapse_label": "Collapse", "collapse_tooltip": "Collapse this comment",
+                                  "expand_label": "Expanded", "expand_tooltip": "Expanded this comment" }'>
+            <span class="icon" aria-hidden="true"></span>
           </button>
         </td>
       </tr>
           [% Hook.process('user', 'bug/changes.html.tmpl') %]
         </td>
         <td class="comment-actions">
-          <button type="button" class="change-spinner minor" id="as-[% id FILTER none %]">-</button>
+          <button type="button" class="change-spinner minor iconic" id="as-[% id FILTER none %]"
+                  title="Collapse this change" aria-label="Collapse" aria-expanded="true"
+                  data-strings='{ "collapse_label": "Collapse", "collapse_tooltip": "Collapse this change",
+                                  "expand_label": "Expanded", "expand_tooltip": "Expanded this change" }'>
+            <span class="icon" aria-hidden="true"></span>
+          </button>
         </td>
       </tr>
       <tr id="ar-[% id FILTER none %]">
index dbf94980229e07f67ebed32320a5fc53ec336a83..b7ce55d193e1eec0fa037683ade214c83be64e36 100644 (file)
@@ -594,7 +594,7 @@ td.flag-requestee {
 }
 
 
-.change-name, .change-time, .comment-private {
+.change-name, .change-time {
     display: inline;
 }
 
@@ -604,11 +604,57 @@ h3.change-name {
 }
 
 .comment-actions {
+    display: flex;
+    align-items: center;
     white-space: nowrap;
     vertical-align: top;
     padding: 2px 2px 0 0 !important;
 }
 
+.comment-private {
+    display: inline-block;
+    margin: 0 8px;
+}
+
+.comment-actions button {
+    outline: 0;
+    margin: 0;
+}
+
+.comment-actions button:not(:first-of-type) {
+    border-top-left-radius: 0;
+    border-bottom-left-radius: 0;
+}
+
+.comment-actions button:not(:last-of-type) {
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 0;
+}
+
+.comment-actions button.iconic:not(:disabled) {
+    color: #555;
+}
+
+.comment-actions button.iconic .icon::before {
+    font-family: 'Material Icons';
+}
+
+.comment-actions .tag-btn .icon::before {
+    content: '\E54E';
+}
+
+.comment-actions .reply-btn .icon::before {
+    content: '\E15E';
+}
+
+.comment-actions .change-spinner[aria-expanded="true"] .icon::before {
+    content: '\E15B';
+}
+
+.comment-actions .change-spinner[aria-expanded="false"] .icon::before {
+    content: '\E145';
+}
+
 .change-spinner {
     width: 29px;
 }
index b09cd851fe88016437b9b213e62d9732d0fabb6e..45ee0890df51289af84991b41a54aa82e25bec8e 100644 (file)
@@ -10,6 +10,16 @@ $(function() {
 
     // comment collapse/expand
 
+    const update_spinner = (spinner, expanded) => {
+        const str = spinner.data('strings');
+
+        spinner.attr({
+            'title': expanded ? str.collapse_tooltip : str.expand_tooltip,
+            'aria-label': expanded ? str.collapse_label : str.expand_label,
+            'aria-expanded': expanded,
+        });
+    };
+
     function toggleChange(spinner, forced) {
         var spinnerID = spinner.attr('id');
         var id = spinnerID.substring(spinnerID.indexOf('-') + 1);
@@ -23,24 +33,24 @@ $(function() {
                 changeSet.find(activitySelector).hide();
                 changeSet.find('.gravatar').css('width', '16px').css('height', '16px');
                 $('#ar-' + id).hide();
-                spinner.text('+');
+                update_spinner(spinner, false);
             }
             else if (forced == 'show' || forced == 'reset') {
                 changeSet.find(activitySelector).show();
                 changeSet.find('.gravatar').css('width', '32px').css('height', '32px');
                 $('#ar-' + id).show();
-                spinner.text('-');
+                update_spinner(spinner, true);
             }
             else {
                 changeSet.find(activitySelector).slideToggle('fast', function() {
                     $('#ar-' + id).toggle();
                     if (changeSet.find(activitySelector + ':visible').length) {
                         changeSet.find('.gravatar').css('width', '32px').css('height', '32px');
-                        spinner.text('-');
+                        update_spinner(spinner, true);
                     }
                     else {
                         changeSet.find('.gravatar').css('width', '16px').css('height', '16px');
-                        spinner.text('+');
+                        update_spinner(spinner, false);
                     }
                 });
             }
@@ -72,7 +82,7 @@ $(function() {
             $('#c' + id).find('.comment-tags').hide();
             $('#c' + id).find('.gravatar').css('width', '16px').css('height', '16px');
             $('#cr-' + id).hide();
-            realSpinner.text('+');
+            update_spinner(realSpinner, false);
         }
         else if (forced == 'show') {
             if (defaultCollapsed) {
@@ -87,14 +97,14 @@ $(function() {
             $('#c' + id).find('.comment-tags').show();
             $('#c' + id).find('.gravatar').css('width', '32px').css('height', '32px');
             $('#cr-' + id).show();
-            realSpinner.text('-');
+            update_spinner(realSpinner, true);
         }
         else {
             $('#ct-' + id).slideToggle('fast', function() {
                 $('#c' + id).find(activitySelector).toggle();
                 if ($('#ct-' + id + ':visible').length) {
                     $('#c' + id).find('.comment-tags').show();
-                    realSpinner.text('-');
+                    update_spinner(realSpinner, true);
                     $('#cr-' + id).show();
                     if (BUGZILLA.user.id !== 0)
                         $('#ctag-' + id).show();
@@ -106,7 +116,7 @@ $(function() {
                 }
                 else {
                     $('#c' + id).find('.comment-tags').hide();
-                    realSpinner.text('+');
+                    update_spinner(realSpinner, false);
                     $('#cr-' + id).hide();
                     if (BUGZILLA.user.id !== 0)
                         $('#ctag-' + id).hide();
index 1aa0efcfe62a0271e2e08ed21f02d189e0d42273..0dd531b9a1294542ace7ff94c11b509f9445047f 100644 (file)
@@ -70,20 +70,12 @@ sub install_update_db {
 sub page_before_template {
     my ($self, $args) = @_;
 
-    return if $args->{'page_id'} ne 'editcomments.html';
+    return if $args->{'page_id'} ne 'comment-revisions.html';
 
     my $vars   = $args->{'vars'};
     my $user   = Bugzilla->user;
     my $params = Bugzilla->input_params;
 
-    # validate group membership
-    my $edit_comments_group = Bugzilla->params->{edit_comments_group};
-    if (!$edit_comments_group || !$user->in_group($edit_comments_group)) {
-        ThrowUserError('auth_failure', { group  => $edit_comments_group,
-                                         action => 'view',
-                                         object => 'editcomments' });
-    }
-
     my $bug_id = $params->{bug_id};
     my $bug = Bugzilla::Bug->check($bug_id);
 
@@ -133,8 +125,9 @@ sub _get_activity {
 
     my $dbh = Bugzilla->dbh;
     my $query = 'SELECT longdescs_activity.comment_id AS id, profiles.userid, ' .
-                        $dbh->sql_date_format('longdescs_activity.change_when', '%Y.%m.%d %H:%i:%s') . '
-                        AS time, longdescs_activity.old_comment AS old
+                        $dbh->sql_date_format('longdescs_activity.change_when', '%Y-%m-%d %H:%i:%s') . '
+                        AS time, longdescs_activity.old_comment AS old,
+                        longdescs_activity.is_hidden as is_hidden
                    FROM longdescs_activity
              INNER JOIN profiles
                      ON profiles.userid = longdescs_activity.who
@@ -148,21 +141,19 @@ sub _get_activity {
     # body that the comment was before the edit, not the actual new version
     # of the comment.
     my @activity;
-    my $new_comment;
-    my $last_old_comment;
+    my $prev_rev;
     my $count = 0;
-    while (my $change_ref = $sth->fetchrow_hashref()) {
-        my %change = %$change_ref;
-        $change{'author'} = Bugzilla::User->new({ id => $change{'userid'}, cache => 1 });
-        if ($count == 0) {
-            $change{new} = $self->body;
-        }
-        else {
-            $change{new} = $new_comment;
-        }
-        $new_comment = $change{old};
-        $last_old_comment = $change{old};
-        push (@activity, \%change);
+    while (my $revision = $sth->fetchrow_hashref()) {
+        my $current = $count == 0;
+        push (@activity, {
+            author        => Bugzilla::User->new({ id => $revision->{userid}, cache => 1 }),
+            created_time  => $revision->{time},
+            old           => $revision->{old},
+            revised_time  => $current ? undef : $prev_rev->{time},
+            new           => $current ? $self->body : $prev_rev->{old},
+            is_hidden     => $current ? 0 : $prev_rev->{is_hidden},
+        });
+        $prev_rev = $revision;
         $count++;
     }
 
@@ -171,10 +162,11 @@ sub _get_activity {
     # Store the original comment as the first or last entry
     # depending on sort order
     push(@activity, {
-        author   => $self->author,
-        body     => $last_old_comment,
-        time     => $self->creation_ts,
-        original => 1
+        author        => $self->author,
+        created_time  => $self->creation_ts,
+        revised_time  => $prev_rev->{time},
+        new           => $prev_rev->{old},
+        is_hidden     => $prev_rev->{is_hidden},
     });
 
     $activity_sort_order
@@ -208,7 +200,7 @@ sub bug_end_of_update {
     # or if editing comments is disabled
     my $user = Bugzilla->user;
     my $edit_comments_group = Bugzilla->params->{edit_comments_group};
-    return if (!$edit_comments_group || !$user->in_group($edit_comments_group));
+    return unless $user->is_insider || $edit_comments_group && $user->in_group($edit_comments_group);
 
     my $bug       = $args->{bug};
     my $timestamp = $args->{timestamp};
@@ -224,11 +216,18 @@ sub bug_end_of_update {
         my ($comment_obj) = grep($_->id == $comment_id, @{ $bug->comments});
         next if (!$comment_obj || ($comment_obj->is_private && !$user->is_insider));
 
+        # Insiders can edit any comment while unprivileged users can only edit their own comments
+        next unless $user->is_insider || $comment_obj->author->id == $user->id;
+
         my $new_comment = $comment_obj->_check_thetext($params->{$param});
 
         my $old_comment = $comment_obj->body;
         next if $old_comment eq $new_comment;
 
+        # Insiders can hide comment revisions where needed
+        my $is_hidden = ($user->is_insider && defined $params->{"edit_comment_checkbox_$comment_id"}
+                            && $params->{"edit_comment_checkbox_$comment_id"} == 'on') ? 1 : 0;
+
         trick_taint($new_comment);
         $dbh->do("UPDATE longdescs SET thetext = ?, edit_count = edit_count + 1
                   WHERE comment_id = ?",
@@ -238,9 +237,9 @@ sub bug_end_of_update {
         # Log old comment to the longdescs activity table
         $timestamp ||= $dbh->selectrow_array("SELECT NOW()");
         $dbh->do("INSERT INTO longdescs_activity " .
-                 "(comment_id, who, change_when, old_comment) " .
-                 "VALUES (?, ?, ?, ?)",
-                 undef, ($comment_id, $user->id, $timestamp, $old_comment));
+                 "(comment_id, who, change_when, old_comment, is_hidden) " .
+                 "VALUES (?, ?, ?, ?, ?)",
+                 undef, ($comment_id, $user->id, $timestamp, $old_comment, $is_hidden));
 
         $comment_obj->{thetext} = $new_comment;
 
@@ -256,7 +255,7 @@ sub config_modify_panels {
         name    => 'edit_comments_group',
         type    => 's',
         choices => \&get_all_group_names,
-        default => 'admin',
+        default => 'editbugs',
         checker => \&check_group
     };
 }
index 6969ca7422857e59d0873f6fb85e3ed5f2d12422..d9ebbc7a819e8fbf8a21cad2fe7e9781035006f8 100644 (file)
@@ -13,12 +13,18 @@ use warnings;
 
 use base qw(Bugzilla::WebService);
 
+use Bugzilla;
+use Bugzilla::Comment;
+use Bugzilla::Constants;
 use Bugzilla::Error;
-use Bugzilla::Util qw(trim);
+use Bugzilla::Template;
+use Bugzilla::Util qw(trick_taint trim);
 use Bugzilla::WebService::Util qw(validate);
 
 use constant PUBLIC_METHODS => qw(
     comments
+    update_comment
+    modify_revision
 );
 
 sub comments {
@@ -59,6 +65,109 @@ sub comments {
     return { comments => \%comments };
 }
 
+# See Bugzilla::Extension::EditComments->bug_end_of_update for the original implementation.
+# This should be migrated to the standard API method at /rest/bug/comment/(comment_id)
+sub update_comment {
+    my ($self, $params) = @_;
+    my $user = Bugzilla->login(LOGIN_REQUIRED);
+    my $edit_comments_group = Bugzilla->params->{edit_comments_group};
+
+    # Validate group membership
+    ThrowUserError('auth_failure', { group => $edit_comments_group, action => 'view', object => 'editcomments' })
+        unless $user->is_insider || $edit_comments_group && $user->in_group($edit_comments_group);
+
+    my $comment_id = (defined $params->{comment_id} && $params->{comment_id} =~ /^(\d+)$/) ? $1 : undef;
+
+    # Validate parameters
+    ThrowCodeError('param_required', { function => 'EditComments.update_comment', param => 'comment_id' })
+        unless defined $comment_id;
+    ThrowCodeError('param_required', { function => 'EditComments.update_comment', param => 'new_comment' })
+        unless defined $params->{new_comment} && trim($params->{new_comment}) ne '';
+
+    my $comment = Bugzilla::Comment->new($comment_id);
+
+    # Validate comment visibility
+    ThrowUserError('comment_id_invalid', { id => $comment_id })
+        unless $comment;
+    ThrowUserError('comment_is_private', { id => $comment->id })
+        unless $user->is_insider || !$comment->is_private;
+
+    # Insiders can edit any comment while unprivileged users can only edit their own comments
+    ThrowUserError('auth_failure', { group => 'insidergroup', action => 'view', object => 'editcomments' })
+        unless $user->is_insider || $comment->author->id == $user->id;
+
+    my $bug = $comment->bug;
+    my $old_comment = $comment->body;
+    my $new_comment = $comment->_check_thetext($params->{new_comment});
+
+    # Validate bug visibility
+    $bug->check_is_visible();
+
+    # Make sure there is any change in the comment
+    ThrowCodeError('param_no_changes', { function => 'EditComments.update_comment', param => 'new_comment' })
+        if $old_comment eq $new_comment;
+
+    my $dbh = Bugzilla->dbh;
+    my $change_when = $dbh->selectrow_array('SELECT NOW()');
+
+    # Insiders can hide comment revisions where needed
+    my $is_hidden = ($user->is_insider && defined $params->{is_hidden} && $params->{is_hidden} == 1) ? 1 : 0;
+
+    # Update the `longdescs` (comments) table
+    trick_taint($new_comment);
+    $dbh->do('UPDATE longdescs SET thetext = ?, edit_count = edit_count + 1 WHERE comment_id = ?',
+             undef, $new_comment, $comment_id);
+    Bugzilla->memcached->clear({ table => 'longdescs', id => $comment_id });
+
+    # Log old comment to the `longdescs_activity` (comment revisions) table
+    $dbh->do('INSERT INTO longdescs_activity (comment_id, who, change_when, old_comment, is_hidden)
+              VALUES (?, ?, ?, ?, ?)', undef, ($comment_id, $user->id, $change_when, $old_comment, $is_hidden));
+
+    $comment->{thetext} = $new_comment;
+    $bug->_sync_fulltext( update_comments => 1 );
+
+    # Respond with the updated comment and number of revisions
+    return {
+        text  => $self->type('string', $new_comment),
+        html  => $self->type('string', Bugzilla::Template::quoteUrls($new_comment, $bug)),
+        count => $self->type('int', $dbh->selectrow_array('SELECT COUNT(*) FROM longdescs_activity
+                                                           WHERE comment_id = ?', undef, ($comment_id))),
+    };
+}
+
+sub modify_revision {
+    my ($self, $params) = @_;
+    my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+    # Only allow insiders to modify revisions
+    ThrowUserError('auth_failure', { group => 'insidergroup', action => 'view', object => 'editcomments' })
+        unless $user->is_insider;
+
+    my $comment_id = (defined $params->{comment_id}
+        && $params->{comment_id} =~ /^(\d+)$/) ? $1 : undef;
+    my $change_when = (defined $params->{change_when}
+        && $params->{change_when} =~ /^(\d{4}-\d{2}-\d{2}\ \d{2}:\d{2}:\d{2})$/) ? $1 : undef;
+    my $is_hidden = defined $params->{is_hidden} && $params->{is_hidden} == 1 ? 1 : 0;
+
+    # Validate parameters
+    ThrowCodeError('param_required', { function => 'EditComments.modify_revision', param => 'comment_id' })
+        unless defined $comment_id;
+    ThrowCodeError('param_required', { function => 'EditComments.modify_revision', param => 'change_when' })
+        unless defined $change_when;
+
+    my $dbh = Bugzilla->dbh;
+
+    # Update revision visibility
+    $dbh->do('UPDATE longdescs_activity SET is_hidden = ? WHERE comment_id = ? AND change_when = ?',
+             undef, ($is_hidden, $comment_id, $change_when));
+
+    # Respond with updated revision info
+    return {
+        change_when => $self->type('dateTime', $change_when),
+        is_hidden   => $self->type('boolean', $is_hidden),
+    };
+}
+
 sub rest_resources {
     return [
         qr{^/editcomments/comment/(\d+)$}, {
@@ -68,12 +177,23 @@ sub rest_resources {
                     return { comment_ids => $_[0] };
                 },
             },
+            PUT => {
+                method => 'update_comment',
+                params => sub {
+                    return { comment_id => $_[0] };
+                },
+            },
         },
         qr{^/editcomments/comment$}, {
             GET => {
                 method => 'comments',
             },
         },
+        qr{^/editcomments/revision$}, {
+            PUT => {
+                method => 'modify_revision',
+            },
+        },
     ];
 };
 
index 01ca7bbb79a35f5a1a4a9e82e28f39477c2f7a73..c35dc0d98555e88956f0df120c2b54b488959551 100644 (file)
@@ -8,6 +8,7 @@
 
 [% IF panel.name == "groupsecurity" %]
   [% panel.param_descs.edit_comments_group =
-    'The name of the group of users who can edit comments. Leave blank to disable comment editing.'
+    'The name of the group of users who can edit their own comments. Leave it blank to disable comment editing. ' _
+    'Insiders can always edit any comment and hide revisions where needed.'
   %]
 [% END -%]
index d5028c4a1a57c83c1b91fd048f74243d0f37d0d4..76208017e48134d1733b28c156662ac029046d16 100644 (file)
@@ -8,7 +8,8 @@
 
 [%
   RETURN IF comment.body == '';
-  RETURN UNLESS Param('edit_comments_group') && user.in_group(Param('edit_comments_group'));
+  RETURN UNLESS user.is_insider
+    || Param('edit_comments_group') && user.in_group(Param('edit_comments_group')) && comment.author.id == user.id;
   RETURN UNLESS
     comment.type == constants.CMT_NORMAL
     || comment.type == constants.CMT_DUPE_OF
     || comment.type == constants.CMT_ATTACHMENT_UPDATED;
 %]
 
-[% IF comment.edit_count %]
-  <a href="[% basepath FILTER none %]page.cgi?id=editcomments.html&bug_id=[% bug.id FILTER none %]&amp;comment_id=[% comment.id FILTER none %]"
-     title="[% comment.edit_count FILTER none %] edit[% "s" UNLESS comment.edit_count == 1 %]">(Edited)</a>
-[% END %]
-
-<button class="edit-comment-btn minor edit-show" type="button" style="display:none"
-  data-id="[% comment.id FILTER none %]"
-  data-no="[% comment.count FILTER none %]">Edit</button>
+<button type="button" class="edit-btn minor iconic" title="Edit this comment" aria-label="Edit">
+  <span class="icon" aria-hidden="true"></span>
+</button>
diff --git a/extensions/EditComments/template/en/default/hook/bug_modal/activity_stream-comment_meta.html.tmpl b/extensions/EditComments/template/en/default/hook/bug_modal/activity_stream-comment_meta.html.tmpl
new file mode 100644 (file)
index 0000000..a6ea7ce
--- /dev/null
@@ -0,0 +1,17 @@
+[%# 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.
+  #%]
+
+[% RETURN UNLESS comment.edit_count %]
+
+&bull;
+<div class="change-revisions">
+  <a href="[% basepath FILTER none %]page.cgi?id=comment-revisions.html&amp;bug_id=
+           [%- bug.id FILTER none %]&amp;comment_id=[% comment.id FILTER none %]"
+     title="[% (comment.edit_count == 1 ? "%d revision" : "%d revisions")
+              FILTER replace('%d', comment.edit_count) %]">Edited</a>
+</div>
index 68597bcb09a94ac17bdd5d7b0472c1455ed6cf38..b4854778591920e1487ef13345418f64f47ecda1 100644 (file)
@@ -7,8 +7,11 @@
   #%]
 
 [%
-  IF Param('edit_comments_group') && user.in_group(Param('edit_comments_group'));
-    style_urls.push('extensions/EditComments/web/styles/editcomments.css');
-    javascript_urls.push('extensions/EditComments/web/js/editcomments.js');
-  END;
+  # Always load CSS to style the "Edited" revision link
+  style_urls.push('extensions/EditComments/web/styles/inline-editor.css');
+
+  RETURN UNLESS user.is_insider
+    || Param('edit_comments_group') && user.in_group(Param('edit_comments_group'));
+
+  javascript_urls.push('extensions/EditComments/web/js/inline-editor.js');
 %]
diff --git a/extensions/EditComments/template/en/default/hook/global/header-start.html.tmpl b/extensions/EditComments/template/en/default/hook/global/header-start.html.tmpl
new file mode 100644 (file)
index 0000000..58cd779
--- /dev/null
@@ -0,0 +1,29 @@
+[%# 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.
+  #%]
+
+[%
+  RETURN UNLESS template.name == 'bug/show-modal.html.tmpl';
+  RETURN UNLESS user.is_insider
+    || Param('edit_comments_group') && user.in_group(Param('edit_comments_group'));
+
+  # Expose strings used in JavaScript
+  js_BUGZILLA.string.InlineCommentEditor = {
+    cancel => 'Cancel',
+    cancel_tooltip => 'Discard the changes',
+    edited => 'Edited',
+    fetch_error => 'Raw comment could not be loaded. Please try again later.',
+    hide_revision => 'Hide This Revision',
+    loading => 'Loading…',
+    revision_count => [ '%d revision', '%d revisions' ],
+    save => 'Update Comment',
+    save_error => 'Updated comment could not be saved. Please try again later.',
+    save_tooltip => 'Save the changes',
+    saving => 'Saving…',
+    toolbar => 'Comment Editor Toolbar',
+  };
+%]
diff --git a/extensions/EditComments/template/en/default/pages/comment-revisions.html.tmpl b/extensions/EditComments/template/en/default/pages/comment-revisions.html.tmpl
new file mode 100644 (file)
index 0000000..d4cc4e0
--- /dev/null
@@ -0,0 +1,58 @@
+[%# 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.
+  #%]
+
+[%
+  PROCESS global/variables.none.tmpl;
+  PROCESS global/header.html.tmpl
+    title           = "$terms.Bug $bug.id Comment $comment.count Edit History"
+    style_urls      = ['extensions/EditComments/web/styles/revisions.css']
+    javascript_urls = (user.is_insider ? ['extensions/EditComments/web/js/revisions.js'] : [])
+    generate_api_token = 1
+%]
+
+<h2>[% "$terms.Bug $bug.id Comment $comment.count" FILTER none %] Edit History</h2>
+
+<p>
+  <strong>Note</strong>: The actual edited comment in the [% terms.bug %] view
+  page will always show the original commentor’s name and original timestamp.
+</p>
+
+[% SET rev_count = 0 %]
+[% FOREACH a = comment.activity %]
+  <article class="revision" data-comment-id="[% comment.id FILTER html %]" data-revised-time="[% a.revised_time FILTER html %]">
+    <header>
+      <div class="metadata">
+        [% a.old ? "Revision $rev_count" : "Original comment" FILTER html %]
+        by [% INCLUDE bug_modal/user.html.tmpl u=a.author %]
+        on <time datetime="[% a.created_time FILTER html %]">[% a.created_time FILTER time %]</time>
+      </div>
+      [% IF user.is_insider && a.revised_time %]
+        <div class="actions">
+          <label><input type="checkbox" name="is_hidden" [% "checked" IF a.is_hidden %]> Hide</label>
+        </div>
+      [% END %]
+    </header>
+    <div class="body">
+      [% IF !user.is_insider && a.is_hidden %]
+        <div class="hidden-comment">(Hidden by Administrator)</div>
+      [% ELSE %]
+        <pre class="bz_comment_text">[% a.new FILTER quoteUrls(bug) %]</pre>
+      [% END %]
+    </div>
+  </article>
+  [% rev_count = rev_count + 1 %]
+[% END %]
+
+[% IF !comment.activity.size %]
+  <p><em>No changes have been made to this comment.</em></p>
+[% END %]
+
+<p>[% "Back to $terms.Bug $bug.id Comment $comment.count"
+      FILTER bug_link(bug, { comment_num => comment.count }) FILTER none %]</p>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/EditComments/template/en/default/pages/editcomments.html.tmpl b/extensions/EditComments/template/en/default/pages/editcomments.html.tmpl
deleted file mode 100644 (file)
index 13364f5..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-[%# 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.
-  #%]
-
-[%
-  PROCESS global/variables.none.tmpl;
-  PROCESS global/header.html.tmpl
-    title      = "$bug.id comment $comment.count Activity"
-    style_urls = ['extensions/EditComments/web/styles/editcomments.css']
-%]
-
-<h4>
-  Comment changes made to
-  [%= "$terms.bug $bug.id comment $comment.count"
-      FILTER bug_link(bug, { comment_num => comment.count })
-      FILTER none %]
-</h4>
-
-<p>
-  <b>Note</b>: The actual edited comment in the [% terms.bug %] view
-  page will always show the original commentor's name and original timestamp.
-</p>
-
-[% FOREACH a = comment.activity %]
-  <div class="edit-head">
-    <div class="edit-author-when">
-    [% a.original ? "Original comment" : "Revision" %]
-    by [% INCLUDE bug_modal/user.html.tmpl u=a.author %]
-    on [% a.time FILTER time %]
-    </div>
-  </div>
-  <pre class="bz_comment_text">
-    [%- a.original ? a.body : a.new FILTER quoteUrls(bug) -%]
-  </pre>
-[% END %]
-
-[% IF comment.activity.size %]
-  <p>
-    [%= "Back to $terms.bug $bug.id comment $comment.count"
-        FILTER bug_link(bug, { comment_num => comment.count })
-        FILTER none %]
-  </p>
-[% END %]
-
-[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/EditComments/web/js/editcomments.js b/extensions/EditComments/web/js/editcomments.js
deleted file mode 100644 (file)
index 04e4a2f..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/.
- *
- * This Source Code Form is "Incompatible With Secondary Licenses", as
- * defined by the Mozilla Public License, v. 2.0.
- */
-
-$(function() {
-    $('.edit-comment-btn')
-        .click(function(event) {
-            event.preventDefault();
-            var that = $(this);
-            var id = that.data('id');
-            var no = that.data('no');
-
-            // cancel editing
-            if (that.data('editing')) {
-                that.data('editing', false).text('Edit');
-                $('#edit_comment_textarea_' + id).remove();
-                $('#ct-' + no).show();
-                return;
-            }
-            that.text('Unedit');
-
-            // replace comment <pre> with loading message
-            $('#ct-' + no)
-                .hide()
-                .after(
-                    $('<pre/>')
-                        .attr('id', 'edit-comment-loading-' + id)
-                        .addClass('edit-comment-loading')
-                        .text('Loading...')
-                );
-
-            // load original comment text
-            bugzilla_ajax(
-                {
-                    url: `${BUGZILLA.config.basepath}rest/editcomments/comment/${id}`,
-                    hideError: true
-                },
-                function(data) {
-                    // create editing textarea
-                    $('#edit-comment-loading-' + id).remove();
-                    that.data('editing', true);
-                    $('#ct-' + no)
-                        .after(
-                            $('<textarea/>')
-                                .attr('name', 'edit_comment_textarea_' + id)
-                                .attr('id', 'edit_comment_textarea_' + id)
-                                .addClass('edit-comment-textarea')
-                                .val(data.comments[id])
-                        );
-                },
-                function(message) {
-                    // unedit and show message
-                    that.data('editing', false).text('Edit');
-                    $('#edit-comment-loading-' + id).remove();
-                    $('#ct-' + no).show();
-                    alert(message);
-                }
-            );
-        });
-});
diff --git a/extensions/EditComments/web/js/inline-editor.js b/extensions/EditComments/web/js/inline-editor.js
new file mode 100644 (file)
index 0000000..f99bc5d
--- /dev/null
@@ -0,0 +1,275 @@
+/* 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.
+ */
+
+/**
+ * Reference or define the Bugzilla app namespace.
+ * @namespace
+ */
+var Bugzilla = Bugzilla || {}; // eslint-disable-line no-var
+
+/**
+ * Iterate all comments, and initialize the inline comment editor on each.
+ */
+Bugzilla.InlineCommentEditorInit = class InlineCommentEditorInit {
+  /**
+   * Initialize a new InlineCommentEditorInit instance.
+   */
+  constructor() {
+    document.querySelectorAll('.change-set').forEach($change_set => {
+      if ($change_set.querySelector('.edit-btn')) {
+        new Bugzilla.InlineCommentEditor($change_set);
+      }
+    });
+  }
+};
+
+/**
+ * Provide the inline comment editing functionality that allows to edit and update a comment on the bug page.
+ */
+Bugzilla.InlineCommentEditor = class InlineCommentEditor {
+  /**
+   * Initialize a new InlineCommentEditor instance.
+   * @param {HTMLElement} $change_set Comment outer.
+   */
+  constructor($change_set) {
+    this.str = BUGZILLA.string.InlineCommentEditor;
+    this.comment_id = Number($change_set.querySelector('.comment').dataset.id);
+    this.commenter_id = Number($change_set.querySelector('.email').dataset.userId);
+
+    this.$change_set = $change_set;
+    this.$edit_button = $change_set.querySelector('.edit-btn');
+    this.$revisions_link = $change_set.querySelector('.change-revisions a');
+    this.$body = $change_set.querySelector('.comment-text');
+
+    this.$edit_button.addEventListener('click', event => this.edit_button_onclick(event));
+  }
+
+  /**
+   * Check if the comment is edited.
+   * @private
+   * @readonly
+   * @type {Boolean}
+   */
+  get edited() {
+    return this.$textarea.value !== this.raw_comment;
+  }
+
+  /**
+   * Check if the user is on the macOS platform.
+   * @private
+   * @readonly
+   * @type {Boolean}
+   */
+  get on_mac() {
+    return navigator.platform === 'MacIntel';
+  }
+
+  /**
+   * Called whenever the Edit button is clicked.
+   * @param {MouseEvent} event Click event.
+   */
+  edit_button_onclick(event) {
+    event.preventDefault();
+
+    this.toggle_toolbar_buttons(true);
+    this.$body.hidden = true;
+
+    // Replace the comment body with a disabled `<textarea>` filled with the text as a placeholder while retrieving the
+    // raw comment text
+    this.$body.insertAdjacentHTML('afterend',
+      `<textarea class="comment-editor-textarea" disabled>${this.$body.textContent}</textarea>`);
+    this.$textarea = this.$body.nextElementSibling;
+    this.$textarea.style.height = `${this.$textarea.scrollHeight}px`;
+
+    // Insert a toolbar that provides the Save and Cancel buttons as well as the Hide This Revision checkbox for admin
+    this.$textarea.insertAdjacentHTML('afterend',
+      `
+      <div role="toolbar" class="comment-editor-toolbar" aria-label="${this.str.toolbar}">
+        ${BUGZILLA.user.is_insider && BUGZILLA.user.id !== this.commenter_id ? `
+          <label><input type="checkbox" value="on" checked> ${this.str.hide_revision}</label>` : ''}
+        <button type="button" class="minor" data-action="cancel" title="${this.str.cancel_tooltip} (Esc)"
+                aria-keyshortcuts="Escape">${this.str.cancel}</button>
+        <button type="button" class="major" disabled data-action="save"
+                title="${this.str.save_tooltip} (${this.on_mac ? '&#x2318;Return' : 'Ctrl+Enter'})"
+                aria-keyshortcuts="${this.on_mac ? 'Meta+Enter' : 'Ctrl+Enter'}">${this.str.save}</button>
+      </div>
+      `
+    );
+    this.$toolbar = this.$textarea.nextElementSibling;
+
+    this.$save_button = this.$toolbar.querySelector('button[data-action="save"]');
+    this.$cancel_button = this.$toolbar.querySelector('button[data-action="cancel"]');
+    this.$is_hidden_checkbox = this.$toolbar.querySelector('input[type="checkbox"]');
+
+    this.$textarea.addEventListener('input', event => this.textarea_oninput(event));
+    this.$textarea.addEventListener('keydown', event => this.textarea_onkeydown(event));
+    this.$save_button.addEventListener('click', () => this.save());
+    this.$cancel_button.addEventListener('click', () => this.finish());
+
+    // Retrieve the raw comment text
+    bugzilla_ajax({
+      url: `${BUGZILLA.config.basepath}rest/editcomments/comment/${this.comment_id}`,
+      hideError: true,
+    }, data => {
+      this.fetch_onload(data);
+    }, message => {
+      this.fetch_onerror(message);
+    });
+  }
+
+  /**
+   * Called whenever the comment `<textarea>` is edited. Enable or disable the Save button depending on the content.
+   * @param {KeyboardEvent} event `input` event.
+   */
+  textarea_oninput(event) {
+    if (event.isComposing) {
+      return;
+    }
+
+    this.$save_button.disabled = !this.edited || !!this.$textarea.value.match(/^\s*$/);
+  }
+
+  /**
+   * Called whenever any key is pressed on the comment `<textarea>`. Handle a couple of shortcut keys.
+   * @param {KeyboardEvent} event `keydown` event.
+   */
+  textarea_onkeydown(event) {
+    if (event.isComposing) {
+      return;
+    }
+
+    const { key, altKey, ctrlKey, metaKey, shiftKey } = event;
+    const accelKey = this.on_mac ? metaKey && !ctrlKey : ctrlKey;
+
+    // Accel + Enter = Save
+    if (key === 'Enter' && accelKey && !altKey && !shiftKey) {
+      this.save();
+    }
+
+    // Escape = Cancel
+    if (key === 'Escape' && !accelKey && !altKey && !shiftKey) {
+      this.finish();
+    }
+  }
+
+  /**
+   * Called whenever the Update Comment button is clicked. Upload the changes to the server.
+   */
+  save() {
+    if (!this.edited) {
+      return;
+    }
+
+    // Disable the `<textarea>` and Save button while waiting for the response
+    this.$textarea.disabled = this.$save_button.disabled = true;
+    this.$save_button.textContent = this.str.saving;
+
+    bugzilla_ajax({
+      url: `${BUGZILLA.config.basepath}rest/editcomments/comment/${this.comment_id}`,
+      type: 'PUT',
+      hideError: true,
+      data: {
+        new_comment: this.$textarea.value,
+        is_hidden: this.$is_hidden_checkbox && this.$is_hidden_checkbox.checked ? 1 : 0,
+      },
+    }, data => {
+      this.save_onsuccess(data);
+    }, message => {
+      this.save_onerror(message);
+    });
+  }
+
+  /**
+   * Finish editing by restoring the UI, once editing is complete or cancelled. Any unsaved comment will be discarded.
+   */
+  finish() {
+    this.toggle_toolbar_buttons(false);
+    this.$edit_button.focus();
+    this.$body.hidden = false;
+    this.$textarea.remove();
+    this.$toolbar.remove();
+  }
+
+  /**
+   * Enable or disable buttons on the comment actions toolbar (not the editor's own toolbar) while editing the comment
+   * to avoid any unexpected behaviour.
+   * @param {Boolean} disabled Whether the buttons should be disabled.
+   */
+  toggle_toolbar_buttons(disabled) {
+    this.$change_set.querySelectorAll('.comment-actions button').forEach($button => $button.disabled = disabled);
+  }
+
+  /**
+   * Called whenever a raw comment text is successfully retrieved. Fill in the `<textarea>` so user can start editing.
+   * @param {Object} data Response data.
+   */
+  fetch_onload(data) {
+    this.$textarea.value = this.raw_comment = data.comments[this.comment_id];
+    this.$textarea.style.height = `${this.$textarea.scrollHeight}px`;
+    this.$textarea.disabled = false;
+    this.$textarea.focus();
+    this.$textarea.selectionStart = this.$textarea.value.length;
+
+    // Add `name` attribute to form widgets so the revision can also be submitted while saving the entire bug
+    this.$textarea.name = `edit_comment_textarea_${this.comment_id}`;
+    this.$is_hidden_checkbox ? this.$is_hidden_checkbox.name = `edit_comment_checkbox_${this.comment_id}` : '';
+  }
+
+  /**
+   * Called whenever a raw comment text could not be retrieved. Restore the UI, and display an error message.
+   * @param {String} message Error message.
+   */
+  fetch_onerror(message) {
+    this.finish();
+    window.alert(`${this.str.fetch_error}\n\n${message}`);
+  }
+
+  /**
+   * Called whenever an updated comment is successfully saved. Restore the UI, and insert/update the revision link.
+   * @param {Object} data Response data.
+   */
+  save_onsuccess(data) {
+    this.$body.innerHTML = data.html;
+    this.finish();
+
+    if (!this.$revisions_link) {
+      const $time = this.$change_set.querySelector('.change-time');
+      const params = new URLSearchParams({
+        id: 'comment-revisions.html',
+        bug_id: BUGZILLA.bug_id,
+        comment_id: this.comment_id,
+      });
+
+      $time.insertAdjacentHTML('afterend',
+        `
+        &bull;
+        <div class="change-revisions">
+          <a href="${BUGZILLA.config.basepath}page.cgi?${params.toString().htmlEncode()}">${this.str.edited}</a>
+        </div>
+        `
+      );
+      this.$revisions_link = $time.nextElementSibling.querySelector('a');
+    }
+
+    this.$revisions_link.title = this.str.revision_count[data.count === 1 ? 0 : 1].replace('%d', data.count);
+  }
+
+  /**
+   * Called whenever an updated comment could not be saved. Re-enable the `<textarea>` and Save button, and display an
+   * error message.
+   * @param {String} message Error message.
+   */
+  save_onerror(message) {
+    this.$textarea.disabled = this.$save_button.disabled = false;
+    this.$save_button.textContent = this.str.save;
+
+    window.alert(`${this.str.save_error}\n\n${message}`);
+  }
+};
+
+window.addEventListener('DOMContentLoaded', () => new Bugzilla.InlineCommentEditorInit());
diff --git a/extensions/EditComments/web/js/revisions.js b/extensions/EditComments/web/js/revisions.js
new file mode 100644 (file)
index 0000000..4284736
--- /dev/null
@@ -0,0 +1,55 @@
+/* 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.
+ */
+
+/**
+ * Reference or define the Bugzilla app namespace.
+ * @namespace
+ */
+var Bugzilla = Bugzilla || {}; // eslint-disable-line no-var
+
+/**
+ * Allow admin to hide specific comment revisions from public, in case any of these contains sensitive info.
+ */
+Bugzilla.CommentRevisionsManager = class CommentRevisionsManager {
+  /**
+   * Initialize a new CommentRevisionsManager instance.
+   */
+  constructor() {
+    document.querySelectorAll('.revision').forEach($revision => this.activate($revision));
+  }
+
+  /**
+   * Activate the "Hide" checkbox on each revision so the change triggers an immediate update to the database.
+   * @param {HTMLElement} $revision Revision container node.
+   */
+  activate($revision) {
+    const $checkbox = $revision.querySelector('input[name="is_hidden"]');
+
+    // The current revision cannot be hidden so there is no checkbox on it
+    if (!$checkbox) {
+      return;
+    }
+
+    const comment_id = Number($revision.dataset.commentId);
+    const change_when = $revision.dataset.revisedTime;
+
+    $checkbox.addEventListener('change', () => {
+      bugzilla_ajax({
+        url: `${BUGZILLA.config.basepath}rest/editcomments/revision`,
+        type: 'PUT',
+        data: {
+          comment_id,
+          change_when,
+          is_hidden: $checkbox.checked ? 1 : 0,
+        },
+      });
+    });
+  }
+};
+
+window.addEventListener('DOMContentLoaded', () => new Bugzilla.CommentRevisionsManager());
diff --git a/extensions/EditComments/web/styles/editcomments.css b/extensions/EditComments/web/styles/editcomments.css
deleted file mode 100644 (file)
index 99f4f96..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-/* 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. */
-
-.edit-comment-textarea {
-    width: 100%;
-    height: 15em;
-}
-
-.edit-comment-loading {
-    background: #fff;
-    padding: 8px;
-    border-top: 1px solid #ddd;
-    margin: 1px 0 0;
-    font-style: italic;
-}
-
-.edit-head {
-    width: 50em;
-    padding: 10px;
-}
-
-.edit-head .vcard {
-    display: inline;
-}
diff --git a/extensions/EditComments/web/styles/inline-editor.css b/extensions/EditComments/web/styles/inline-editor.css
new file mode 100644 (file)
index 0000000..17bd95b
--- /dev/null
@@ -0,0 +1,39 @@
+/* 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. */
+
+.change-revisions {
+  display: inline;
+  font-size: small;
+}
+
+.comment-actions .edit-btn .icon::before {
+  content: '\E254';
+}
+
+.comment-editor-toolbar {
+  padding: 8px;
+  background-color: #EEE;
+  text-align: right;
+}
+
+.comment-editor-toolbar label {
+  margin: 0 8px;
+}
+
+.comment-editor-textarea {
+  border: 0;
+  border-radius: 0;
+  padding: 8px;
+  width: 100%;
+  min-height: 5em;
+  font: 13px/1.2 "Droid Sans Mono", Menlo, Monaco, "Courier New", Courier, monospace;
+  resize: vertical;
+}
+
+.comment-editor-textarea:disabled {
+  background-color: #F3F3F3;
+}
diff --git a/extensions/EditComments/web/styles/revisions.css b/extensions/EditComments/web/styles/revisions.css
new file mode 100644 (file)
index 0000000..ccaeb54
--- /dev/null
@@ -0,0 +1,33 @@
+/* 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. */
+
+.revision header {
+  display: flex;
+  padding: 10px;
+  width: 50em;
+}
+
+.revision .metadata {
+  flex: auto;
+}
+
+.revision .metadata .vcard {
+  display: inline;
+}
+
+.revision .actions {
+  flex: none;
+}
+
+.revision .hidden-comment {
+  padding: 10px;
+  font-style: italic;
+}
+
+.revision .bz_comment_text {
+  font: 13px/1.2 "Droid Sans Mono", Menlo, Monaco, "Courier New", Courier, monospace;
+}
index 45f8225a3ab5a5264a216ee80b4a290566abb476..3eccd5d048f2fd13d0594228df73380f054b9cb3 100644 (file)
@@ -35,7 +35,7 @@ $sel->click_ok("link=Groups");
 check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/editgroups.cgi});
 $sel->title_is("Edit Groups");
 $sel->click_ok("link=Master");
-check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/editgroups.cgi?action=changeform&group=26});
+check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/editgroups.cgi?action=changeform&group=25});
 $sel->title_is("Change Group: Master");
 my $group_url = $sel->get_location();
 $group_url =~ /group=(\d+)$/;
index 6356762c8433c73f5af7b64188e523be83c51564..8bc2a78b545640c90da7d689a15dbfbf38ffb1f4 100755 (executable)
@@ -394,13 +394,6 @@ my @groups = (
         bug_group    => 1,
         all_products => 1,
     },
-    {
-        name         => 'can_edit_comments',
-        description  => 'Members of this group will be able to edit comments',
-        no_admin     => 0,
-        bug_group    => 0,
-        all_products => 0,
-    },
     {
         name         => 'can_restrict_comments',
         description  => 'Members of this group will be able to restrict comments on bugs',
@@ -528,7 +521,7 @@ my %set_params = (
                                  '&emailtype2=exact&order=Importance&keywords_type=allwords' .
                                  '&long_desc_type=substring',
     defaultseverity           => 'normal',
-    edit_comments_group       => 'can_edit_comments',
+    edit_comments_group       => 'editbugs',
     insidergroup              => 'core-security-release',
     last_visit_keep_days      => '28',
     lxr_url                   => 'http://mxr.mozilla.org/mozilla',
index 19a77207bc7846f0bbe0d0760c592f481f2bba3c..76f6aa250f6a1477716ae0a485f9f8c4c7c0aa75 100644 (file)
@@ -1669,7 +1669,9 @@ button.minor {
     box-shadow: 0 1px 0 0 rgba(0,0,0,0.1), inset 0 -1px 0 0 rgba(0,0,0,0.1), inset 0 0 1px 0 rgba(0,0,0,0.1);
 }
 
-button.minor:hover {
+button.minor:not(:disabled):hover,
+button.minor:not(:disabled):focus,
+button.minor:not(:disabled):active {
     -webkit-box-shadow: 0 1px 0 0 rgba(0,0,0,0.2), inset 0 -1px 0 0 rgba(0,0,0,0.3), inset 0 12px 24px 2px #ddd;
     -moz-box-shadow: 0 1px 0 0 rgba(0,0,0,0.2), inset 0 -1px 0 0 rgba(0,0,0,0.3), inset 0 12px 24px 2px #ddd;
     box-shadow: 0 1px 0 0 rgba(0,0,0,0.1), inset 0 -1px 0 0 rgba(0,0,0,0.1), inset 0 12px 24px 2px #ddd;
@@ -1679,6 +1681,10 @@ button.minor[disabled] {
     color: #999;
 }
 
+button::-moz-focus-inner {
+    border: 0;
+}
+
 .notransition {
   -webkit-transition: none !important;
   -moz-transition: none !important;
index 8aaf1012794e8e0d0c7104d0911d36c7395b33a0..0a404b9f056a443ddb1ea6e7c3b758e24a43394e 100644 (file)
     Invalid parameter <code>[% param FILTER html %]</code> passed to
     <code>[% function FILTER html %]</code>: It must be numeric.
 
+  [% ELSIF error == "param_no_changes" %]
+    [% title = "Invalid Parameter" %]
+    Invalid parameter <code>[% param FILTER html %]</code> passed to
+    <code>[% function FILTER html %]</code>: It must be different from the current value.
+
   [% ELSIF error == "param_required" %]
     [% title = "Missing Parameter" %]
     The function <code>[% function FILTER html %]</code> requires