]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 1477931 - Show number of review/feedback/needinfo in user autocomplete and preven...
authorKohei Yoshino <kohei.yoshino@gmail.com>
Fri, 8 Mar 2019 14:27:04 +0000 (09:27 -0500)
committerGitHub <noreply@github.com>
Fri, 8 Mar 2019 14:27:04 +0000 (09:27 -0500)
extensions/Gravatar/Extension.pm
extensions/Needinfo/template/en/default/bug/needinfo.html.tmpl
js/field.js
skins/standard/global.css
template/en/default/flag/list.html.tmpl
template/en/default/global/userselect.html.tmpl

index 04d5f309075ff04c1c8ef074bb2b8a4ddefd2f8f..f63f88517d13eeb6cf91378085f6979123ac42fc 100644 (file)
@@ -15,7 +15,9 @@ use base qw(Bugzilla::Extension);
 
 use Bugzilla::Extension::Gravatar::Data qw( %gravatar_user_map );
 use Bugzilla::User::Setting;
+use Bugzilla::WebService::Util qw(filter_wants);
 use Digest::MD5 qw(md5_hex);
+use Scalar::Util qw(blessed);
 
 use constant DEFAULT_URL => 'extensions/Gravatar/web/default.jpg';
 
@@ -55,4 +57,35 @@ sub install_before_final_checks {
   });
 }
 
+#
+# hooks
+#
+
+sub webservice_user_get {
+  my ($self, $args) = @_;
+  my ($webservice, $params, $users) = @$args{qw(webservice params users)};
+
+  return unless filter_wants($params, 'gravatar');
+
+  my $ids = [
+    map { blessed($_->{id}) ? $_->{id}->value : $_->{id} }
+    grep { exists $_->{id} }
+    @$users
+  ];
+
+  return unless @$ids;
+
+  my %user_map = map { $_->id => $_ } @{ Bugzilla::User->new_from_list($ids) };
+  foreach my $user (@$users) {
+    my $id = blessed($user->{id}) ? $user->{id}->value : $user->{id};
+    my $user_obj = $user_map{$id};
+    $user->{gravatar} = $user_obj->gravatar;
+  }
+}
+
+sub webservice_user_suggest {
+  my ($self, $args) = @_;
+  $self->webservice_user_get($args);
+}
+
 __PACKAGE__->NAME;
index 46083e108538e60d1ed75feccb64f20439fc540d..43e632e6c3c6b451b83387d0245c90276b9f5498 100644 (file)
@@ -233,6 +233,7 @@ $(function() {
               multiple    => 5
               classes     => ["needinfo_from_changed"]
               field_title => "Enter one or more comma separated users to request more information from"
+              request_type => "needinfo"
           %]
         </span>
         <span id="needinfo_role_identity"></span>
index 6c7e70692b26a3658a23019bbf2c60dde7a7412e..109d253b5e54930596ec6a017a37adb416a19018 100644 (file)
@@ -700,9 +700,6 @@ function browserCanHideOptions(aSelect) {
  */
 
 $(function() {
-
-    // single user
-
     function searchComplete() {
         var that = $(this);
         that.data('counter', that.data('counter') - 1);
@@ -726,22 +723,55 @@ $(function() {
         noCache: true,
         tabDisabled: true,
         autoSelectFirst: true,
+        preserveInput: true,
         triggerSelectOnValidInput: false,
         transformResult: function(response) {
             response = $.parseJSON(response);
             return {
-                suggestions: $.map(response.users, function(dataItem) {
+                suggestions: $.map(response.users, function({ name, real_name, requests, gravatar } = {}) {
                     return {
-                        value: dataItem.name,
-                        data : { login: dataItem.name, name: dataItem.real_name }
+                        value: name,
+                        data : { email: name, real_name, requests, gravatar }
                     };
                 })
             };
         },
-        formatResult: function(suggestion, currentValue) {
-            return (suggestion.data.name === '' ?
-                suggestion.data.login : suggestion.data.name + ' (' + suggestion.data.login + ')')
-                .htmlEncode();
+        formatResult: function(suggestion) {
+            const $input = this;
+            const user = suggestion.data;
+            const request_type = $input.getAttribute('data-request-type');
+            const blocked = user.requests && request_type ? user.requests[request_type].blocked : false;
+            const pending = user.requests && request_type ? user.requests[request_type].pending : 0;
+            const image = user.gravatar ? `<img itemprop="image" alt="" src="${user.gravatar}">` : '';
+            const description = blocked ? '<span class="icon" aria-hidden="true"></span> Requests blocked' :
+                pending ? `${pending} pending ${request_type}${pending === 1 ? '' : 's'}` : '';
+
+            return `<div itemscope itemtype="http://schema.org/Person">${image} ` +
+                `<span itemprop="name">${user.real_name.htmlEncode()}</span> ` +
+                `<span class="minor" itemprop="email">${user.email.htmlEncode()}</span> ` +
+                `<span class="minor${blocked ? ' blocked' : ''}" itemprop="description">${description}</span></div>`;
+        },
+        onSelect: function (suggestion) {
+            const $input = this;
+            const user = suggestion.data;
+            const is_multiple = !!$input.getAttribute('data-multiple');
+            const request_type = $input.getAttribute('data-request-type');
+            const blocked = user.requests && request_type ? user.requests[request_type].blocked : false;
+
+            if (blocked) {
+                window.alert(`${user.real_name} is not accepting ${request_type} requests at this time. ` +
+                    'If you’re in a hurry, ask someone else for help.');
+            } else if (is_multiple) {
+                const _values = $input.value.split(',').map(value => value.trim());
+
+                _values.pop();
+                _values.push(suggestion.value);
+                $input.value = _values.join(', ') + ', ';
+            } else {
+                $input.value = suggestion.value;
+            }
+
+            $input.focus();
         },
         onSearchStart: function(params) {
             var that = $(this);
@@ -766,28 +796,21 @@ $(function() {
         onSearchError: searchComplete
     };
 
-    // multiple users (based on single user)
-    var options_users = {
-        delimiter: /,\s*/,
-        onSelect: function() {
-            this.value = this.value + ', ';
-            this.focus();
-        },
-    };
-    $.extend(options_users, options_user);
-
     // init user autocomplete fields
     $('.bz_autocomplete_user')
         .each(function() {
-            var that = $(this);
-            that.data('counter', 0);
-            if (that.data('multiple')) {
-                that.devbridgeAutocomplete(options_users);
-            }
-            else {
-                that.devbridgeAutocomplete(options_user);
-            }
-            that.addClass('bz_autocomplete');
+            const $input = this;
+            const is_multiple = !!$input.getAttribute('data-multiple');
+            const options = Object.assign({}, options_user);
+
+            options.delimiter = is_multiple ? /,\s*/ : undefined;
+            // Override `this` in the relevant functions
+            options.formatResult = options.formatResult.bind($input);
+            options.onSelect = options.onSelect.bind($input);
+
+            $input.dataset.counter = 0;
+            $input.classList.add('bz_autocomplete');
+            $(this).devbridgeAutocomplete(options);
         });
 
     // init autocomplete fields with array of values
index 3737731dd9b98f2562e14fa15dcf3586bdd4e196..7ba7d3aab1d15afc629d150506b8cc7c2f0183a9 100644 (file)
@@ -996,8 +996,10 @@ input.required, select.required, span.required_explanation {
 
 .autocomplete-suggestions {
     border: 1px solid #999;
+    border-radius: 4px;
     background: #fff;
     color: #000;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, .3);
     overflow-x: hidden;
     overflow-y: auto;
     cursor: pointer;
@@ -1005,11 +1007,56 @@ input.required, select.required, span.required_explanation {
 }
 
 .autocomplete-suggestion {
-    padding: 2px 5px;
+    padding: 4px 6px;
     white-space: nowrap;
     overflow: hidden;
     width: 100%;
-    margin-right: 1.5em;
+    box-sizing: border-box;
+}
+
+.autocomplete-suggestion [itemtype] {
+    display: flex;
+    align-items: center;
+    padding: 2px 2px 2px 0;
+    font-size: 14px;
+    pointer-events: none;
+}
+
+.autocomplete-suggestion [itemtype] > span {
+    margin-left: 12px;
+}
+
+.autocomplete-suggestion [itemtype] > span:first-of-type,
+.autocomplete-suggestion [itemtype] > span:empty {
+    margin-left: 0;
+}
+
+.autocomplete-suggestion [itemtype] img {
+    margin-right: 6px;
+    border-radius: 50%;
+    width: 20px;
+    height: 20px;
+}
+
+.autocomplete-suggestion [itemtype] .minor {
+    opacity: .7;
+    font-size: 13px;
+}
+
+.autocomplete-suggestion [itemtype] .blocked {
+    margin-left: 8px;
+    border-radius: 12px;
+    padding: 1px 6px 1px 3px;
+    color: #C00;
+    background-color: #FFF;
+    opacity: 1;
+}
+
+.autocomplete-suggestion [itemtype] .blocked .icon::before {
+    font-size: 15px;
+    font-family: 'Material Icons';
+    vertical-align: -3px;
+    content: '\E033';
 }
 
 .autocomplete-selected {
index f4aeb1e99110dc23f64d32bdb9c35d06a4b0c45c..bb191e1dd68b4bda7506b75f46e781607799ce29 100644 (file)
                          classes  => ["requestee"]
                          custom_userlist => grant_list
                          disabled => !can_edit_flag
+                         request_type => type.name
               %]
               [% Hook.process("requestee", "flag/list.html.tmpl") %]
             </span>
index 18e38e1f2ae5e7864b93f4995f4ef38fd31bb6c7..6f59e3702bde054295c78cd0350efc72d860ceab 100644 (file)
@@ -22,6 +22,7 @@
   # placeholder: optional, input only; placeholder attribute value
   # mandatory: optional; if true, the field cannot be empty.
   # aria_labelledby: optiona; extra information to use for arai labels
+  # request_type: optional; one of the request flag types: "review", "feedback" or "needinfo"
   #%]
 
 [% THROW "onchange is not allowed" IF onchange %]
@@ -96,5 +97,6 @@
     [% IF id %] id="[% id FILTER html %]" [% END %]
     [% IF mandatory %] required [% END %]
     [% IF multiple %] data-multiple="1" [% END %]
+    [% IF request_type %] data-request-type="[% request_type FILTER html %]" [% END %]
   >
 [% END %]