From: Kohei Yoshino Date: Fri, 8 Mar 2019 14:27:04 +0000 (-0500) Subject: Bug 1477931 - Show number of review/feedback/needinfo in user autocomplete and preven... X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=57f20388f0a580a50056870d0e6553ecd6a5b9d2;p=thirdparty%2Fbugzilla.git Bug 1477931 - Show number of review/feedback/needinfo in user autocomplete and prevent person from being added if requests are blocked --- diff --git a/extensions/Gravatar/Extension.pm b/extensions/Gravatar/Extension.pm index 04d5f3090..f63f88517 100644 --- a/extensions/Gravatar/Extension.pm +++ b/extensions/Gravatar/Extension.pm @@ -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; diff --git a/extensions/Needinfo/template/en/default/bug/needinfo.html.tmpl b/extensions/Needinfo/template/en/default/bug/needinfo.html.tmpl index 46083e108..43e632e6c 100644 --- a/extensions/Needinfo/template/en/default/bug/needinfo.html.tmpl +++ b/extensions/Needinfo/template/en/default/bug/needinfo.html.tmpl @@ -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" %] diff --git a/js/field.js b/js/field.js index 6c7e70692..109d253b5 100644 --- a/js/field.js +++ b/js/field.js @@ -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 ? `` : ''; + const description = blocked ? ' Requests blocked' : + pending ? `${pending} pending ${request_type}${pending === 1 ? '' : 's'}` : ''; + + return `
${image} ` + + `${user.real_name.htmlEncode()} ` + + `${user.email.htmlEncode()} ` + + `${description}
`; + }, + 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 diff --git a/skins/standard/global.css b/skins/standard/global.css index 3737731dd..7ba7d3aab 100644 --- a/skins/standard/global.css +++ b/skins/standard/global.css @@ -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 { diff --git a/template/en/default/flag/list.html.tmpl b/template/en/default/flag/list.html.tmpl index f4aeb1e99..bb191e1dd 100644 --- a/template/en/default/flag/list.html.tmpl +++ b/template/en/default/flag/list.html.tmpl @@ -186,6 +186,7 @@ classes => ["requestee"] custom_userlist => grant_list disabled => !can_edit_flag + request_type => type.name %] [% Hook.process("requestee", "flag/list.html.tmpl") %] diff --git a/template/en/default/global/userselect.html.tmpl b/template/en/default/global/userselect.html.tmpl index 18e38e1f2..6f59e3702 100644 --- a/template/en/default/global/userselect.html.tmpl +++ b/template/en/default/global/userselect.html.tmpl @@ -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 %]