summaryrefslogtreecommitdiffstats
path: root/extensions
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 /extensions
parent351b399991094e57e890a9291484c4ab69ca628b (diff)
downloadbugzilla-535a1fd09e4b150e31165e2e79af42c2e5f2bda5.tar.gz
bugzilla-535a1fd09e4b150e31165e2e79af42c2e5f2bda5.tar.xz
Bug 1472954 - Implement one-click component watching on bug modal and component description pages
Diffstat (limited to 'extensions')
-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
14 files changed, 488 insertions, 212 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 });