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