summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKohei Yoshino <kohei.yoshino@gmail.com>2018-01-08 20:52:22 +0100
committerDylan William Hardison <dylan@hardison.net>2018-01-08 20:52:22 +0100
commit91dc6dc99fa7a699e0b8e822a5c294509c9e9eb7 (patch)
tree01ab20bc979b3703856b776643c638f8b300f84d
parent9b3834764b26d0a806b72fdc3657075b7ab9c7c0 (diff)
downloadbugzilla-91dc6dc99fa7a699e0b8e822a5c294509c9e9eb7.tar.gz
bugzilla-91dc6dc99fa7a699e0b8e822a5c294509c9e9eb7.tar.xz
Bug 1428641 - Implement Requests quick look dropdown on global header
-rw-r--r--extensions/Review/template/en/default/hook/global/header-badge.html.tmpl47
-rw-r--r--extensions/Review/template/en/default/hook/global/header-start.html.tmpl8
-rw-r--r--extensions/Review/web/js/badge.js102
-rw-r--r--extensions/Review/web/styles/badge.css27
-rw-r--r--js/lib/md5.min.js2
-rw-r--r--skins/standard/global.css88
-rw-r--r--template/en/default/request/queue.json.tmpl42
7 files changed, 294 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&amp;requestee=[% user.login FILTER uri %]&amp;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&amp;requestee=[% user.login FILTER uri %]&amp;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}&amp;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&amp;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} &ndash; ${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;
diff --git a/js/lib/md5.min.js b/js/lib/md5.min.js
new file mode 100644
index 000000000..7d8a3f53c
--- /dev/null
+++ b/js/lib/md5.min.js
@@ -0,0 +1,2 @@
+!function(n){"use strict";function t(n,t){var r=(65535&n)+(65535&t);return(n>>16)+(t>>16)+(r>>16)<<16|65535&r}function r(n,t){return n<<t|n>>>32-t}function e(n,e,o,u,c,f){return t(r(t(t(e,n),t(u,f)),c),o)}function o(n,t,r,o,u,c,f){return e(t&r|~t&o,n,t,u,c,f)}function u(n,t,r,o,u,c,f){return e(t&o|r&~o,n,t,u,c,f)}function c(n,t,r,o,u,c,f){return e(t^r^o,n,t,u,c,f)}function f(n,t,r,o,u,c,f){return e(r^(t|~o),n,t,u,c,f)}function i(n,r){n[r>>5]|=128<<r%32,n[14+(r+64>>>9<<4)]=r;var e,i,a,d,h,l=1732584193,g=-271733879,v=-1732584194,m=271733878;for(e=0;e<n.length;e+=16)i=l,a=g,d=v,h=m,g=f(g=f(g=f(g=f(g=c(g=c(g=c(g=c(g=u(g=u(g=u(g=u(g=o(g=o(g=o(g=o(g,v=o(v,m=o(m,l=o(l,g,v,m,n[e],7,-680876936),g,v,n[e+1],12,-389564586),l,g,n[e+2],17,606105819),m,l,n[e+3],22,-1044525330),v=o(v,m=o(m,l=o(l,g,v,m,n[e+4],7,-176418897),g,v,n[e+5],12,1200080426),l,g,n[e+6],17,-1473231341),m,l,n[e+7],22,-45705983),v=o(v,m=o(m,l=o(l,g,v,m,n[e+8],7,1770035416),g,v,n[e+9],12,-1958414417),l,g,n[e+10],17,-42063),m,l,n[e+11],22,-1990404162),v=o(v,m=o(m,l=o(l,g,v,m,n[e+12],7,1804603682),g,v,n[e+13],12,-40341101),l,g,n[e+14],17,-1502002290),m,l,n[e+15],22,1236535329),v=u(v,m=u(m,l=u(l,g,v,m,n[e+1],5,-165796510),g,v,n[e+6],9,-1069501632),l,g,n[e+11],14,643717713),m,l,n[e],20,-373897302),v=u(v,m=u(m,l=u(l,g,v,m,n[e+5],5,-701558691),g,v,n[e+10],9,38016083),l,g,n[e+15],14,-660478335),m,l,n[e+4],20,-405537848),v=u(v,m=u(m,l=u(l,g,v,m,n[e+9],5,568446438),g,v,n[e+14],9,-1019803690),l,g,n[e+3],14,-187363961),m,l,n[e+8],20,1163531501),v=u(v,m=u(m,l=u(l,g,v,m,n[e+13],5,-1444681467),g,v,n[e+2],9,-51403784),l,g,n[e+7],14,1735328473),m,l,n[e+12],20,-1926607734),v=c(v,m=c(m,l=c(l,g,v,m,n[e+5],4,-378558),g,v,n[e+8],11,-2022574463),l,g,n[e+11],16,1839030562),m,l,n[e+14],23,-35309556),v=c(v,m=c(m,l=c(l,g,v,m,n[e+1],4,-1530992060),g,v,n[e+4],11,1272893353),l,g,n[e+7],16,-155497632),m,l,n[e+10],23,-1094730640),v=c(v,m=c(m,l=c(l,g,v,m,n[e+13],4,681279174),g,v,n[e],11,-358537222),l,g,n[e+3],16,-722521979),m,l,n[e+6],23,76029189),v=c(v,m=c(m,l=c(l,g,v,m,n[e+9],4,-640364487),g,v,n[e+12],11,-421815835),l,g,n[e+15],16,530742520),m,l,n[e+2],23,-995338651),v=f(v,m=f(m,l=f(l,g,v,m,n[e],6,-198630844),g,v,n[e+7],10,1126891415),l,g,n[e+14],15,-1416354905),m,l,n[e+5],21,-57434055),v=f(v,m=f(m,l=f(l,g,v,m,n[e+12],6,1700485571),g,v,n[e+3],10,-1894986606),l,g,n[e+10],15,-1051523),m,l,n[e+1],21,-2054922799),v=f(v,m=f(m,l=f(l,g,v,m,n[e+8],6,1873313359),g,v,n[e+15],10,-30611744),l,g,n[e+6],15,-1560198380),m,l,n[e+13],21,1309151649),v=f(v,m=f(m,l=f(l,g,v,m,n[e+4],6,-145523070),g,v,n[e+11],10,-1120210379),l,g,n[e+2],15,718787259),m,l,n[e+9],21,-343485551),l=t(l,i),g=t(g,a),v=t(v,d),m=t(m,h);return[l,g,v,m]}function a(n){var t,r="",e=32*n.length;for(t=0;t<e;t+=8)r+=String.fromCharCode(n[t>>5]>>>t%32&255);return r}function d(n){var t,r=[];for(r[(n.length>>2)-1]=void 0,t=0;t<r.length;t+=1)r[t]=0;var e=8*n.length;for(t=0;t<e;t+=8)r[t>>5]|=(255&n.charCodeAt(t/8))<<t%32;return r}function h(n){return a(i(d(n),8*n.length))}function l(n,t){var r,e,o=d(n),u=[],c=[];for(u[15]=c[15]=void 0,o.length>16&&(o=i(o,8*n.length)),r=0;r<16;r+=1)u[r]=909522486^o[r],c[r]=1549556828^o[r];return e=i(u.concat(d(t)),512+8*t.length),a(i(c.concat(e),640))}function g(n){var t,r,e="";for(r=0;r<n.length;r+=1)t=n.charCodeAt(r),e+="0123456789abcdef".charAt(t>>>4&15)+"0123456789abcdef".charAt(15&t);return e}function v(n){return unescape(encodeURIComponent(n))}function m(n){return h(v(n))}function p(n){return g(m(n))}function s(n,t){return l(v(n),v(t))}function C(n,t){return g(s(n,t))}function A(n,t,r){return t?r?s(t,n):C(t,n):r?m(n):p(n)}"function"==typeof define&&define.amd?define(function(){return A}):"object"==typeof module&&module.exports?module.exports=A:n.md5=A}(this);
+//# sourceMappingURL=md5.min.js.map \ No newline at end of file
diff --git a/skins/standard/global.css b/skins/standard/global.css
index ea25eb88a..48e4754ab 100644
--- a/skins/standard/global.css
+++ b/skins/standard/global.css
@@ -311,6 +311,94 @@
background-color: transparent;
}
+ #header .dropdown-panel {
+ padding: 0 !important;
+ width: 400px;
+ max-width: none !important;
+ }
+
+ #header .dropdown-panel header {
+ border-bottom: 1px solid #CCC;
+ }
+
+ #header .dropdown-panel h2 {
+ margin: 0;
+ padding: 8px 12px;
+ font-size: 14px;
+ line-height: 100%;
+ font-weight: normal;
+ }
+
+ #header .dropdown-panel ul {
+ overflow-y: auto;
+ margin: 0;
+ padding: 0;
+ max-height: 480px;
+ list-style-type: none;
+ }
+
+ #header .dropdown-panel li:not(:last-child) {
+ border-bottom: 1px solid #CCC;
+ }
+
+ #header .dropdown-panel li a {
+ padding: 12px !important;
+ }
+
+ #header .dropdown-panel li a:hover {
+ background-color: rgba(0, 0, 0, .05) !important;
+ }
+
+ #header .dropdown-panel li a * {
+ pointer-events: none;
+ }
+
+ #header .dropdown-panel .notifications img {
+ float: left;
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+ }
+
+ #header .dropdown-panel .notifications img ~ * {
+ display: block;
+ margin-left: 52px;
+ }
+
+ #header .dropdown-panel .notifications label {
+ overflow: hidden;
+ max-height: 40px;
+ }
+
+ #header .dropdown-panel .notifications strong {
+ font-weight: 600;
+ }
+
+ #header .dropdown-panel .notifications time {
+ font-size: 12px;
+ color: #999;
+ }
+
+ #header .dropdown-panel .loading,
+ #header .dropdown-panel .empty {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 240px;
+ line-height: 150%;
+ text-align: center;
+ }
+
+ #header .dropdown-panel footer {
+ border-top: 1px solid #CCC;
+ text-align: center;
+ }
+
+ #header .dropdown-panel footer a {
+ padding: 8px 16px !important;
+ line-height: 100% !important;
+ }
+
#header-search h2 {
position: absolute;
left: -99999px;
diff --git a/template/en/default/request/queue.json.tmpl b/template/en/default/request/queue.json.tmpl
new file mode 100644
index 000000000..121b52337
--- /dev/null
+++ b/template/en/default/request/queue.json.tmpl
@@ -0,0 +1,42 @@
+[%# 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. #%]
+
+[% RAWPERL %]
+my @display_columns = (
+ "requester", "requestee", "type", "status", "bug_id", "bug_summary",
+ "attach_id", "attach_summary", "ispatch", "created", "category"
+);
+my $requests = $stash->get('requests');
+my $time_filter = $context->filter('time', [ '%Y-%m-%dT%H:%M:%SZ', 'UTC' ]);
+my $mail_filter = $context->filter('email');
+
+my @results;
+foreach my $request (@$requests) {
+ my %item = ();
+ foreach my $column (@display_columns) {
+ my $val;
+ if ( $column eq 'created' ) {
+ $val = $time_filter->( $request->{$column} );
+ }
+ elsif ( $column =~ /^requeste/ ) {
+ $val = $mail_filter->( $request->{$column} );
+ }
+ elsif ( $column =~ /_id$/ ) {
+ $val = $request->{$column} ? 0 + $request->{$column} : undef;
+ }
+ elsif ( $column =~ /^is/ ) {
+ $val = $request->{$column} ? \1 : \0;
+ }
+ else {
+ $val = $request->{$column};
+ }
+ $item{$column} = $val;
+ }
+ push @results, \%item;
+}
+$output .= JSON::XS::encode_json( \@results );
+[% END %]