diff options
Diffstat (limited to 'extensions/Review')
4 files changed, 162 insertions, 22 deletions
diff --git a/extensions/Review/template/en/default/hook/global/header-badge.html.tmpl b/extensions/Review/template/en/default/hook/global/header-badge.html.tmpl index aca61561e..df3dd82be 100644 --- a/extensions/Review/template/en/default/hook/global/header-badge.html.tmpl +++ b/extensions/Review/template/en/default/hook/global/header-badge.html.tmpl @@ -6,18 +6,35 @@ # defined by the Mozilla Public License, v. 2.0. #%] -[% RETURN UNLESS - user.review_request_count - || user.feedback_request_count - || user.needinfo_request_count -%] - -<a id="header-flags" class="badge" - href="request.cgi?action=queue&requestee=[% user.login FILTER uri %]&group=type" - title="Flags requested of you: - [%- " review (" _ user.review_request_count _ ")" IF user.review_request_count -%] - [%- " feedback (" _ user.feedback_request_count _ ")" IF user.feedback_request_count -%] - [%- " needinfo (" _ user.needinfo_request_count _ ")" IF user.needinfo_request_count -%] -"> - [%- user.review_request_count + user.feedback_request_count + user.needinfo_request_count ~%] -</a> +[% IF user.id %] + [% request_count = user.review_request_count + user.feedback_request_count + user.needinfo_request_count %] + <div id="header-requests" class="dropdown"> + <button type="button" id="header-requests-menu-button" class="dropdown-button minor" + title="Requests for you[%- IF request_count -%]: + [%- " review (" _ user.review_request_count _ ")" IF user.review_request_count -%] + [%- " feedback (" _ user.feedback_request_count _ ")" IF user.feedback_request_count -%] + [%- " needinfo (" _ user.needinfo_request_count _ ")" IF user.needinfo_request_count -%][%- END -%]" + aria-label="Requests for you" aria-expanded="false" aria-haspopup="true" aria-controls="header-requests-menu"> + [%- IF request_count -%] + <span class="badge">[% request_count FILTER html %]</span> + [%- ELSE -%] + <span class="icon" aria-hidden="true"></span> + [%- END -%] + </button> + <section class="dropdown-content dropdown-panel left" id="header-requests-menu" role="menu" style="display:none;"> + <header> + <h2>Requests</h2> + </header> + [%- IF request_count -%] + <div class="loading">Loading…</div> + <ul class="notifications" role="none" hidden></ul> + [%- ELSE -%] + <div class="empty">You’re all caught up!</div> + [%- END -%] + <footer> + <div><a href="request.cgi?action=queue&requestee=[% user.login FILTER uri %]&group=requestee" + role="menuitem" tabindex="-1">See All</a></div> + </footer> + </section> + </div> +[% END %] diff --git a/extensions/Review/template/en/default/hook/global/header-start.html.tmpl b/extensions/Review/template/en/default/hook/global/header-start.html.tmpl index 3da136f41..5441ea270 100644 --- a/extensions/Review/template/en/default/hook/global/header-start.html.tmpl +++ b/extensions/Review/template/en/default/hook/global/header-start.html.tmpl @@ -6,12 +6,8 @@ # defined by the Mozilla Public License, v. 2.0. #%] -[% IF user.review_request_count - || user.feedback_request_count - || user.needinfo_request_count -%] - [% style_urls.push('extensions/Review/web/styles/badge.css') %] -[% END %] +[% style_urls.push('extensions/Review/web/styles/badge.css') %] +[% javascript_urls.push('js/util.js', 'js/lib/md5.min.js', 'extensions/Review/web/js/badge.js') %] [% RETURN UNLESS template.name == 'attachment/edit.html.tmpl' || template.name == 'attachment/create.html.tmpl' diff --git a/extensions/Review/web/js/badge.js b/extensions/Review/web/js/badge.js new file mode 100644 index 000000000..cff22dc40 --- /dev/null +++ b/extensions/Review/web/js/badge.js @@ -0,0 +1,102 @@ +/* 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 || {}; + +/** + * Reference or define the Review namespace. + * @namespace + */ +Bugzilla.Review = Bugzilla.Review || {}; + +/** + * Provide the Badge functionality that shows the current review summary in the dropdown. + */ +Bugzilla.Review.Badge = class Badge { + /** + * Get a new Badge instance. + * @returns {Badge} New Badge instance. + */ + constructor() { + this.$button = document.querySelector('#header-requests-menu-button'); + this.$panel = document.querySelector('#header-requests .dropdown-panel'); + this.$loading = document.querySelector('#header-requests .dropdown-panel .loading'); + + if (this.$loading) { + this.$button.addEventListener('click', () => this.init(), { once: true }); + } + } + + /** + * Initialize the Reviews dropdown menu. + */ + async init() { + const url = this.$panel.querySelector('footer a').href + '&ctype=json'; + const response = await fetch(url, { credentials: 'same-origin' }); + const _requests = response.ok ? await response.json() : []; + + if (!response.ok) { + this.$loading.innerHTML = 'Couldn’t load requests for you.<br>Please try again later.'; + + return; + } + + if (!_requests.length) { + this.$loading.className = 'empty'; + this.$loading.innerHTML = 'You’re all caught up!'; + + return; + } + + const requests = []; + const $ul = this.$panel.querySelector('ul'); + const $fragment = document.createDocumentFragment(); + + // Sort requests from new to old, then group reviews/feedbacks asked by the same person in the same bug + _requests.reverse().forEach(_req => { + const dup_index = requests.findIndex(req => req.requester === _req.requester + && req.bug_id === _req.bug_id && req.type === _req.type && req.attach_id && _req.attach_id); + + if (dup_index > -1) { + requests[dup_index].dup_count++; + } else { + _req.dup_count = 1; + requests.push(_req); + } + }); + + // Show up to 20 newest requests + requests.slice(0, 20).forEach(req => { + const $li = document.createElement('li'); + const [, name, email] = req.requester.match(/^(.*)\s<(.*)>$/); + const pretty_name = name.replace(/([\[\(<‹].*?[›>\)\]]|\:[\w\-]+|\s+\-\s+.*)/g, '').trim(); + const link = req.attach_id && req.dup_count === 1 + ? `attachment.cgi?id=${req.attach_id}&action=edit` : `show_bug.cgi?id=${req.bug_id}`; + + $li.setAttribute('role', 'none'); + $li.innerHTML = `<a href="${link}" role="menuitem" tabindex="-1" data-type="${req.type}"> + <img src="https://secure.gravatar.com/avatar/${md5(email)}?d=mm&size=64" alt=""> + <label><strong>${pretty_name.htmlEncode()}</strong> asked for your + ${(req.type === 'needinfo' ? 'info' : req.type)} ${(req.attach_id ? 'on' : '')} + ${(req.attach_id && req.ispatch ? (req.dup_count > 1 ? `${req.dup_count} patches` : 'a patch') : '')} + ${(req.attach_id && !req.ispatch ? (req.dup_count > 1 ? `${req.dup_count} files` : 'a file') : '')} + in <strong>Bug ${req.bug_id} – ${req.bug_summary.htmlEncode()}</strong>.</label> + <time datetime="${req.created}">${timeAgo(new Date(req.created))}</time></a>`; + $fragment.appendChild($li); + }); + + this.$loading.remove(); + $ul.appendChild($fragment); + $ul.hidden = false; + } +} + +window.addEventListener('DOMContentLoaded', () => new Bugzilla.Review.Badge(), { once: true }); diff --git a/extensions/Review/web/styles/badge.css b/extensions/Review/web/styles/badge.css index 53f539df8..9a6e14a8c 100644 --- a/extensions/Review/web/styles/badge.css +++ b/extensions/Review/web/styles/badge.css @@ -5,7 +5,32 @@ * This Source Code Form is "Incompatible With Secondary Licenses", as * defined by the Mozilla Public License, v. 2.0. */ -#header .badge { +#header-requests { + display: flex; + justify-content: center; + align-items: center; + margin: 0 4px !important; + width: 32px; + height: 32px; +} + +#header-requests-menu-button .icon { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + background: #BBB; + color: #FFF; + border-radius: 50%; +} + +#header-requests-menu-button .icon::before { + font-size: 16px; + content: '\E7F4'; +} + +#header-requests-menu-button .badge { display: flex; align-items: center; justify-content: center; |