]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 1472954 - Implement one-click component watching on bug modal and component descr...
authorKohei Yoshino <kohei.yoshino@gmail.com>
Thu, 19 Jul 2018 02:44:16 +0000 (22:44 -0400)
committerDylan William Hardison <dylan@hardison.net>
Thu, 19 Jul 2018 02:44:16 +0000 (22:44 -0400)
22 files changed:
extensions/BMO/template/en/default/hook/global/header-external-links.html.tmpl
extensions/BMO/template/en/default/hook/reports/components-start.html.tmpl [new file with mode: 0644]
extensions/BMO/template/en/default/reports/components.html.tmpl [deleted file]
extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl
extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl
extensions/BugModal/template/en/default/bug_modal/header.html.tmpl
extensions/BugModal/web/bug_modal.css
extensions/BugModal/web/bug_modal.js
extensions/ComponentWatching/Extension.pm
extensions/ComponentWatching/lib/WebService.pm [new file with mode: 0644]
extensions/ComponentWatching/template/en/default/hook/reports/components-component_footer.html.tmpl [new file with mode: 0644]
extensions/ComponentWatching/template/en/default/hook/reports/components-product_header.html.tmpl [new file with mode: 0644]
extensions/ComponentWatching/template/en/default/hook/reports/components-start.html.tmpl [new file with mode: 0644]
extensions/ComponentWatching/web/js/overlay.js [new file with mode: 0644]
js/dropdown.js
js/global.js
skins/standard/describecomponents.css [new file with mode: 0644]
skins/standard/global.css
skins/standard/reports.css [deleted file]
template/en/default/global/header.html.tmpl
template/en/default/reports/components.html.tmpl
template/en/default/reports/menu.html.tmpl

index 54a2f0e4933ec53c135b5bcc808cf48e8499e172..f79548e3d13eb8ef9a2d2b4246e634e628388406 100644 (file)
@@ -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 (file)
index 0000000..a4234ca
--- /dev/null
@@ -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 (file)
index 3e23d38..0000000
+++ /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 %]
index af2077e00b9b698a117edc2553632605aac5ee96..36494773bf699f68db80ae164d07d531300ce47b 100644 (file)
@@ -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>
   <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>
index e2c8bba261a1203b23536c9b6362a747d67b5fa8..60ed6ca49800fe82a27b2e243a5cec0497e6b247 100644 (file)
       <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>
         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
         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 %]
     <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>
       <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>
             <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>
index 47d97fb32a35d183193680cd352e4d303712d69e..20561c7602bce37166e02b5bed503b9da626ebaa 100644 (file)
@@ -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",
index a8c469ad639f1e8f54c8430053af3e1037266387..ee50c6b776baf2bc13c792edc3d66da4f13c664d 100644 (file)
@@ -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 {
index c4bff9412f969e20913df9bbeb3ece8729daef6b..a4ae83d72cd01a5077016abaf0385d10baf93ce4 100644 (file)
@@ -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) {
index 01d843c7a55db6b16652216bbdeb26cc178498a4..674e0da7bce3c4198d717b2ac8cffb87d2c6d98e 100644 (file)
@@ -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 (file)
index 0000000..ba4cb02
--- /dev/null
@@ -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 (file)
index 0000000..b8921bc
--- /dev/null
@@ -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 (file)
index 0000000..bc7120b
--- /dev/null
@@ -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 (file)
index 0000000..76cf6bc
--- /dev/null
@@ -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 (file)
index 0000000..c0c5402
--- /dev/null
@@ -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 });
index fd71d0b6e0d3e8e19c5324615b8237e6400842d1..03345206b4b807eb59f51bf2307e573f4d15462e 100644 (file)
@@ -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);
         }
     }
index d0396d6a8552a95725aa86c7b897cd291e69dce1..37567e3deae4c1e8bc54ff37b19dbd8caf0ef412 100644 (file)
@@ -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 (file)
index 0000000..cf5c1a9
--- /dev/null
@@ -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;
+}
index 48d79366a8f8f7336b519022999837461539426b..d004f3fbe328933764c3117b25b1bc9c2766831b 100644 (file)
     #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;
     }
 
         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;
         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 (file)
index 2059465..0000000
+++ /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;
-}
index 153137394b8657efda7a9ab32102537ab308c137..a87b2015e1f217f765a1bccab1f8de20d32399b4 100644 (file)
             </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>
               <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>
             <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>
index b2a21ccc15c53f4b7f86caa6721ebbe66947404f..f8b0f3f8057880dbbfc1ab3c30429bf6be1e61f5 100644 (file)
@@ -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:
   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 %]
 
 [%############################################################################%]
 
 [% 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 %]
index 5e19b120906dd7e1dbf9d2f265e84825c30b52f0..f5e9c4664e1595e90fa80b43c20326d8fca18d72 100644 (file)
@@ -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>
   <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 %]