summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKohei Yoshino <kohei.yoshino@gmail.com>2018-07-19 04:44:16 +0200
committerDylan William Hardison <dylan@hardison.net>2018-07-19 04:44:16 +0200
commit535a1fd09e4b150e31165e2e79af42c2e5f2bda5 (patch)
treec2f1e9379e330a5c46611b09af7d98cb71322f1c
parent351b399991094e57e890a9291484c4ab69ca628b (diff)
downloadbugzilla-535a1fd09e4b150e31165e2e79af42c2e5f2bda5.tar.gz
bugzilla-535a1fd09e4b150e31165e2e79af42c2e5f2bda5.tar.xz
Bug 1472954 - Implement one-click component watching on bug modal and component description pages
-rw-r--r--extensions/BMO/template/en/default/hook/global/header-external-links.html.tmpl2
-rw-r--r--extensions/BMO/template/en/default/hook/reports/components-start.html.tmpl10
-rw-r--r--extensions/BMO/template/en/default/reports/components.html.tmpl99
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl12
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl88
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/header.html.tmpl1
-rw-r--r--extensions/BugModal/web/bug_modal.css52
-rw-r--r--extensions/BugModal/web/bug_modal.js40
-rw-r--r--extensions/ComponentWatching/Extension.pm33
-rw-r--r--extensions/ComponentWatching/lib/WebService.pm113
-rw-r--r--extensions/ComponentWatching/template/en/default/hook/reports/components-component_footer.html.tmpl10
-rw-r--r--extensions/ComponentWatching/template/en/default/hook/reports/components-product_header.html.tmpl10
-rw-r--r--extensions/ComponentWatching/template/en/default/hook/reports/components-start.html.tmpl12
-rw-r--r--extensions/ComponentWatching/web/js/overlay.js218
-rw-r--r--js/dropdown.js69
-rw-r--r--js/global.js41
-rw-r--r--skins/standard/describecomponents.css97
-rw-r--r--skins/standard/global.css465
-rw-r--r--skins/standard/reports.css97
-rw-r--r--template/en/default/global/header.html.tmpl6
-rw-r--r--template/en/default/reports/components.html.tmpl111
-rw-r--r--template/en/default/reports/menu.html.tmpl5
22 files changed, 994 insertions, 597 deletions
diff --git a/extensions/BMO/template/en/default/hook/global/header-external-links.html.tmpl b/extensions/BMO/template/en/default/hook/global/header-external-links.html.tmpl
index 54a2f0e49..f79548e3d 100644
--- a/extensions/BMO/template/en/default/hook/global/header-external-links.html.tmpl
+++ b/extensions/BMO/template/en/default/hook/global/header-external-links.html.tmpl
@@ -15,7 +15,7 @@
<li role="presentation">
<a href="https://www.mozilla.org/" role="menuitem" tabindex="-1">Mozilla Home</a>
</li>
- <li role="separator" class="dropdown-separator"></li>
+ <li role="separator"></li>
<li role="presentation">
<a href="https://www.mozilla.org/privacy/websites/" role="menuitem" tabindex="-1">Privacy</a>
</li>
diff --git a/extensions/BMO/template/en/default/hook/reports/components-start.html.tmpl b/extensions/BMO/template/en/default/hook/reports/components-start.html.tmpl
new file mode 100644
index 000000000..a4234caa2
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/reports/components-start.html.tmpl
@@ -0,0 +1,10 @@
+[%# 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.
+ #%]
+
+[%# Don't show the default assignees and QA contacts %]
+[% show_default_people = 0 %]
diff --git a/extensions/BMO/template/en/default/reports/components.html.tmpl b/extensions/BMO/template/en/default/reports/components.html.tmpl
deleted file mode 100644
index 3e23d389e..000000000
--- a/extensions/BMO/template/en/default/reports/components.html.tmpl
+++ /dev/null
@@ -1,99 +0,0 @@
-[%# The contents of this file are subject to the Mozilla Public
- # License Version 1.1 (the "License"); you may not use this file
- # except in compliance with the License. You may obtain a copy of
- # the License at http://www.mozilla.org/MPL/
- #
- # Software distributed under the License is distributed on an "AS
- # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
- # implied. See the License for the specific language governing
- # rights and limitations under the License.
- #
- # The Original Code is the Bugzilla Bug Tracking System.
- #
- # The Initial Developer of the Original Code is Netscape Communications
- # Corporation. Portions created by Netscape are
- # Copyright (C) 1998 Netscape Communications Corporation. All
- # Rights Reserved.
- #
- # Contributor(s): Bradley Baetz <bbaetz@student.usyd.edu.au>
- # Max Kanat-Alexander <mkanat@bugzilla.org>
- #%]
-
-[%# INTERFACE:
- # product: object. The product for which we want to display component
- # descriptions.
- # component: string. The name of the component to hilight in the browser
- #%]
-
-[% title = BLOCK %]
- Components for [% product.name FILTER html %]
-[% END %]
-
-[% inline_style = BLOCK %]
-.product_name {
- font-size: 2em;
- font-weight: normal;
-}
-.component_name {
- font-size: 1.5em;
- font-weight: normal;
-}
-.product_desc, .component_desc {
- padding-left: 1em;
- font-size: 1em;
-}
-.component_container {
- padding-left: 1em;
- margin-bottom: 1em;
-}
-.product_container, .instructions {
- margin-bottom: 1em;
-}
-.component_highlight {
- padding: 0 0 0 1em;
-}
-[% END %]
-
-[% PROCESS global/header.html.tmpl
- style_urls = [ "skins/standard/reports.css" ]
- title = title
- style = inline_style
-%]
-
-<h2>[% mark FILTER html %]</h2>
-
-<div class="product_container">
- <span class="product_name">[% product.name FILTER html %]</span>
- <div class="product_desc">
- [% product.description FILTER html_light %]
- </div>
-</div>
-
-<div class="instructions">
- Select a component to see open [% terms.bugs %] in that component:
-</div>
-
-[% FOREACH comp = product.components %]
- [% INCLUDE describe_comp %]
-[% END %]
-
-[% PROCESS global/footer.html.tmpl %]
-
-[%############################################################################%]
-[%# BLOCK for components %]
-[%############################################################################%]
-
-[% BLOCK describe_comp %]
- <div class="component_container [%- IF comp.name == component_mark %] component_hilite[% END %]">
- <div class="component_name">
- <a name="[% comp.name FILTER html %]"
- href="buglist.cgi?product=
- [%- product.name FILTER uri %]&amp;component=
- [%- comp.name FILTER uri %]&amp;resolution=---">
- [% comp.name FILTER html %]</a>
- </div>
- <div class="component_desc">
- [% comp.description FILTER html_light %]
- </div>
- </div>
-[% END %]
diff --git a/extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl
index af2077e00..36494773b 100644
--- a/extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl
+++ b/extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl
@@ -15,8 +15,8 @@
<div class="dropdown">
<button type="button" id="comment-tags-btn" arai-haspopup="true" aria-label="Tags Menu"
aria-expanded="false" aria-controls="comment-tags-menu" class="dropdown-button minor">Tags &#9662;</button>
- <ul id="comment-tags-menu" role="menu" tabindex="0" class="dropdown-content" style="display:none">
- <li class="dropdown-separator" role="presentation">
+ <ul id="comment-tags-menu" role="menu" tabindex="0" class="dropdown-content left" style="display:none">
+ <li role="presentation">
<a role="menuitem" tabindex="-1" data-comment-tag="">Reset</a>
</li>
</ul>
@@ -24,19 +24,21 @@
<div class="dropdown">
<button type="button" id="view-menu-btn" arai-haspopup="true" aria-label="View Menu"
aria-expanded="false" aria-controls="view-menu" class="dropdown-button minor">View &#9662;</button>
- <ul id="view-menu" role="menu" tabindex="0" class="dropdown-content" style="display:none">
- <li class="dropdown-separator" role="presentation">
+ <ul id="view-menu" role="menu" tabindex="0" class="dropdown-content left" style="display:none">
+ <li role="presentation">
<a id="view-reset" role="menuitem" tabindex="-1">Reset</a>
</li>
+ <li role="separator"></li>
<li role="presentation">
<a id="view-collapse-all" role="menuitem" tabindex="-1">Collapse All</a>
</li>
<li role="presentation">
<a id="view-expand-all" role="menuitem" tabindex="-1">Expand All</a>
</li>
- <li class="dropdown-separator" role="presentation">
+ <li role="presentation">
<a id="view-comments-only" role="menuitem" tabindex="-1">Comments Only</a>
</li>
+ <li role="separator"></li>
<li role="presentation">
<a id="view-toggle-cc" role="menuitem" tabindex="-1">Show CC Changes</a>
</li>
diff --git a/extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl
index e2c8bba26..60ed6ca49 100644
--- a/extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl
+++ b/extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl
@@ -320,24 +320,26 @@
<div class="dropdown">
<button type="button" id="action-menu-btn" aria-haspopup="true" aria-label="Actions Menu"
aria-expanded="false" aria-controls="action-menu" class="dropdown-button minor">&#9662;</button>
- <ul class="dropdown-content" id="action-menu" role="menu" style="display:none;">
+ <ul class="dropdown-content left" id="action-menu" role="menu" style="display:none;">
<li role="presentation">
<a id="action-reset" role="menuitem" tabindex="-1">Reset Sections</a>
</li>
<li role="presentation">
<a id="action-expand-all" role="menuitem" tabindex="-1">Expand All Sections</a>
</li>
- <li class="dropdown-separator" role="presentation">
+ <li role="presentation">
<a id="action-collapse-all" role="menuitem" tabindex="-1">Collapse All Sections</a>
</li>
+ <li role="separator"></li>
[% IF user.id %]
<li role="presentation">
<a id="action-add-comment" role="menuitem" tabindex="-1">Add Comment</a>
</li>
[% END %]
- <li class="dropdown-separator" role="presentation">
+ <li role="presentation">
<a id="action-last-comment" role="menuitem" tabindex="-1">Last Comment</a>
</li>
+ <li role="separator"></li>
<li role="presentation">
<a id="action-history" role="menuitem" tabindex="-1">History</a>
</li>
@@ -374,17 +376,29 @@
hide_on_edit = can_edit_product
help = "describecomponents.cgi?product=$filtered_product"
%]
- <span aria-owns="product-name product-latch">
- <span role="button" aria-label="show product information" aria-expanded="false" tabindex="0"
- class="spin-latch" id="product-latch" data-latch="product" data-for="product">&#9656;</span>
- <div title="show product information" tabindex="0" class="spin-toggle"
- id="product-name" data-latch="product" data-for="product">
+ <div class="name-info-outer dropdown">
+ <span id="product-name" class="dropdown-button" tabindex="0" role="button"
+ aria-haspopup="menu" aria-controls="product-info">
[% bug.product FILTER html %]
- </div>
- <div id="product-info" style="display:none">
- [% bug.product_obj.description FILTER html_light %]
- </div>
- </span>
+ <span class="icon" aria-hidden="true">&#x25BE;</span>
+ </span>
+ <aside id="product-info" class="name-info-popup dropdown-content right hover-display" hidden role="menu"
+ aria-label="Product description and actions">
+ <header>
+ <div class="title">[%~ bug.product FILTER html ~%]</div>
+ <div class="description">[% bug.product_obj.description FILTER html_light %]</div>
+ </header>
+ <li role="separator"></li>
+ <div class="actions">
+ <div><a href="buglist.cgi?product=[% bug.product FILTER uri %]&amp;bug_status=__open__"
+ target="_blank" role="menuitem" tabindex="-1">See Other [% terms.Bugs %]</a></div>
+ <div><button disabled type="button" class="minor component-watching" role="menuitem" tabindex="-1"
+ data-product="[% bug.product FILTER html %]"
+ data-label-watch="Watch This Product" data-label-unwatch="Unwatch This Product"
+ data-source="BugModal">Watch This Product</button></div>
+ </div>
+ </aside>
+ </div>
[% END %]
[% WRAPPER bug_modal/field.html.tmpl
field = bug_fields.product
@@ -417,20 +431,30 @@
help = "describecomponents.cgi?product=$filtered_product&component=$filtered_component#$filtered_component"
%]
- <span aria-owns="component-name component-latch">
- <span role="button" aria-label="show component description" aria-expanded="false" tabindex="0"
- class="spin-latch" id="component-latch" data-latch="component" data-for="component">&#9656;</span>
- <div title="show component information" tabindex="0" class="spin-toggle" id="component-name"
- data-latch="#component-latch" data-for="component">
- [% bug.component FILTER html %]
- </div>
- <div id="component-info" style="display:none">
- <div>[% bug.component_obj.description FILTER html_light %]</div>
- <a href="buglist.cgi?component=[% bug.component FILTER uri %]&amp;
- [%~ %]product=[% bug.product FILTER uri %]&amp;
- [%~ %]bug_status=__open__" target="_blank">Other [% terms.Bugs %]</a>
- </div>
- </span>
+ <div class="name-info-outer dropdown">
+ <span id="component-name" class="dropdown-button" tabindex="0" role="button"
+ aria-haspopup="menu" aria-controls="component-info">
+ [% bug.component FILTER html %]
+ <span class="icon" aria-hidden="true">&#x25BE;</span>
+ </span>
+ <aside id="component-info" class="name-info-popup dropdown-content right hover-display" hidden role="menu"
+ aria-label="Component description and actions">
+ <header>
+ <div class="title">[%~ bug.product _ " :: " _ bug.component FILTER html ~%]</div>
+ <div class="description">[% bug.component_obj.description FILTER html_light %]</div>
+ </header>
+ <li role="separator"></li>
+ <div class="actions">
+ <div><a href="buglist.cgi?product=[% bug.product FILTER uri %]&amp;
+ [%~ %]component=[% bug.component FILTER uri %]&amp;bug_status=__open__"
+ target="_blank" role="menuitem" tabindex="-1">See Other [% terms.Bugs %]</a></div>
+ <div><button disabled type="button" class="minor component-watching" role="menuitem" tabindex="-1"
+ data-product="[% bug.product FILTER html %]" data-component="[% bug.component FILTER html %]"
+ data-label-watch="Watch This Component" data-label-unwatch="Unwatch This Component"
+ data-source="BugModal">Watch This Component</button></div>
+ </div>
+ </aside>
+ </div>
[% END %]
[%# importance %]
@@ -1325,7 +1349,7 @@
<div class="dropdown">
<button type="button" id="format-btn" aria-haspopup="true" aria-label="Format [% terms.Bug %] Menu"
aria-expanded="false" aria-controls="format-menu" class="dropdown-button minor">Format [% terms.Bug %] &#9652;</button>
- <ul class="dropdown-content menu-up" id="format-menu" role="menu" style="display:none;">
+ <ul class="dropdown-content left menu-up" id="format-menu" role="menu" style="display:none;">
<li role="presentation">
<a href="show_bug.cgi?format=multiple&amp;id=[% bug.id FILTER uri %]" role="menuitem" tabindex="-1">For Printing</a>
</li>
@@ -1346,7 +1370,7 @@
<div class="dropdown">
<button type="button" id="new-bug-btn" aria-haspopup="true" aria-label="New/Clone [% terms.Bug %] Menu"
aria-expanded="false" aria-controls="new-bug-menu" class="dropdown-button minor">New/Clone [% terms.Bug %] &#9652;</button>
- <ul class="dropdown-content menu-up" id="new-bug-menu" role="menu" style="display:none;">
+ <ul class="dropdown-content left menu-up" id="new-bug-menu" role="menu" style="display:none;">
<li role="presentation">
<a href="enter_bug.cgi" role="menuitem" tabindex="-1" target="_blank">
Create a new [% terms.bug %]</a>
@@ -1355,18 +1379,20 @@
<a href="enter_bug.cgi?product=[% bug.product FILTER uri %]"
role="menuitem" tabindex="-1" target="_blank">&#8230; in this product</a>
</li>
- <li class="dropdown-separator" role="presentation">
+ <li role="presentation">
<a href="enter_bug.cgi?product=[% bug.product FILTER uri %]&amp;component=[% bug.component FILTER uri %]"
role="menuitem" tabindex="-1" target="_blank">&#8230; in this component</a>
</li>
+ <li role="separator"></li>
<li role="presentation">
<a href="enter_bug.cgi?format=__default__&amp;product=[% bug.product FILTER uri %]&amp;blocked=[% bug.id FILTER uri %]"
role="menuitem" tabindex="-1" target="_blank">&#8230; that blocks this [% terms.bug %]</a>
</li>
- <li class="dropdown-separator" role="presentation">
+ <li role="presentation">
<a href="enter_bug.cgi?format=__default__&amp;product=[% bug.product FILTER uri %]&amp;dependson=[% bug.id FILTER uri %]"
role="menuitem" tabindex="-1" target="_blank">&#8230; that depends on this [% terms.bug %]</a>
</li>
+ <li role="separator"></li>
<li role="presentation">
<a href="enter_bug.cgi?format=__default__&amp;product=[% bug.product FILTER uri %]&amp;cloned_bug_id=[% bug.id FILTER uri %]"
role="menuitem" tabindex="-1" target="_blank">&#8230; as a clone of this [% terms.bug %]</a>
diff --git a/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl
index 47d97fb32..20561c760 100644
--- a/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl
+++ b/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl
@@ -54,6 +54,7 @@
"extensions/ProdCompSearch/web/js/prod_comp_search.js",
"extensions/BugModal/web/bug_modal.js",
"extensions/BugModal/web/comments.js",
+ "extensions/ComponentWatching/web/js/overlay.js",
"js/bugzilla-readable-status-min.js",
"js/field.js",
"js/comments.js",
diff --git a/extensions/BugModal/web/bug_modal.css b/extensions/BugModal/web/bug_modal.css
index a8c469ad6..ee50c6b77 100644
--- a/extensions/BugModal/web/bug_modal.css
+++ b/extensions/BugModal/web/bug_modal.css
@@ -44,26 +44,6 @@ button.major {
padding: 4px 12px;
}
-button.minor {
- background-color: #eee;
- background-image: linear-gradient(#fcfcfc, #eee);
- color: #000;
- font-size: inherit;
- font-weight: 500;
- padding: 4px 8px;
- margin-bottom: 1px;
- text-shadow: none;
- -web-kit-box-shadow: 0 1px 0 0 rgba(0,0,0,0.1), inset 0 -1px 0 0 rgba(0,0,0,0.1);
- -moz-box-shadow: 0 1px 0 0 rgba(0,0,0,0.1), inset 0 -1px 0 0 rgba(0,0,0,0.1);
- 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 {
- -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;
-}
-
select[multiple], .text_input, .yui-ac-input, input {
font-size: 12px !important;
}
@@ -329,16 +309,6 @@ input[type="number"] {
margin-bottom: 50px;
}
-#product-info, #component-info {
- color: #484;
- white-space: normal;
-}
-
-#product-latch, #component-latch {
- padding-right: 0;
- cursor: pointer;
-}
-
#cc-latch {
color: #999;
}
@@ -968,6 +938,28 @@ div.ui-tooltip {
right: 8px;
}
+/* product/component popup */
+
+.name-info-popup {
+ width: 320px;
+}
+
+.name-info-popup header {
+ margin: 8px 16px;
+}
+
+.name-info-popup header .title {
+ margin: 0 0 4px;
+ font-size: 16px;
+}
+
+.name-info-popup header .description {
+ font-size: 12px;
+ line-height: 150%;
+ white-space: normal;
+ color: #666;
+}
+
/* product search */
#field-product {
diff --git a/extensions/BugModal/web/bug_modal.js b/extensions/BugModal/web/bug_modal.js
index c4bff9412..a4ae83d72 100644
--- a/extensions/BugModal/web/bug_modal.js
+++ b/extensions/BugModal/web/bug_modal.js
@@ -1434,46 +1434,6 @@ if (history && history.replaceState) {
}
}
-// ajax wrapper, to simplify error handling and auth
-function bugzilla_ajax(request, done_fn, error_fn) {
- $('#xhr-error').hide('');
- $('#xhr-error').html('');
- request.url += (request.url.match('\\?') ? '&' : '?') +
- 'Bugzilla_api_token=' + encodeURIComponent(BUGZILLA.api_token);
- if (request.type != 'GET') {
- request.contentType = 'application/json';
- request.processData = false;
- if (request.data && request.data.constructor === Object) {
- request.data = JSON.stringify(request.data);
- }
- }
- return $.ajax(request)
- .done(function(data) {
- if (data.error) {
- if (!request.hideError) {
- $('#xhr-error').html(data.message);
- $('#xhr-error').show('fast');
- }
- if (error_fn)
- error_fn(data.message);
- }
- else if (done_fn) {
- done_fn(data);
- }
- })
- .fail(function(data) {
- if (data.statusText === 'abort')
- return;
- var message = data.responseJSON ? data.responseJSON.message : 'Unexpected Error'; // all errors are unexpected :)
- if (!request.hideError) {
- $('#xhr-error').html(message);
- $('#xhr-error').show('fast');
- }
- if (error_fn)
- error_fn(message);
- });
-}
-
// lightbox
function lb_show(el) {
diff --git a/extensions/ComponentWatching/Extension.pm b/extensions/ComponentWatching/Extension.pm
index 01d843c7a..674e0da7b 100644
--- a/extensions/ComponentWatching/Extension.pm
+++ b/extensions/ComponentWatching/Extension.pm
@@ -467,15 +467,18 @@ sub bugmail_relationships {
#
sub _getWatches {
- my ($user) = @_;
+ my ($user, $watch_id) = @_;
my $dbh = Bugzilla->dbh;
+ $watch_id = (defined $watch_id && $watch_id =~ /^(\d+)$/) ? $1 : undef;
+
my $sth = $dbh->prepare("
SELECT id, product_id, component_id, component_prefix
FROM component_watch
- WHERE user_id = ?
- ");
- $sth->execute($user->id);
+ WHERE user_id = ?" . ($watch_id ? " AND id = ?" : "")
+ );
+ $watch_id ? $sth->execute($user->id, $watch_id) : $sth->execute($user->id);
+
my @watches;
while (my ($id, $productId, $componentId, $prefix) = $sth->fetchrow_array) {
my $product = Bugzilla::Product->new({ id => $productId, cache => 1 });
@@ -498,6 +501,10 @@ sub _getWatches {
push @watches, \%watch;
}
+ if ($watch_id) {
+ return $watches[0] || {};
+ }
+
@watches = sort {
$a->{'product_name'} cmp $b->{'product_name'}
|| $a->{'component_name'} cmp $b->{'component_name'}
@@ -563,6 +570,8 @@ sub _addProductWatch {
VALUES (?, ?)
");
$sth->execute($user->id, $product->id);
+
+ return _getWatches($user, $dbh->bz_last_key());
}
sub _addComponentWatch {
@@ -583,6 +592,8 @@ sub _addComponentWatch {
VALUES (?, ?, ?)
");
$sth->execute($user->id, $component->product_id, $component->id);
+
+ return _getWatches($user, $dbh->bz_last_key());
}
sub _addPrefixWatch {
@@ -618,8 +629,9 @@ sub _deleteWatch {
my $dbh = Bugzilla->dbh;
detaint_natural($id) || ThrowCodeError("component_watch_invalid_id");
- $dbh->do("DELETE FROM component_watch WHERE id=? AND user_id=?",
- undef, $id, $user->id);
+
+ return $dbh->do("DELETE FROM component_watch WHERE id=? AND user_id=?",
+ undef, $id, $user->id);
}
sub _addDefaultSettings {
@@ -715,4 +727,13 @@ sub sanitycheck_repair {
}
}
+#
+# webservice
+#
+
+sub webservice {
+ my ($self, $args) = @_;
+ $args->{dispatch}->{ComponentWatching} = "Bugzilla::Extension::ComponentWatching::WebService";
+}
+
__PACKAGE__->NAME;
diff --git a/extensions/ComponentWatching/lib/WebService.pm b/extensions/ComponentWatching/lib/WebService.pm
new file mode 100644
index 000000000..ba4cb0225
--- /dev/null
+++ b/extensions/ComponentWatching/lib/WebService.pm
@@ -0,0 +1,113 @@
+# 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.
+
+package Bugzilla::Extension::ComponentWatching::WebService;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use base qw(Bugzilla::WebService);
+
+use Bugzilla;
+use Bugzilla::Component;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Product;
+use Bugzilla::User;
+
+sub rest_resources {
+ return [
+ qr{^/component-watching$}, {
+ GET => {
+ method => 'list',
+ },
+ POST => {
+ method => 'add',
+ },
+ },
+ qr{^/component-watching/(\d+)$}, {
+ GET => {
+ method => 'get',
+ params => sub {
+ return { id => $_[0] }
+ },
+ },
+ DELETE => {
+ method => 'remove',
+ params => sub {
+ return { id => $_[0] }
+ },
+ },
+ },
+ ];
+}
+
+#
+# API methods based on Bugzilla::Extension::ComponentWatching->user_preferences
+#
+
+sub list {
+ my ($self, $params) = @_;
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+ return Bugzilla::Extension::ComponentWatching::_getWatches($user);
+}
+
+sub add {
+ my ($self, $params) = @_;
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+ my $result;
+
+ # load product and verify access
+ my $productName = $params->{'product'};
+ my $product = Bugzilla::Product->new({ name => $productName, cache => 1 });
+ unless ($product && $user->can_access_product($product)) {
+ ThrowUserError('product_access_denied', { product => $productName });
+ }
+
+ my $ra_componentNames = $params->{'component'};
+ $ra_componentNames = [$ra_componentNames || ''] unless ref($ra_componentNames);
+
+ if (grep { $_ eq '' } @$ra_componentNames) {
+ # watching a product
+ $result = Bugzilla::Extension::ComponentWatching::_addProductWatch($user, $product);
+
+ } else {
+ # watching specific components
+ foreach my $componentName (@$ra_componentNames) {
+ my $component = Bugzilla::Component->new({
+ name => $componentName, product => $product, cache => 1
+ });
+ unless ($component) {
+ ThrowUserError('product_access_denied', { product => $productName });
+ }
+ $result = Bugzilla::Extension::ComponentWatching::_addComponentWatch($user, $component);
+ }
+ }
+
+ Bugzilla::Extension::ComponentWatching::_addDefaultSettings($user);
+
+ return $result;
+}
+
+sub get {
+ my ($self, $params) = @_;
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+ return Bugzilla::Extension::ComponentWatching::_getWatches($user, $params->{'id'});
+}
+
+sub remove {
+ my ($self, $params) = @_;
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+ my %result = (status => Bugzilla::Extension::ComponentWatching::_deleteWatch($user, $params->{'id'}));
+
+ return \%result;
+}
+
+1;
diff --git a/extensions/ComponentWatching/template/en/default/hook/reports/components-component_footer.html.tmpl b/extensions/ComponentWatching/template/en/default/hook/reports/components-component_footer.html.tmpl
new file mode 100644
index 000000000..b8921bcf0
--- /dev/null
+++ b/extensions/ComponentWatching/template/en/default/hook/reports/components-component_footer.html.tmpl
@@ -0,0 +1,10 @@
+[%# 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.
+ #%]
+
+<button disabled type="button" class="minor component-watching" data-product="[% product.name FILTER html %]"
+ data-component="[% comp.name FILTER html %]" data-source="Component Description">Watch</button>
diff --git a/extensions/ComponentWatching/template/en/default/hook/reports/components-product_header.html.tmpl b/extensions/ComponentWatching/template/en/default/hook/reports/components-product_header.html.tmpl
new file mode 100644
index 000000000..bc7120b4e
--- /dev/null
+++ b/extensions/ComponentWatching/template/en/default/hook/reports/components-product_header.html.tmpl
@@ -0,0 +1,10 @@
+[%# 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.
+ #%]
+
+<button disabled type="button" class="minor component-watching" data-product="[% product.name FILTER html %]"
+ data-source="Component Description">Watch</button>
diff --git a/extensions/ComponentWatching/template/en/default/hook/reports/components-start.html.tmpl b/extensions/ComponentWatching/template/en/default/hook/reports/components-start.html.tmpl
new file mode 100644
index 000000000..76cf6bc08
--- /dev/null
+++ b/extensions/ComponentWatching/template/en/default/hook/reports/components-start.html.tmpl
@@ -0,0 +1,12 @@
+[%# 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.
+ #%]
+
+[%
+ javascript_urls.push('extensions/ComponentWatching/web/js/overlay.js');
+ generate_api_token = 1;
+%]
diff --git a/extensions/ComponentWatching/web/js/overlay.js b/extensions/ComponentWatching/web/js/overlay.js
new file mode 100644
index 000000000..c0c540257
--- /dev/null
+++ b/extensions/ComponentWatching/web/js/overlay.js
@@ -0,0 +1,218 @@
+/* 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 || {};
+
+/**
+ * Implement the one-click Component Watching functionality that can be added to any page.
+ * @abstract
+ */
+Bugzilla.ComponentWatching = class ComponentWatching {
+ /**
+ * Initialize a new ComponentWatching instance. Since constructors can't be async, use a separate function to move on.
+ */
+ constructor() {
+ this.buttons = document.querySelectorAll('button.component-watching');
+
+ this.init();
+ }
+
+ /**
+ * Send a REST API request, and return the results in a Promise.
+ * @param {Object} [request={}] Request data. If omitted, all the current watches will be returned.
+ * @param {String} [path=''] Optional path to be appended to the request URL.
+ * @returns {Promise<Object|String>} Response data or error message.
+ */
+ async fetch(request = {}, path = '') {
+ request.url = `/rest/component-watching${path}`;
+
+ return new Promise((resolve, reject) => bugzilla_ajax(request, data => resolve(data), error => reject(error)));
+ }
+
+ /**
+ * Start watching the current product or component.
+ * @param {String} product Product name.
+ * @param {String} [component=''] Component name. If omitted, all the components in the product will be watched.
+ * @returns {Promise<Object|String>} Response data or error message.
+ */
+ async watch(product, component = '') {
+ return this.fetch({ type: 'POST', data: { product, component } });
+ }
+
+ /**
+ * Stop watching the current product or component.
+ * @param {Number} id ID of the watch to be removed.
+ * @returns {Promise<Object|String>} Response data or error message.
+ */
+ async unwatch(id) {
+ return this.fetch({ type: 'DELETE' }, `/${id}`);
+ }
+
+ /**
+ * Log an event with Google Analytics if possible. For privacy reasons, we don't send any specific product or
+ * component name.
+ * @param {String} source Event source that will be part of the event category.
+ * @param {String} action `watch` or `unwatch`.
+ * @param {String} type `product` or `component`.
+ * @param {Number} code `0` for a successful change, `1` otherwise.
+ * @see https://developers.google.com/analytics/devguides/collection/analyticsjs/events
+ */
+ track_event(source, action, type, code) {
+ if ('ga' in window) {
+ ga('send', 'event', `Component Watching: ${source}`, action, type, code);
+ }
+ }
+
+ /**
+ * Show a short floating message if the button is on BugModal. This code is from bug_modal.js, requiring jQuery.
+ * @param {String} message Message text.
+ */
+ show_message(message) {
+ if (!document.querySelector('#floating-message')) {
+ return;
+ }
+
+ $('#floating-message-text').text(message);
+ $('#floating-message').fadeIn(250).delay(2500).fadeOut();
+ }
+
+ /**
+ * Get all the component watching buttons on the current page.
+ * @param {String} [product] Optional product name.
+ * @param {String} [component] Optional component name.
+ * @returns {HTMLButtonElement[]} List of button elements.
+ */
+ get_buttons(product = undefined, component = undefined) {
+ let buttons = [...this.buttons];
+
+ if (product) {
+ buttons = buttons.filter($button => $button.dataset.product === product);
+ }
+
+ if (component) {
+ buttons = buttons.filter($button => $button.dataset.component === component);
+ }
+
+ return buttons;
+ }
+
+ /**
+ * Update a Watch/Unwatch button for a product or component.
+ * @param {HTMLButtonElement} $button Button element to be updated.
+ * @param {Boolean} disabled Whether the button has to be disabled.
+ * @param {Number} [watchId] Optional watch ID if the product or component is being watched.
+ */
+ update_button($button, disabled, watchId = undefined) {
+ const { product, component } = $button.dataset;
+
+ if (watchId) {
+ $button.dataset.watchId = watchId;
+ $button.textContent = $button.getAttribute('data-label-unwatch') || 'Unwatch';
+ $button.title = component ?
+ `Stop watching the ${component} component` :
+ `Stop watching all components in the ${product} product`;
+ } else {
+ delete $button.dataset.watchId;
+
+ $button.textContent = $button.getAttribute('data-label-watch') || 'Watch';
+ $button.title = component ?
+ `Start watching the ${component} component` :
+ `Start watching all components in the ${product} product`;
+ }
+
+ $button.disabled = disabled;
+ }
+
+ /**
+ * Called whenever a Watch/Unwatch button is clicked. Send a request to update the user's watch list, and update the
+ * relevant buttons on the page.
+ * @param {HTMLButtonElement} $button Clicked button element.
+ */
+ async button_onclick($button) {
+ const { product, component, watchId, source } = $button.dataset;
+ let message = '';
+ let code = 0;
+
+ // Disable the button until the request is complete
+ $button.disabled = true;
+
+ try {
+ if (watchId) {
+ await this.unwatch(watchId);
+
+ if (component) {
+ message = `You are no longer watching the ${component} component`;
+
+ this.get_buttons(product, component).forEach($button => this.update_button($button, false));
+ } else {
+ message = `You are no longer watching all components in the ${product} product`;
+
+ this.get_buttons(product).forEach($button => this.update_button($button, false));
+ }
+ } else {
+ const watch = await this.watch(product, component);
+
+ if (component) {
+ message = `You are now watching the ${component} component`;
+
+ this.get_buttons(product, component).forEach($button => this.update_button($button, false, watch.id));
+ } else {
+ message = `You are now watching all components in the ${product} product`;
+
+ this.get_buttons(product).forEach($button => {
+ if ($button.dataset.component) {
+ this.update_button($button, true);
+ } else {
+ this.update_button($button, false, watch.id);
+ }
+ });
+ }
+ }
+ } catch (ex) {
+ message = 'Your watch list could not be updated. Please try again later.';
+ code = 1;
+ }
+
+ this.show_message(message);
+ this.track_event(source, watchId ? 'unwatch' : 'watch', component ? 'component' : 'product', code);
+ }
+
+ /**
+ * Retrieve the current watch list, and initialize all the buttons.
+ */
+ async init() {
+ try {
+ const all_watches = await this.fetch();
+
+ this.get_buttons().forEach($button => {
+ const { product, component } = $button.dataset;
+ const watches = all_watches.filter(watch => watch.product_name === product);
+ const product_watch = watches.find(watch => !watch.component);
+
+ if (!component) {
+ // This button is for product watching
+ this.update_button($button, false, product_watch ? product_watch.id : undefined);
+ } else if (product_watch) {
+ // Disabled the button because all the components in the product is being watched
+ this.update_button($button, true);
+ } else {
+ const watch = watches.find(watch => watch.component_name === component);
+
+ this.update_button($button, false, watch ? watch.id : undefined);
+ }
+
+ $button.addEventListener('click', () => this.button_onclick($button));
+ });
+ } catch (ex) {}
+ }
+};
+
+window.addEventListener('DOMContentLoaded', () => new Bugzilla.ComponentWatching(), { once: true });
diff --git a/js/dropdown.js b/js/dropdown.js
index fd71d0b6e..03345206b 100644
--- a/js/dropdown.js
+++ b/js/dropdown.js
@@ -16,7 +16,7 @@ $(function() {
// clicking dropdown button opens or closes the dropdown content
if (!$(e.target).hasClass('dropdown-button')) {
$('.dropdown-button').each(function() {
- toggleDropDown(e, $(this), $('#' + $(this).attr('aria-controls')), 1);
+ toggleDropDown(e, $(this), $('#' + $(this).attr('aria-controls')), false, true);
});
}
}).keydown(function(e) {
@@ -25,7 +25,7 @@ $(function() {
$('.dropdown-button').each(function() {
var $button = $(this);
if ($button.siblings('.dropdown-content').is(':visible')) {
- toggleDropDown(e, $button, $('#' + $button.attr('aria-controls')), 1);
+ toggleDropDown(e, $button, $('#' + $button.attr('aria-controls')), false, true);
$button.focus();
}
});
@@ -83,7 +83,7 @@ $(function() {
// navigate to an active link or click on it
// note that `trigger('click')` doesn't always work
if (e.keyCode == 13) {
- var $link = $('.dropdown-content:visible a.active');
+ var $link = $('.dropdown-content:visible .active');
if ($link.length) {
if ($link.attr('href')) {
location.href = $link.attr('href');
@@ -105,7 +105,7 @@ $(function() {
var $content = $div.find('.dropdown-content');
$button.click(function(e) {
// Do not handle non-primary click.
- if (e.button != 0) {
+ if (e.button != 0 || $content.hasClass('hover-display')) {
return;
}
toggleDropDown(e, $button, $content);
@@ -115,9 +115,13 @@ $(function() {
// prevent the form being submitted if the search bar is empty
e.preventDefault();
// navigate to an active link if any
- var $link = $content.find('a.active');
+ var $link = $content.find('.active');
if ($link.length) {
- location.href = $link.attr('href');
+ if ($link.attr('href')) {
+ location.href = $link.attr('href');
+ } else {
+ $link.trigger('click');
+ }
}
}
@@ -125,9 +129,49 @@ $(function() {
toggleDropDown(e, $button, $content);
}
});
+
+ if ($content.hasClass('hover-display')) {
+ const $_button = $button.get(0);
+ const $_content = $content.get(0);
+ let timer;
+
+ const button_handler = event => {
+ event.preventDefault();
+ event.stopPropagation();
+ window.clearTimeout(timer);
+
+ if (event.type === 'mouseleave' && $_content.matches('.hovered')) {
+ return;
+ }
+
+ timer = window.setTimeout(() => {
+ toggleDropDown(event, $button, $content, event.type === 'mouseenter', event.type === 'mouseleave');
+ }, 250);
+ };
+
+ const content_handler = event => {
+ event.preventDefault();
+ event.stopPropagation();
+ window.clearTimeout(timer);
+
+ $_content.classList.toggle('hovered', event.type === 'mouseenter');
+
+ if (event.type === 'mouseleave') {
+ timer = window.setTimeout(() => {
+ toggleDropDown(event, $button, $content, false, true);
+ }, 250);
+ }
+ };
+
+ // Use raw `addEventListener` as jQuery actually listens `mouseover` and `mouseout`
+ $_button.addEventListener('mouseenter', event => button_handler(event));
+ $_button.addEventListener('mouseleave', event => button_handler(event));
+ $_content.addEventListener('mouseenter', event => content_handler(event));
+ $_content.addEventListener('mouseleave', event => content_handler(event));
+ }
});
- function toggleDropDown(e, $button, $content, hide_only) {
+ function toggleDropDown(e, $button, $content, show_only, hide_only) {
// hide other expanded dropdown menu if any
var $expanded = $('.dropdown-button[aria-expanded="true"]');
if ($expanded.length && !$expanded.is($button)) {
@@ -148,13 +192,12 @@ $(function() {
$('[aria-controls="' + content_id + '"]').removeAttr('aria-activedescendant');
$content.find('#' + content_id + '-active-item').removeAttr('id');
}
- if ($content.is(':visible')) {
- $content.hide();
- $button.attr('aria-expanded', false);
- }
// if not using Escape or clicking outside the dropdown div, then we are hiding
- else if (!hide_only) {
- $content.show();
+ if ($content.is(':visible') || hide_only) {
+ $content.fadeOut('fast');
+ $button.attr('aria-expanded', false);
+ } else if (!$content.is(':visible') || show_only) {
+ $content.fadeIn('fast');
$button.attr('aria-expanded', true);
}
}
diff --git a/js/global.js b/js/global.js
index d0396d6a8..37567e3de 100644
--- a/js/global.js
+++ b/js/global.js
@@ -155,6 +155,47 @@ function display_value(field, value) {
return value;
}
+// ajax wrapper, to simplify error handling and auth
+// TODO: Rewrite this method using Promise (Bug 1380437)
+function bugzilla_ajax(request, done_fn, error_fn) {
+ $('#xhr-error').hide('');
+ $('#xhr-error').html('');
+ request.url += (request.url.match('\\?') ? '&' : '?') +
+ 'Bugzilla_api_token=' + encodeURIComponent(BUGZILLA.api_token);
+ if (request.type != 'GET') {
+ request.contentType = 'application/json';
+ request.processData = false;
+ if (request.data && request.data.constructor === Object) {
+ request.data = JSON.stringify(request.data);
+ }
+ }
+ return $.ajax(request)
+ .done(function(data) {
+ if (data.error) {
+ if (!request.hideError) {
+ $('#xhr-error').html(data.message);
+ $('#xhr-error').show('fast');
+ }
+ if (error_fn)
+ error_fn(data.message);
+ }
+ else if (done_fn) {
+ done_fn(data);
+ }
+ })
+ .fail(function(data) {
+ if (data.statusText === 'abort')
+ return;
+ var message = data.responseJSON ? data.responseJSON.message : 'Unexpected Error'; // all errors are unexpected :)
+ if (!request.hideError) {
+ $('#xhr-error').html(message);
+ $('#xhr-error').show('fast');
+ }
+ if (error_fn)
+ error_fn(message);
+ });
+}
+
// polyfill .trim
if (!String.prototype.trim) {
(function() {
diff --git a/skins/standard/describecomponents.css b/skins/standard/describecomponents.css
new file mode 100644
index 000000000..cf5c1a98d
--- /dev/null
+++ b/skins/standard/describecomponents.css
@@ -0,0 +1,97 @@
+/* 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. */
+
+.product {
+ margin: 40px auto;
+ max-width: 960px;
+ font-size: 14px;
+ line-height: 1.5;
+}
+
+.product > header,
+.product .instructions {
+ margin: 0 auto;
+ max-width: 800px;
+ text-align: center;
+}
+
+.product h1 {
+ margin: 0;
+ font-size: 48px;
+ font-weight: normal;
+}
+
+.product > header p {
+ font-size: 16px;
+}
+
+.product .instructions p {
+ font-size: 14px;
+ font-style: italic;
+}
+
+.component {
+ display: flex;
+ align-items: center;
+ margin: 8px 0;
+ border: 1px solid #CCC;
+ border-radius: 4px;
+ padding: 16px;
+ background-color: #FFF;
+ box-shadow: 0 0 4px #CCC;
+}
+
+.component.highlight {
+ margin: 0;
+ padding: 1em 0;
+ background-color: lightgreen;
+}
+
+.component header {
+ flex: none;
+ margin-right: 16px;
+ width: 240px;
+}
+
+.component h2 {
+ margin: 0;
+ font-size: 24px;
+ font-weight: normal;
+}
+
+.component div {
+ flex: auto;
+}
+
+.component p {
+ margin: 0;
+ font-size: 16px;
+}
+
+.component ul {
+ display: flex;
+ margin: 8px 0 0;
+ border-top: 1px solid #DDD;
+ padding: 8px 0 0;
+ list-style: none;
+ font-size: 14px;
+ color: #999;
+}
+
+.component li {
+ margin: 0 16px 0 0;
+ padding: 0;
+}
+
+.component footer {
+ flex: none;
+ margin-left: 16px;
+}
+
+.component footer:empty {
+ display: none;
+}
diff --git a/skins/standard/global.css b/skins/standard/global.css
index 48d79366a..d004f3fbe 100644
--- a/skins/standard/global.css
+++ b/skins/standard/global.css
@@ -162,15 +162,13 @@
#header .links a:hover,
#header .links a:focus,
#header-tools-menu-button:hover,
- #header-tools-menu-button:focus,
- #header .dropdown-content a.active {
+ #header-tools-menu-button:focus {
background-color: rgba(0, 0, 0, .05) !important;
}
#header .title a:active,
#header .links a:active,
- #header-tools-menu-button:active,
- #header .dropdown-content a:active {
+ #header-tools-menu-button:active {
background-color: rgba(0, 0, 0, .1) !important;
}
@@ -266,168 +264,6 @@
transition: none;
}
- #header .dropdown-content {
- top: calc(100% + 4px);
- border-color: #BBB #999 #777;
- border-radius: 4px;
- padding: 4px 0;
- min-width: 160px;
- max-width: 240px;
- background-color: #FCFCFC;
- box-shadow: 0 2px 8px rgba(0,0,0,.3);
- }
-
- #header .dropdown-content.right {
- left: -4px;
- }
-
- #header .dropdown-content.left {
- right: -4px;
- }
-
- #header .dropdown-content::before,
- #header .dropdown-content::after {
- content: '';
- display: block;
- width: 0;
- height: 0;
- position: absolute;
- border-width: 8px;
- border-color: transparent;
- border-style: solid;
- }
-
- #header .dropdown-content.right::before,
- #header .dropdown-content.right::after {
- left: 11px;
- }
-
- #header .dropdown-content.left::before,
- #header .dropdown-content.left::after {
- right: 11px;
- }
-
- #header .dropdown-content::before {
- top: -17px;
- border-bottom-color: #BBB;
- }
-
- #header .dropdown-content::after {
- top: -16px;
- border-bottom-color: #FFF;
- }
-
- #header .dropdown-content a,
- #header .dropdown-content li > div {
- padding: 2px 16px;
- line-height: 1.5;
- white-space: normal;
- color: inherit !important;
- 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 a {
- overflow: hidden;
- }
-
- #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 .notifications .secure .icon {
- display: inline;
- font-size: 16px;
- vertical-align: text-bottom;
- }
-
- #header .dropdown-panel .notifications .secure .icon::before {
- content: '\E88D';
- }
-
- #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;
@@ -533,12 +369,6 @@
color: #666;
}
- #header .dropdown-separator {
- height: 0;
- margin: 4px 0;
- border-color: #BBB;
- }
-
#header-login .mini-popup {
position: absolute;
top: 48px;
@@ -1715,7 +1545,31 @@ button[disabled], input[type=submit][disabled], input[type=button][disabled], bu
background-image: -webkit-linear-gradient(#bfc7cd,#9ca3aa);
background-image: linear-gradient(#bfc7cd,#9ca3aa);
box-shadow: 0 1px 0 0 rgba(0,0,0,0.2),inset 0 -1px 0 0 rgba(0,0,0,0.3);
- cursor: pointer;
+ pointer-events: none;
+}
+
+button.minor {
+ background-color: #eee;
+ background-image: linear-gradient(#fcfcfc, #eee);
+ color: #000;
+ font-size: inherit;
+ font-weight: 500;
+ padding: 4px 8px;
+ margin-bottom: 1px;
+ text-shadow: none;
+ -web-kit-box-shadow: 0 1px 0 0 rgba(0,0,0,0.1), inset 0 -1px 0 0 rgba(0,0,0,0.1);
+ -moz-box-shadow: 0 1px 0 0 rgba(0,0,0,0.1), inset 0 -1px 0 0 rgba(0,0,0,0.1);
+ 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 {
+ -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;
+}
+
+button.minor[disabled] {
+ color: #999;
}
.notransition {
@@ -1861,50 +1715,253 @@ a.controller {
/******************/
/* Dropdown Menus */
/******************/
-
/* The container <div> - needed to position the dropdown content */
.dropdown {
- position: relative;
- display: inline-block;
+ position: relative;
+ display: inline-block;
+}
+
+.dropdown-button {
+ cursor: pointer;
+}
+
+.dropdown-button * {
+ pointer-events: none;
}
/* Dropdown Content (Hidden by Default) */
.dropdown-content {
- position: absolute;
- background-color: #eee;
- min-width: 120px;
- z-index: 1;
- text-align: left;
- margin: 0;
- padding: 0;
- border: 1px solid #ddd;
- -webkit-box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
- -moz-box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
- box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
- list-style: none;
- right: 0px;
+ position: absolute;
+ top: calc(100% + 4px);
+ right: 0;
+ z-index: 1;
+ margin: 0;
+ border-width: 1px;
+ border-style: solid;
+ border-color: #BBB #999 #777;
+ border-radius: 4px;
+ padding: 4px 0;
+ min-width: 160px;
+ max-width: 400px;
+ background-color: #FCFCFC;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, .3);
+ text-align: left;
}
.dropdown-content.menu-up {
- bottom: 100%;
+ top: auto;
+ bottom: calc(100% + 4px);
}
-.dropdown-separator {
- border-bottom: 1px solid #ddd;
+.dropdown-content.right {
+ left: -4px;
}
-/* Links inside the dropdown */
-.dropdown-content a {
- white-space: nowrap;
- background-color: #eee;
- color: black !important;
- padding: 4px 8px;
- text-decoration: none !important;
- display: block;
+.dropdown-content.left {
+ right: -4px;
}
-/* Change color of dropdown links on hover */
-.dropdown-content li .active {
- text-decoration: none;
- background-color: #39f;
+.dropdown-content::before,
+.dropdown-content::after {
+ content: '';
+ display: block;
+ width: 0;
+ height: 0;
+ position: absolute;
+ border-width: 8px;
+ border-color: transparent;
+ border-style: solid;
+}
+
+.dropdown-content.right::before,
+.dropdown-content.right::after {
+ left: 11px;
+}
+
+.dropdown-content.left::before,
+.dropdown-content.left::after {
+ right: 11px;
+}
+
+.dropdown-content:not(.menu-up)::before {
+ top: -17px;
+ border-bottom-color: #BBB;
+}
+
+.dropdown-content:not(.menu-up)::after {
+ top: -16px;
+ border-bottom-color: #FFF;
+}
+
+.dropdown-content.menu-up::before {
+ bottom: -17px;
+ border-top-color: #BBB;
+}
+
+.dropdown-content.menu-up::after {
+ bottom: -16px;
+ border-top-color: #FFF;
+}
+
+.dropdown-content ul,
+.dropdown-content li {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.dropdown-content [role="menuitem"],
+.dropdown-content [role="option"],
+.dropdown-content li > div {
+ display: block;
+ box-sizing: border-box;
+ padding: 2px 16px;
+ width: 100%;
+ color: #555;
+ line-height: 1.5;
+ white-space: nowrap;
+ background: none transparent;
+}
+
+.dropdown-content [role="menuitem"],
+.dropdown-content [role="option"] {
+ outline: 0;
+ text-decoration: none;
+ cursor: pointer;
+}
+
+.dropdown-content [role="menuitem"]:hover,
+.dropdown-content [role="menuitem"]:focus,
+.dropdown-content [role="menuitem"]:active,
+.dropdown-content [role="menuitem"].active,
+.dropdown-content [role="option"]:hover,
+.dropdown-content [role="option"]:focus,
+.dropdown-content [role="option"]:active,
+.dropdown-content [role="option"].active {
+ color: #333;
+ background-color: rgba(0, 0, 0, .1) !important;
+}
+
+.dropdown-content button[role="menuitem"] {
+ -moz-appearance: none;
+ -webkit-appearance: none;
+ appearance: none;
+ outline: 0;
+ border: 0;
+ border-radius: 0;
+ box-shadow: none;
+ font-weight: normal;
+ text-align: left;
+}
+
+.dropdown-content button[role="menuitem"]::-moz-focus-inner {
+ border: 0;
+}
+
+.dropdown-content [role="separator"] {
+ height: 0;
+ margin: 4px 0 !important;
+ border-bottom: 1px solid #BBB;
+}
+
+.dropdown-panel {
+ padding: 0 !important;
+ width: 400px;
+ max-width: none !important;
+}
+
+.dropdown-panel header {
+ border-bottom: 1px solid #CCC;
+}
+
+.dropdown-panel h2 {
+ margin: 0;
+ padding: 8px 12px;
+ font-size: 14px;
+ line-height: 100%;
+ font-weight: normal;
+}
+
+.dropdown-panel ul {
+ overflow-y: auto;
+ margin: 0;
+ padding: 0;
+ max-height: 480px;
+ list-style-type: none;
+}
+
+.dropdown-panel li:not(:last-child) {
+ border-bottom: 1px solid #CCC;
+}
+
+.dropdown-panel li a {
+ padding: 12px !important;
+}
+
+.dropdown-panel li a:hover {
+ background-color: rgba(0, 0, 0, .05) !important;
+}
+
+.dropdown-panel li a * {
+ pointer-events: none;
+}
+
+.dropdown-panel .notifications a {
+ overflow: hidden;
+}
+
+.dropdown-panel .notifications img {
+ float: left;
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+}
+
+.dropdown-panel .notifications img ~ * {
+ display: block;
+ margin-left: 52px;
+}
+
+.dropdown-panel .notifications label {
+ overflow: hidden;
+ max-height: 40px;
+}
+
+.dropdown-panel .notifications strong {
+ font-weight: 600;
+}
+
+.dropdown-panel .notifications time {
+ font-size: 12px;
+ color: #999;
+}
+
+.dropdown-panel .notifications .secure .icon {
+ display: inline;
+ font-size: 16px;
+ vertical-align: text-bottom;
+}
+
+.dropdown-panel .notifications .secure .icon::before {
+ content: '\E88D';
+}
+
+.dropdown-panel .loading,
+.dropdown-panel .empty {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 240px;
+ line-height: 150%;
+ text-align: center;
+}
+
+.dropdown-panel footer {
+ border-top: 1px solid #CCC;
+ text-align: center;
+}
+
+.dropdown-panel footer a {
+ padding: 8px 16px !important;
+ line-height: 100% !important;
}
diff --git a/skins/standard/reports.css b/skins/standard/reports.css
deleted file mode 100644
index 205946550..000000000
--- a/skins/standard/reports.css
+++ /dev/null
@@ -1,97 +0,0 @@
-/* The contents of this file are subject to the Mozilla Public
- * License Version 1.1 (the "License"); you may not use this file
- * except in compliance with the License. You may obtain a copy of
- * the License at http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS
- * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
- * implied. See the License for the specific language governing
- * rights and limitations under the License.
- *
- * The Original Code is the Bugzilla Bug Tracking System.
- *
- * The Initial Developer of the Original Code is Everything Solved,
- * Inc. Portions created by the Initial Developer are Copyright (C)
- * 2009 the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- * Max Kanat-Alexander <mkanat@bugzilla.org>
- */
-
-/* describecomponents.cgi */
-
-#components_header_table {
- margin-bottom: 1em;
-}
-
-.product_container {
- width: 65%;
-}
-
-.product_name {
- font-weight: bold;
- font-size: 150%;
- margin: 0;
-}
-
-.product_desc {
- /* This is padding instead of margin because it looks better
- * with the scrollbar. */
- padding: 0 2em;
- font-style: italic;
- max-height: 5em;
- overflow: auto;
-}
-
-.instructions {
- font-weight: bold;
- font-size: 105%;
- padding-right: 1em;
-}
-
-.components_header {
- margin: 0;
- font-size: 140%;
- font-weight: bold;
-}
-
-.component_table {
- margin-top: -1em;
- margin-left: 2em;
-}
-
-.component_table thead th {
- padding-right: 1em;
- vertical-align: bottom;
- text-align: left;
-}
-
-.component_table td {
- border-bottom: 1px dotted gray;
-}
-
-.component_table td.component_assignee,
-.component_table td.component_qa_contact
-{
- border: none;
- padding-top: .5em;
-}
-
-.component_name {
- font-size: 115%;
- font-weight: bold;
- padding-right: 1em;
- vertical-align: middle;
- min-width: 8em;
-}
-
-.component_description {
- padding-bottom: .5em;
- color: #333;
-}
-
-.component_hilite {
- background-color: lightgreen;
- margin: 0;
- padding: 1em 0;
-}
diff --git a/template/en/default/global/header.html.tmpl b/template/en/default/global/header.html.tmpl
index 153137394..a87b2015e 100644
--- a/template/en/default/global/header.html.tmpl
+++ b/template/en/default/global/header.html.tmpl
@@ -305,7 +305,7 @@
</li>
[% END %]
[% IF Param('docs_urlbase') %]
- <li role="separator" class="dropdown-separator"></li>
+ <li role="separator"></li>
<li role="presentation">
<a href="[% docs_urlbase FILTER html %]" role="menuitem" tabindex="-1">Documentation</a>
</li>
@@ -331,7 +331,7 @@
<div class="email">[% user.login FILTER html %]</div>
</div>
</li>
- <li role="separator" class="dropdown-separator"></li>
+ <li role="separator"></li>
<li role="presentation">
<a href="user_profile" role="menuitem" tabindex="-1">My Profile</a>
</li>
@@ -343,7 +343,7 @@
<a href="userprefs.cgi" role="menuitem" tabindex="-1">Preferences</a>
</li>
[% IF user.authorizer.can_logout %]
- <li role="separator" class="dropdown-separator"></li>
+ <li role="separator"></li>
<li role="presentation">
<a href="index.cgi?logout=1" role="menuitem" tabindex="-1">Log out</a>
</li>
diff --git a/template/en/default/reports/components.html.tmpl b/template/en/default/reports/components.html.tmpl
index b2a21ccc1..f8b0f3f80 100644
--- a/template/en/default/reports/components.html.tmpl
+++ b/template/en/default/reports/components.html.tmpl
@@ -17,6 +17,7 @@
#
# Contributor(s): Bradley Baetz <bbaetz@student.usyd.edu.au>
# Max Kanat-Alexander <mkanat@bugzilla.org>
+ # Kohei Yoshino <kohei.yoshino@gmail.com>
#%]
[%# INTERFACE:
@@ -29,53 +30,36 @@
Components for [% product.name FILTER html %]
[% END %]
-[% PROCESS global/header.html.tmpl
- style_urls = [ "skins/standard/reports.css" ]
- title = title
+[% DEFAULT
+ style_urls = [ "skins/standard/describecomponents.css" ]
+ javascript_urls = []
+ title = title
+ show_default_people = 1
%]
-[% IF Param("useqacontact") %]
- [% numcols = 3 %]
-[% ELSE %]
- [% numcols = 2 %]
-[% END %]
-
-<h2>[% mark FILTER html %]</h2>
-
-<table cellpadding="0" cellspacing="0" id="components_header_table">
- <tr>
- <td class="instructions">
- Select a component to see open [% terms.bugs %] in that component:
- </td>
- <td class="product_container">
- <span class="product_name">[% product.name FILTER html %]</span>
- <div class="product_desc">
- [% product.description FILTER html_light %]
- </div>
- </td>
- </tr>
-</table>
+[% Hook.process('start') %]
-<span class="components_header">Components</span>
+[% PROCESS global/header.html.tmpl
+ style_urls = style_urls
+ javascript_urls = javascript_urls
+ title = title
+%]
-<table summary="Components table"
- class="component_table" cellspacing="0" cellpadding="0">
- <thead>
- <tr>
- <th>&nbsp;</th>
- <th>Default Assignee</th>
- [% IF Param("useqacontact") %]
- <th>Default QA Contact</th>
+<section class="product">
+ <header>
+ <h1>[% product.name FILTER html %]</h1>
+ <p>[% product.description FILTER html_light %]</p>
+ [% Hook.process('product_header') %]
+ </header>
+ <div class="instructions">
+ <p>Select a component to see open [% terms.bugs %] in that component:</p>
+ </div>
+ <div class="list">
+ [% FOREACH comp = product.components %]
+ [% INCLUDE describe_comp %]
[% END %]
- </tr>
- </thead>
-
- <tbody>
- [% FOREACH comp = product.components %]
- [% INCLUDE describe_comp %]
- [% END %]
- </tbody>
-</table>
+ </div>
+</section>
[% PROCESS global/footer.html.tmpl %]
@@ -84,27 +68,24 @@
[%############################################################################%]
[% BLOCK describe_comp %]
- <tr id="[% comp.name FILTER html %]"
- [%- IF comp.name == component_mark %] class="component_hilite"[% END %]>
- <td rowspan="2" class="component_name">
- <a name="[% comp.name FILTER html %]"
- href="buglist.cgi?product=
- [%- product.name FILTER uri %]&amp;component=
- [%- comp.name FILTER uri %]&amp;resolution=---">
- [% comp.name FILTER html %]</a>
- </td>
- <td class="component_assignee">
- [% INCLUDE global/user.html.tmpl who = comp.default_assignee %]
- </td>
- [% IF Param("useqacontact") %]
- <td class="component_qa_contact">
- [% INCLUDE global/user.html.tmpl who = comp.default_qa_contact %]
- </td>
- [% END %]
- </tr>
- <tr[% IF comp.name == component_mark %] class="component_hilite"[% END %]>
- <td colspan="[% numcols - 1 %]" class="component_description">
- [% comp.description FILTER html_light %]
- </td>
- </tr>
+ <section id="[% comp.name FILTER html %]" class="component[%- IF comp.name == component_mark %] highlight[% END %]">
+ <header>
+ <h2><a href="buglist.cgi?product=[%- product.name FILTER uri %]&amp;component=
+ [%- comp.name FILTER uri %]&amp;resolution=---">[% comp.name FILTER html %]</a></h2>
+ </header>
+ <div>
+ <p class="description">[% comp.description FILTER html_light %]</p>
+ [% IF show_default_people %]
+ <ul>
+ <li>Assignee: [% INCLUDE global/user.html.tmpl who = comp.default_assignee %]</li>
+ [% IF Param("useqacontact") %]
+ <li>QA: [% INCLUDE global/user.html.tmpl who = comp.default_qa_contact %]</li>
+ [% END %]
+ </ul>
+ [% END %]
+ </div>
+ <footer>
+ [% Hook.process('component_footer', 'reports/components.html.tmpl') %]
+ </footer>
+ </section>
[% END %]
diff --git a/template/en/default/reports/menu.html.tmpl b/template/en/default/reports/menu.html.tmpl
index 5e19b1209..f5e9c4664 100644
--- a/template/en/default/reports/menu.html.tmpl
+++ b/template/en/default/reports/menu.html.tmpl
@@ -28,7 +28,6 @@
[% PROCESS global/header.html.tmpl
title = "Reporting and Charting Kitchen"
doc_section = "reporting.html"
- style_urls = ['skins/standard/reports.css']
%]
<p>
@@ -70,14 +69,14 @@
<ul>
[% IF feature_enabled('old_charts') %]
<li id="old_charts">
- <strong><a href="reports.cgi">Old Charts</a></strong> -
+ <strong><a href="reports.cgi">Old Charts</a></strong> -
plot the status and/or resolution of [% terms.bugs %] against
time, for each product in your database.
</li>
[% END %]
[% IF feature_enabled('new_charts') AND user.in_group(Param("chartgroup")) %]
<li id="new_charts">
- <strong><a href="chart.cgi">New Charts</a></strong> -
+ <strong><a href="chart.cgi">New Charts</a></strong> -
plot any arbitrary search against time. Far more powerful.
</li>
[% END %]