summaryrefslogtreecommitdiffstats
path: root/extensions
diff options
context:
space:
mode:
authorDylan William Hardison <dylan@hardison.net>2018-08-04 18:24:15 +0200
committerDylan William Hardison <dylan@hardison.net>2018-08-04 18:24:15 +0200
commitf44392e8cdbea85ac308b2472f813ee605ebae4b (patch)
tree6e7adaf99a0e5a43eb1bf5a0d673d86b60f34f99 /extensions
parent5be3a7fd0061aa0bc3059e09079741873b9b833f (diff)
parent4528b21bc922f8b1e0ba8581d230a492aa43c9cf (diff)
downloadbugzilla-f44392e8cdbea85ac308b2472f813ee605ebae4b.tar.gz
bugzilla-f44392e8cdbea85ac308b2472f813ee605ebae4b.tar.xz
Merge branch 'mojo-poc'
Diffstat (limited to 'extensions')
-rw-r--r--extensions/BMO/Extension.pm92
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-swag.html.tmpl159
-rw-r--r--extensions/BMO/template/en/default/hook/global/header-external-links.html.tmpl2
-rw-r--r--extensions/BMO/template/en/default/hook/reports/components-start.html.tmpl10
-rw-r--r--extensions/BMO/template/en/default/pages/group_members.html.tmpl2
-rw-r--r--extensions/BMO/template/en/default/reports/components.html.tmpl99
-rw-r--r--extensions/BMO/web/js/edituser_menu.js2
-rw-r--r--extensions/BugModal/lib/MonkeyPatches.pm25
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl20
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl94
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/header.html.tmpl4
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/user.html.tmpl6
-rw-r--r--extensions/BugModal/web/bug_modal.css52
-rw-r--r--extensions/BugModal/web/bug_modal.js80
-rw-r--r--extensions/BugmailFilter/template/en/default/account/prefs/bugmail_filter.html.tmpl2
-rw-r--r--extensions/ComponentWatching/Extension.pm33
-rw-r--r--extensions/ComponentWatching/lib/WebService.pm113
-rw-r--r--extensions/ComponentWatching/template/en/default/hook/reports/components-component_footer.html.tmpl10
-rw-r--r--extensions/ComponentWatching/template/en/default/hook/reports/components-product_header.html.tmpl10
-rw-r--r--extensions/ComponentWatching/template/en/default/hook/reports/components-start.html.tmpl12
-rw-r--r--extensions/ComponentWatching/web/js/overlay.js218
-rw-r--r--extensions/GoogleAnalytics/web/js/analytics.js1
-rw-r--r--extensions/Gravatar/template/en/default/hook/bug/comments-user-image.html.tmpl6
-rw-r--r--extensions/PhabBugz/lib/Feed.pm36
-rw-r--r--extensions/PhabBugz/lib/Project.pm8
-rw-r--r--extensions/PhabBugz/lib/Revision.pm2
-rw-r--r--extensions/PhabBugz/lib/User.pm16
-rw-r--r--extensions/PhabBugz/lib/Util.pm3
-rw-r--r--extensions/PhabBugz/lib/WebService.pm71
-rw-r--r--extensions/Review/Extension.pm49
-rw-r--r--extensions/UserProfile/template/en/default/hook/account/prefs/account-start.html.tmpl2
31 files changed, 834 insertions, 405 deletions
diff --git a/extensions/BMO/Extension.pm b/extensions/BMO/Extension.pm
index d2e62eccd..743d03099 100644
--- a/extensions/BMO/Extension.pm
+++ b/extensions/BMO/Extension.pm
@@ -2742,4 +2742,96 @@ sub enter_bug_entrydefaultvars {
}
}
+sub app_startup {
+ my ($self, $args) = @_;
+ my $app = $args->{app};
+ my $r = $app->routes;
+
+ $r->any( '/:REWRITE_itrequest' => [ REWRITE_itrequest => qr{form[\.:]itrequest} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Infrastructure & Operations', 'format' => 'itrequest' } );
+ $r->any( '/:REWRITE_mozlist' => [ REWRITE_mozlist => qr{form[\.:]mozlist} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'product' => 'mozilla.org', 'format' => 'mozlist' } );
+ $r->any( '/:REWRITE_poweredby' => [ REWRITE_poweredby => qr{form[\.:]poweredby} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'product' => 'mozilla.org', 'format' => 'poweredby' } );
+ $r->any( '/:REWRITE_presentation' => [ REWRITE_presentation => qr{form[\.:]presentation} ] )
+ ->to( 'cgi#enter_bug_cgi' => { 'product' => 'mozilla.org', 'format' => 'presentation' } );
+ $r->any( '/:REWRITE_trademark' => [ REWRITE_trademark => qr{form[\.:]trademark} ] )
+ ->to( 'cgi#enter_bug_cgi' => { 'product' => 'mozilla.org', 'format' => 'trademark' } );
+ $r->any( '/:REWRITE_recoverykey' => [ REWRITE_recoverykey => qr{form[\.:]recoverykey} ] )
+ ->to( 'cgi#enter_bug_cgi' => { 'product' => 'mozilla.org', 'format' => 'recoverykey' } );
+ $r->any( '/:REWRITE_legal' => [ REWRITE_legal => qr{form[\.:]legal} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Legal', 'format' => 'legal' }, );
+ $r->any( '/:REWRITE_recruiting' => [ REWRITE_recruiting => qr{form[\.:]recruiting} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Recruiting', 'format' => 'recruiting' } );
+ $r->any( '/:REWRITE_intern' => [ REWRITE_intern => qr{form[\.:]intern} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Recruiting', 'format' => 'intern' } );
+ $r->any( '/:REWRITE_mozpr' => [ REWRITE_mozpr => qr{form[\.:]mozpr} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Mozilla PR', 'format' => 'mozpr' }, );
+ $r->any( '/:REWRITE_reps_mentorship' => [ REWRITE_reps_mentorship => qr{form[\.:]reps[\.:]mentorship} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Mozilla Reps', 'format' => 'mozreps' }, );
+ $r->any( '/:REWRITE_reps_budget' => [ REWRITE_reps_budget => qr{form[\.:]reps[\.:]budget} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Mozilla Reps', 'format' => 'remo-budget' } );
+ $r->any( '/:REWRITE_reps_swag' => [ REWRITE_reps_swag => qr{form[\.:]reps[\.:]swag} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Mozilla Reps', 'format' => 'remo-swag' } );
+ $r->any( '/:REWRITE_reps_it' => [ REWRITE_reps_it => qr{form[\.:]reps[\.:]it} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Mozilla Reps', 'format' => 'remo-it' } );
+ $r->any( '/:REWRITE_reps_payment' => [ REWRITE_reps_payment => qr{form[\.:]reps[\.:]payment} ] )
+ ->to( 'CGI#page_cgi' => { 'id' => 'remo-form-payment.html' } );
+ $r->any( '/:REWRITE_csa_discourse' => [ REWRITE_csa_discourse => qr{form[\.:]csa[\.:]discourse} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Infrastructure & Operations', 'format' => 'csa-discourse' } );
+ $r->any( '/:REWRITE_employee_incident' => [ REWRITE_employee_incident => qr{form[\.:]employee[\.\-:]incident} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'product' => 'mozilla.org', 'format' => 'employee-incident' } );
+ $r->any( '/:REWRITE_brownbag' => [ REWRITE_brownbag => qr{form[\.:]brownbag} ] )
+ ->to( 'CGI#https_air_mozilla_org_requests' => {} );
+ $r->any( '/:REWRITE_finance' => [ REWRITE_finance => qr{form[\.:]finance} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Finance', 'format' => 'finance' } );
+ $r->any(
+ '/:REWRITE_moz_project_review' => [ REWRITE_moz_project_review => qr{form[\.:]moz[\.\-:]project[\.\-:]review} ]
+ )->to( 'CGI#enter_bug_cgi' => { 'product' => 'mozilla.org', 'format' => 'moz-project-review' } );
+ $r->any( '/:REWRITE_docs' => [ REWRITE_docs => qr{form[\.:]docs?} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Developer Documentation', 'format' => 'doc' } );
+ $r->any( '/:REWRITE_mdn' => [ REWRITE_mdn => qr{form[\.:]mdn?} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'format' => 'mdn', 'product' => 'developer.mozilla.org' } );
+ $r->any( '/:REWRITE_swag_gear' => [ REWRITE_swag_gear => qr{form[\.:](swag|gear)} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'format' => 'swag', 'product' => 'Marketing' } );
+ $r->any( '/:REWRITE_costume' => [ REWRITE_costume => qr{form[\.:]costume} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Marketing', 'format' => 'costume' } );
+ $r->any( '/:REWRITE_ipp' => [ REWRITE_ipp => qr{form[\.:]ipp} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Internet Public Policy', 'format' => 'ipp' } );
+ $r->any( '/:REWRITE_creative' => [ REWRITE_creative => qr{form[\.:]creative} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'format' => 'creative', 'product' => 'Marketing' } );
+ $r->any( '/:REWRITE_user_engagement' => [ REWRITE_user_engagement => qr{form[\.:]user[\.\-:]engagement} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'format' => 'user-engagement', 'product' => 'Marketing' } );
+ $r->any( '/:REWRITE_dev_engagement_event' =>
+ [ REWRITE_dev_engagement_event => qr{form[\.:]dev[\.\-:]engagement[\.\-\:]event} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Developer Engagement', 'format' => 'dev-engagement-event' } );
+ $r->any( '/:REWRITE_mobile_compat' => [ REWRITE_mobile_compat => qr{form[\.:]mobile[\.\-:]compat} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Tech Evangelism', 'format' => 'mobile-compat' } );
+ $r->any( '/:REWRITE_web_bounty' => [ REWRITE_web_bounty => qr{form[\.:]web[\.:]bounty} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'format' => 'web-bounty', 'product' => 'mozilla.org' } );
+ $r->any( '/:REWRITE_automative' => [ REWRITE_automative => qr{form[\.:]automative} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Testing', 'format' => 'automative' } );
+ $r->any( '/:REWRITE_comm_newsletter' => [ REWRITE_comm_newsletter => qr{form[\.:]comm[\.:]newsletter} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'format' => 'comm-newsletter', 'product' => 'Marketing' } );
+ $r->any( '/:REWRITE_screen_share_whitelist' =>
+ [ REWRITE_screen_share_whitelist => qr{form[\.:]screen[\.:]share[\.:]whitelist} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'format' => 'screen-share-whitelist', 'product' => 'Firefox' } );
+ $r->any( '/:REWRITE_data_compliance' => [ REWRITE_data_compliance => qr{form[\.:]data[\.\-:]compliance} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Data Compliance', 'format' => 'data-compliance' } );
+ $r->any( '/:REWRITE_fsa_budget' => [ REWRITE_fsa_budget => qr{form[\.:]fsa[\.:]budget} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'product' => 'FSA', 'format' => 'fsa-budget' } );
+ $r->any( '/:REWRITE_triage_request' => [ REWRITE_triage_request => qr{form[\.:]triage[\.\-]request} ] )
+ ->to( 'CGI#page_cgi' => { 'id' => 'triage_request.html' } );
+ $r->any( '/:REWRITE_crm_CRM' => [ REWRITE_crm_CRM => qr{form[\.:](crm|CRM)} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'format' => 'crm', 'product' => 'Marketing' } );
+ $r->any( '/:REWRITE_nda' => [ REWRITE_nda => qr{form[\.:]nda} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Legal', 'format' => 'nda' } );
+ $r->any( '/:REWRITE_name_clearance' => [ REWRITE_name_clearance => qr{form[\.:]name[\.:]clearance} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'format' => 'name-clearance', 'product' => 'Legal' } );
+ $r->any( '/:REWRITE_shield_studies' => [ REWRITE_shield_studies => qr{form[\.:]shield[\.:]studies} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Shield', 'format' => 'shield-studies' } );
+ $r->any( '/:REWRITE_client_bounty' => [ REWRITE_client_bounty => qr{form[\.:]client[\.:]bounty} ] )
+ ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Firefox', 'format' => 'client-bounty' } );
+}
+
__PACKAGE__->NAME;
diff --git a/extensions/BMO/template/en/default/bug/create/create-swag.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-swag.html.tmpl
index cff5e5796..05ec4e2d9 100644
--- a/extensions/BMO/template/en/default/bug/create/create-swag.html.tmpl
+++ b/extensions/BMO/template/en/default/bug/create/create-swag.html.tmpl
@@ -13,106 +13,106 @@
items = [
{ id => '', name => 'Splendidest Gear' },
{ id => '#185687', name => 'Moleskine Notebook (Firefox)' },
- { id => '#155749', name => 'Rickshaw Messenger Bag ' },
- { id => '#155415S', name => 'Champion Hooded Sweatshirt S' },
- { id => '#155415M', name => 'Champion Hooded Sweatshirt M' },
- { id => '#155415L', name => 'Champion Hooded Sweatshirt L' },
- { id => '#155415X', name => 'Champion Hooded Sweatshirt XL' },
- { id => '#1554152', name => 'Champion Hooded Sweatshirt 2XL' },
- { id => '#157452S', name => 'Very Splendid Package Ladies S' },
- { id => '#157452M', name => 'Very Splendid Package Ladies M' },
- { id => '#157452L', name => 'Very Splendid Package Ladies L' },
- { id => '#157452X', name => 'Very Splendid Package Ladies XL' },
- { id => '#1574522', name => 'Very Splendid Package Ladies 2XL' },
- { id => '#157454S', name => 'Very Splendid Package Men\'s S' },
- { id => '#157454M', name => 'Very Splendid Package Men\'s M' },
- { id => '#157454L', name => 'Very Splendid Package Men\'s L' },
- { id => '#157454X', name => 'Very Splendid Package Men\'s XL' },
- { id => '#1574542', name => 'Very Splendid Package Men\'s 2XL' },
- { id => '#1574543', name => 'Very Splendid Package Men\'s 3XL' },
- { id => '#157451S', name => 'Most Splendid Package, S' },
- { id => '#157451M', name => 'Most Splendid Package, M' },
- { id => '#157451L', name => 'Most Splendid Package, L' },
- { id => '#157451X', name => 'Most Splendid Package, XL' },
- { id => '', name => 'Splendider Gear' },
- { id => '#155751', name => 'Drawstring Tote ' },
- { id => '#155340', name => 'Beanie' },
- { id => '#155339', name => 'Black Cap with Tote ' },
- { id => '#212669', name => 'Stoneware Ceramic Mug' },
- { id => '#190928S', name => 'Firefox Tee w/Woven Tag Navy S' },
- { id => '#190928M', name => 'Firefox Tee w/Woven Tag Navy M' },
- { id => '#190928L', name => 'Firefox Tee w/Woven Tag Navy L' },
- { id => '#190928X', name => 'Firefox Tee w/Woven Tag Navy XL' },
- { id => '#1909282', name => 'Firefox Tee w/Woven Tag Navy 2XL' },
- { id => '#1909283', name => 'Firefox Tee w/Woven Tag Navy 3XL' },
- { id => '#190929S', name => 'Firefox SS Lapis Tee Lapis S' },
- { id => '#190929M', name => 'Firefox SS Lapis Tee Lapis M' },
- { id => '#190929L', name => 'Firefox SS Lapis Tee Lapis L' },
- { id => '#190929X', name => 'Firefox SS Lapis Tee Lapis XL' },
- { id => '#155413S', name => 'Ladies\' T-shirt Navy S' },
- { id => '#155413M', name => 'Ladies\' T-shirt Navy M' },
- { id => '#155413L', name => 'Ladies\' T-shirt Navy L' },
- { id => '#155413X', name => 'Ladies\' T-shirt Navy XL' },
- { id => '#1554132', name => 'Ladies\' T-shirt Navy 2XL' },
+# { id => '#155749', name => 'Rickshaw Messenger Bag ' },
+# { id => '#155415S', name => 'Champion Hooded Sweatshirt S' },
+# { id => '#155415M', name => 'Champion Hooded Sweatshirt M' },
+# { id => '#155415L', name => 'Champion Hooded Sweatshirt L' },
+# { id => '#155415X', name => 'Champion Hooded Sweatshirt XL' },
+# { id => '#1554152', name => 'Champion Hooded Sweatshirt 2XL' },
+# { id => '#157452S', name => 'Very Splendid Package Ladies S' },
+# { id => '#157452M', name => 'Very Splendid Package Ladies M' },
+# { id => '#157452L', name => 'Very Splendid Package Ladies L' },
+# { id => '#157452X', name => 'Very Splendid Package Ladies XL' },
+# { id => '#1574522', name => 'Very Splendid Package Ladies 2XL' },
+# { id => '#157454S', name => 'Very Splendid Package Men\'s S' },
+# { id => '#157454M', name => 'Very Splendid Package Men\'s M' },
+# { id => '#157454L', name => 'Very Splendid Package Men\'s L' },
+# { id => '#157454X', name => 'Very Splendid Package Men\'s XL' },
+# { id => '#1574542', name => 'Very Splendid Package Men\'s 2XL' },
+# { id => '#1574543', name => 'Very Splendid Package Men\'s 3XL' },
+# { id => '#157451S', name => 'Most Splendid Package, S' },
+# { id => '#157451M', name => 'Most Splendid Package, M' },
+# { id => '#157451L', name => 'Most Splendid Package, L' },
+# { id => '#157451X', name => 'Most Splendid Package, XL' },
+# { id => '', name => 'Splendider Gear' },
+# { id => '#155751', name => 'Drawstring Tote ' },
+# { id => '#155340', name => 'Beanie' },
+# { id => '#155339', name => 'Black Cap with Tote ' },
+# { id => '#212669', name => 'Stoneware Ceramic Mug' },
+# { id => '#190928S', name => 'Firefox Tee w/Woven Tag Navy S' },
+# { id => '#190928M', name => 'Firefox Tee w/Woven Tag Navy M' },
+# { id => '#190928L', name => 'Firefox Tee w/Woven Tag Navy L' },
+# { id => '#190928X', name => 'Firefox Tee w/Woven Tag Navy XL' },
+# { id => '#1909282', name => 'Firefox Tee w/Woven Tag Navy 2XL' },
+# { id => '#1909283', name => 'Firefox Tee w/Woven Tag Navy 3XL' },
+# { id => '#190929S', name => 'Firefox SS Lapis Tee Lapis S' },
+# { id => '#190929M', name => 'Firefox SS Lapis Tee Lapis M' },
+# { id => '#190929L', name => 'Firefox SS Lapis Tee Lapis L' },
+# { id => '#190929X', name => 'Firefox SS Lapis Tee Lapis XL' },
+# { id => '#155413S', name => 'Ladies\' T-shirt Navy S' },
+# { id => '#155413M', name => 'Ladies\' T-shirt Navy M' },
+# { id => '#155413L', name => 'Ladies\' T-shirt Navy L' },
+# { id => '#155413X', name => 'Ladies\' T-shirt Navy XL' },
+# { id => '#1554132', name => 'Ladies\' T-shirt Navy 2XL' },
{ id => '', name => 'Splendid Gear' },
- { id => '#192150', name => '1.25" Firefox Button-PKG25 ' },
+# { id => '#192150', name => '1.25" Firefox Button-PKG25 ' },
{ id => '#197156', name => 'Firefox Tattoos- Pkg50' },
- { id => '#197158', name => 'Firefox Sticker' },
- { id => '#197159', name => 'Firefox Laminated Badge' },
- { id => '#155754', name => 'Lanyard with Bulldog Clip (Mozilla)' },
+# { id => '#197158', name => 'Firefox Sticker' },
+# { id => '#197159', name => 'Firefox Laminated Badge' },
+# { id => '#155754', name => 'Lanyard with Bulldog Clip (Mozilla)' },
{ id => '#155756', name => 'Silicone Wristband ' },
- { id => '', name => 'Limited Availability Gear' },
- { id => '#265073', name => 'Mozilla Cap' },
+# { id => '', name => 'Limited Availability Gear' },
+# { id => '#265073', name => 'Mozilla Cap' },
{ id => '#265080', name => 'Fox Plush' },
- { id => '#265072S', name => 'Mozilla Custom Hoodie' },
- { id => '#265072M', name => 'Mozilla Custom Hoodie' },
- { id => '#265072L', name => 'Mozilla Custom Hoodie' },
- { id => '#265072X', name => 'Mozilla Custom Hoodie' },
- { id => '#2650722', name => 'Mozilla Custom Hoodie' },
- { id => '#265074S', name => 'Ladies\' Firefox Logo T-Shirt' },
- { id => '#265074M', name => 'Ladies\' Firefox Logo T-Shirt' },
- { id => '#265074L', name => 'Ladies\' Firefox Logo T-Shirt' },
- { id => '#265074X', name => 'Ladies\' Firefox Logo T-Shirt' },
- { id => '#2650742', name => 'Ladies\' Firefox Logo T-Shirt' },
- { id => '#265075S', name => 'Women\'s Vertical T-Shirt' },
- { id => '#265075M', name => 'Women\'s Vertical T-Shirt' },
- { id => '#265075L', name => 'Women\'s Vertical T-Shirt' },
- { id => '#265075X', name => 'Women\'s Vertical T-Shirt' },
- { id => '#2650752', name => 'Women\'s Vertical T-Shirt' },
- { id => '#265078S', name => 'Mozilla Horizontal T-Shirt' },
- { id => '#265078M', name => 'Mozilla Horizontal T-Shirt' },
- { id => '#265078L', name => 'Mozilla Horizontal T-Shirt' },
- { id => '#265078X', name => 'Mozilla Horizontal T-Shirt' },
- { id => '#2650782', name => 'Mozilla Horizontal T-Shirt' },
+# { id => '#265072S', name => 'Mozilla Custom Hoodie' },
+# { id => '#265072M', name => 'Mozilla Custom Hoodie' },
+# { id => '#265072L', name => 'Mozilla Custom Hoodie' },
+# { id => '#265072X', name => 'Mozilla Custom Hoodie' },
+# { id => '#2650722', name => 'Mozilla Custom Hoodie' },
+# { id => '#265074S', name => 'Ladies\' Firefox Logo T-Shirt' },
+# { id => '#265074M', name => 'Ladies\' Firefox Logo T-Shirt' },
+# { id => '#265074L', name => 'Ladies\' Firefox Logo T-Shirt' },
+# { id => '#265074X', name => 'Ladies\' Firefox Logo T-Shirt' },
+# { id => '#2650742', name => 'Ladies\' Firefox Logo T-Shirt' },
+# { id => '#265075S', name => 'Women\'s Vertical T-Shirt' },
+# { id => '#265075M', name => 'Women\'s Vertical T-Shirt' },
+# { id => '#265075L', name => 'Women\'s Vertical T-Shirt' },
+# { id => '#265075X', name => 'Women\'s Vertical T-Shirt' },
+# { id => '#2650752', name => 'Women\'s Vertical T-Shirt' },
+# { id => '#265078S', name => 'Mozilla Horizontal T-Shirt' },
+# { id => '#265078M', name => 'Mozilla Horizontal T-Shirt' },
+# { id => '#265078L', name => 'Mozilla Horizontal T-Shirt' },
+# { id => '#265078X', name => 'Mozilla Horizontal T-Shirt' },
+# { id => '#2650782', name => 'Mozilla Horizontal T-Shirt' },
];
mozspaces = [
{
name => 'Beijing',
- address1 => 'Mozilla Online Ltd, International Club Office Tower 800A',
- address2 => '21 Jian Guo Men Wai Avenue',
+ address1 => 'Mozilla Online Ltd.',
+ address2 => 'China Resources Building, Suite 1708, 8 Jianguomenbei Avenue',
city => 'Beijing',
- state => 'Chaoyang District',
+ state => 'Dongcheng District',
country => 'China',
- postcode => '100020',
+ postcode => '100005',
},
{
name => 'Berlin',
address1 => 'MZ Denmark ApS - Germany',
- address2 => 'Voltastrasse 5 / Building (Haus) 10 / Stair (Treppe) 6 / 2nd floor.',
+ address2 => 'GSG-Hof Schlesische Straße, Gebäude 3, 4. Obergeschoss, Schlesische Straße 27',
city => 'Berlin',
state => 'Germany',
country => 'Germany',
- postcode => '13355',
+ postcode => '10997',
},
{
name => 'London',
address1 => 'Mozilla London',
- address2 => '101 St. Martin\'s Lane, 3rd Floor',
+ address2 => 'Metal Box Factory, Suite 441, 4th floor, 30 Great Guildford Street',
city => 'London',
state => 'Greater London',
country => 'UK',
- postcode => 'WC2N 4AZ',
+ postcode => 'SE1 0HS',
},
{
name => 'Mountain View',
@@ -160,15 +160,6 @@ mozspaces = [
postcode => '11047',
},
{
- name => 'Tokyo',
- address1 => '7-5-6 Roppongi',
- address2 => '',
- city => 'Minato-ku',
- state => 'Tokyo',
- country => 'Japan',
- postcode => '106-0032',
- },
- {
name => 'Toronto',
address1 => 'Mozilla Canada',
address2 => '366 Adelaide Street W, Suite 500',
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/pages/group_members.html.tmpl b/extensions/BMO/template/en/default/pages/group_members.html.tmpl
index ec2cb2e46..1c593c07e 100644
--- a/extensions/BMO/template/en/default/pages/group_members.html.tmpl
+++ b/extensions/BMO/template/en/default/pages/group_members.html.tmpl
@@ -82,7 +82,7 @@
<a href="editusers.cgi?action=edit&amp;userid=[% member.id FILTER none %]"
target="_blank">
[% ELSE %]
- <a href="user_profile?login=[% member.login FILTER uri %]"
+ <a href="user_profile?user_id=[% member.id FILTER none %]"
target="_blank">
[% END %]
<span [% 'class="bz_inactive"' UNLESS member.is_enabled %]>
diff --git a/extensions/BMO/template/en/default/reports/components.html.tmpl b/extensions/BMO/template/en/default/reports/components.html.tmpl
deleted file mode 100644
index 3e23d389e..000000000
--- a/extensions/BMO/template/en/default/reports/components.html.tmpl
+++ /dev/null
@@ -1,99 +0,0 @@
-[%# The contents of this file are subject to the Mozilla Public
- # License Version 1.1 (the "License"); you may not use this file
- # except in compliance with the License. You may obtain a copy of
- # the License at http://www.mozilla.org/MPL/
- #
- # Software distributed under the License is distributed on an "AS
- # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
- # implied. See the License for the specific language governing
- # rights and limitations under the License.
- #
- # The Original Code is the Bugzilla Bug Tracking System.
- #
- # The Initial Developer of the Original Code is Netscape Communications
- # Corporation. Portions created by Netscape are
- # Copyright (C) 1998 Netscape Communications Corporation. All
- # Rights Reserved.
- #
- # Contributor(s): Bradley Baetz <bbaetz@student.usyd.edu.au>
- # Max Kanat-Alexander <mkanat@bugzilla.org>
- #%]
-
-[%# INTERFACE:
- # product: object. The product for which we want to display component
- # descriptions.
- # component: string. The name of the component to hilight in the browser
- #%]
-
-[% title = BLOCK %]
- Components for [% product.name FILTER html %]
-[% END %]
-
-[% inline_style = BLOCK %]
-.product_name {
- font-size: 2em;
- font-weight: normal;
-}
-.component_name {
- font-size: 1.5em;
- font-weight: normal;
-}
-.product_desc, .component_desc {
- padding-left: 1em;
- font-size: 1em;
-}
-.component_container {
- padding-left: 1em;
- margin-bottom: 1em;
-}
-.product_container, .instructions {
- margin-bottom: 1em;
-}
-.component_highlight {
- padding: 0 0 0 1em;
-}
-[% END %]
-
-[% PROCESS global/header.html.tmpl
- style_urls = [ "skins/standard/reports.css" ]
- title = title
- style = inline_style
-%]
-
-<h2>[% mark FILTER html %]</h2>
-
-<div class="product_container">
- <span class="product_name">[% product.name FILTER html %]</span>
- <div class="product_desc">
- [% product.description FILTER html_light %]
- </div>
-</div>
-
-<div class="instructions">
- Select a component to see open [% terms.bugs %] in that component:
-</div>
-
-[% FOREACH comp = product.components %]
- [% INCLUDE describe_comp %]
-[% END %]
-
-[% PROCESS global/footer.html.tmpl %]
-
-[%############################################################################%]
-[%# BLOCK for components %]
-[%############################################################################%]
-
-[% BLOCK describe_comp %]
- <div class="component_container [%- IF comp.name == component_mark %] component_hilite[% END %]">
- <div class="component_name">
- <a name="[% comp.name FILTER html %]"
- href="buglist.cgi?product=
- [%- product.name FILTER uri %]&amp;component=
- [%- comp.name FILTER uri %]&amp;resolution=---">
- [% comp.name FILTER html %]</a>
- </div>
- <div class="component_desc">
- [% comp.description FILTER html_light %]
- </div>
- </div>
-[% END %]
diff --git a/extensions/BMO/web/js/edituser_menu.js b/extensions/BMO/web/js/edituser_menu.js
index 7008a2b84..a300f5c24 100644
--- a/extensions/BMO/web/js/edituser_menu.js
+++ b/extensions/BMO/web/js/edituser_menu.js
@@ -10,7 +10,7 @@ function show_usermenu(id, email, show_edit) {
{
name: "Profile",
callback: function () {
- var href = "user_profile?login=" + encodeURIComponent(email);
+ var href = "user_profile?user_id=" + id;
window.open(href, "_blank");
}
},
diff --git a/extensions/BugModal/lib/MonkeyPatches.pm b/extensions/BugModal/lib/MonkeyPatches.pm
index 88fce11af..54bd6e560 100644
--- a/extensions/BugModal/lib/MonkeyPatches.pm
+++ b/extensions/BugModal/lib/MonkeyPatches.pm
@@ -40,31 +40,6 @@ sub active_attachments {
1;
-package Bugzilla::User;
-
-use 5.10.1;
-use strict;
-use warnings;
-
-sub moz_nick {
- my ($self) = @_;
- if (!exists $self->{moz_nick}) {
- if ($self->name =~ /:?:(\S+?)\b/) {
- $self->{moz_nick} = $1;
- }
- elsif ($self->name) {
- $self->{moz_nick} = $self->name;
- }
- else {
- $self->login =~ /^([^\@]+)\@/;
- $self->{moz_nick} = $1;
- }
- }
- return $self->{moz_nick};
-}
-
-1;
-
package Bugzilla::Attachment;
use 5.10.1;
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 51919ab27..36494773b 100644
--- a/extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl
+++ b/extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl
@@ -15,8 +15,8 @@
<div class="dropdown">
<button type="button" id="comment-tags-btn" arai-haspopup="true" aria-label="Tags Menu"
aria-expanded="false" aria-controls="comment-tags-menu" class="dropdown-button minor">Tags &#9662;</button>
- <ul id="comment-tags-menu" role="menu" tabindex="0" class="dropdown-content" style="display:none">
- <li class="dropdown-separator" role="presentation">
+ <ul id="comment-tags-menu" role="menu" tabindex="0" class="dropdown-content left" style="display:none">
+ <li role="presentation">
<a role="menuitem" tabindex="-1" data-comment-tag="">Reset</a>
</li>
</ul>
@@ -24,19 +24,21 @@
<div class="dropdown">
<button type="button" id="view-menu-btn" arai-haspopup="true" aria-label="View Menu"
aria-expanded="false" aria-controls="view-menu" class="dropdown-button minor">View &#9662;</button>
- <ul id="view-menu" role="menu" tabindex="0" class="dropdown-content" style="display:none">
- <li class="dropdown-separator" role="presentation">
+ <ul id="view-menu" role="menu" tabindex="0" class="dropdown-content left" style="display:none">
+ <li role="presentation">
<a id="view-reset" role="menuitem" tabindex="-1">Reset</a>
</li>
+ <li role="separator"></li>
<li role="presentation">
<a id="view-collapse-all" role="menuitem" tabindex="-1">Collapse All</a>
</li>
<li role="presentation">
<a id="view-expand-all" role="menuitem" tabindex="-1">Expand All</a>
</li>
- <li class="dropdown-separator" role="presentation">
+ <li role="presentation">
<a id="view-comments-only" role="menuitem" tabindex="-1">Comments Only</a>
</li>
+ <li role="separator"></li>
<li role="presentation">
<a id="view-toggle-cc" role="menuitem" tabindex="-1">Show CC Changes</a>
</li>
@@ -146,7 +148,7 @@
[% END %]
<button type="button" class="reply-btn minor"
data-reply-id="[% comment.count FILTER none %]"
- data-reply-name="[% comment.author.name || comment.author.moz_nick FILTER html %]"
+ data-reply-name="[% comment.author.name || comment.author.nick FILTER html %]"
>Reply</button>
[% END %]
<button type="button" class="change-spinner minor" id="cs-[% comment.count FILTER none %]">-</button>
@@ -184,9 +186,9 @@
<tr>
<td class="comment-collapse-reason"
[% IF user.setting("ui_use_absolute_time") == "on" %]
- title="[% comment.author.moz_nick FILTER html %] [[% comment.creation_ts FILTER time("%Y-%m-%d %H:%M %Z") FILTER html %]]">
+ title="[% comment.author.nick FILTER html %] [[% comment.creation_ts FILTER time("%Y-%m-%d %H:%M %Z") FILTER html %]]">
[% ELSE %]
- title="[% comment.author.moz_nick FILTER html %] [[% comment.creation_ts FILTER time_duration FILTER html %]]">
+ title="[% comment.author.nick FILTER html %] [[% comment.creation_ts FILTER time_duration FILTER html %]]">
[% END %]
Comment hidden ([% comment.collapsed_reason FILTER html %])
</td>
@@ -383,7 +385,7 @@
value FILTER bug_list_link;
ELSE;
- value FILTER truncate(256, '&hellip;') FILTER html;
+ value FILTER truncate(256, '…') FILTER html;
END;
END;
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 48c2c1803..4e740e35d 100644
--- a/extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl
+++ b/extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl
@@ -320,24 +320,26 @@
<div class="dropdown">
<button type="button" id="action-menu-btn" aria-haspopup="true" aria-label="Actions Menu"
aria-expanded="false" aria-controls="action-menu" class="dropdown-button minor">&#9662;</button>
- <ul class="dropdown-content" id="action-menu" role="menu" style="display:none;">
+ <ul class="dropdown-content left" id="action-menu" role="menu" style="display:none;">
<li role="presentation">
<a id="action-reset" role="menuitem" tabindex="-1">Reset Sections</a>
</li>
<li role="presentation">
<a id="action-expand-all" role="menuitem" tabindex="-1">Expand All Sections</a>
</li>
- <li class="dropdown-separator" role="presentation">
+ <li role="presentation">
<a id="action-collapse-all" role="menuitem" tabindex="-1">Collapse All Sections</a>
</li>
+ <li role="separator"></li>
[% IF user.id %]
<li role="presentation">
<a id="action-add-comment" role="menuitem" tabindex="-1">Add Comment</a>
</li>
[% END %]
- <li class="dropdown-separator" role="presentation">
+ <li role="presentation">
<a id="action-last-comment" role="menuitem" tabindex="-1">Last Comment</a>
</li>
+ <li role="separator"></li>
<li role="presentation">
<a id="action-history" role="menuitem" tabindex="-1">History</a>
</li>
@@ -374,17 +376,29 @@
hide_on_edit = can_edit_product
help = "describecomponents.cgi?product=$filtered_product"
%]
- <span aria-owns="product-name product-latch">
- <span role="button" aria-label="show product information" aria-expanded="false" tabindex="0"
- class="spin-latch" id="product-latch" data-latch="product" data-for="product">&#9656;</span>
- <div title="show product information" tabindex="0" class="spin-toggle"
- id="product-name" data-latch="product" data-for="product">
+ <div class="name-info-outer dropdown">
+ <span id="product-name" class="dropdown-button" tabindex="0" role="button"
+ aria-haspopup="menu" aria-controls="product-info">
[% bug.product FILTER html %]
- </div>
- <div id="product-info" style="display:none">
- [% bug.product_obj.description FILTER html_light %]
- </div>
- </span>
+ <span class="icon" aria-hidden="true">&#x25BE;</span>
+ </span>
+ <aside id="product-info" class="name-info-popup dropdown-content right hover-display" hidden role="menu"
+ aria-label="Product description and actions">
+ <header>
+ <div class="title">[%~ bug.product FILTER html ~%]</div>
+ <div class="description">[% bug.product_obj.description FILTER html_light %]</div>
+ </header>
+ <li role="separator"></li>
+ <div class="actions">
+ <div><a href="buglist.cgi?product=[% bug.product FILTER uri %]&amp;bug_status=__open__"
+ target="_blank" role="menuitem" tabindex="-1">See Other [% terms.Bugs %]</a></div>
+ <div><button disabled type="button" class="minor component-watching" role="menuitem" tabindex="-1"
+ data-product="[% bug.product FILTER html %]"
+ data-label-watch="Watch This Product" data-label-unwatch="Unwatch This Product"
+ data-source="BugModal">Watch This Product</button></div>
+ </div>
+ </aside>
+ </div>
[% END %]
[% WRAPPER bug_modal/field.html.tmpl
field = bug_fields.product
@@ -417,20 +431,30 @@
help = "describecomponents.cgi?product=$filtered_product&component=$filtered_component#$filtered_component"
%]
- <span aria-owns="component-name component-latch">
- <span role="button" aria-label="show component description" aria-expanded="false" tabindex="0"
- class="spin-latch" id="component-latch" data-latch="component" data-for="component">&#9656;</span>
- <div title="show component information" tabindex="0" class="spin-toggle" id="component-name"
- data-latch="#component-latch" data-for="component">
- [% bug.component FILTER html %]
- </div>
- <div id="component-info" style="display:none">
- <div>[% bug.component_obj.description FILTER html_light %]</div>
- <a href="buglist.cgi?component=[% bug.component FILTER uri %]&amp;
- [%~ %]product=[% bug.product FILTER uri %]&amp;
- [%~ %]bug_status=__open__" target="_blank">Other [% terms.Bugs %]</a>
- </div>
- </span>
+ <div class="name-info-outer dropdown">
+ <span id="component-name" class="dropdown-button" tabindex="0" role="button"
+ aria-haspopup="menu" aria-controls="component-info">
+ [% bug.component FILTER html %]
+ <span class="icon" aria-hidden="true">&#x25BE;</span>
+ </span>
+ <aside id="component-info" class="name-info-popup dropdown-content right hover-display" hidden role="menu"
+ aria-label="Component description and actions">
+ <header>
+ <div class="title">[%~ bug.product _ " :: " _ bug.component FILTER html ~%]</div>
+ <div class="description">[% bug.component_obj.description FILTER html_light %]</div>
+ </header>
+ <li role="separator"></li>
+ <div class="actions">
+ <div><a href="buglist.cgi?product=[% bug.product FILTER uri %]&amp;
+ [%~ %]component=[% bug.component FILTER uri %]&amp;bug_status=__open__"
+ target="_blank" role="menuitem" tabindex="-1">See Other [% terms.Bugs %]</a></div>
+ <div><button disabled type="button" class="minor component-watching" role="menuitem" tabindex="-1"
+ data-product="[% bug.product FILTER html %]" data-component="[% bug.component FILTER html %]"
+ data-label-watch="Watch This Component" data-label-unwatch="Unwatch This Component"
+ data-source="BugModal">Watch This Component</button></div>
+ </div>
+ </aside>
+ </div>
[% END %]
[%# importance %]
@@ -565,8 +589,8 @@
[%
sub = [];
- sub.push("Reporter: " _ bug.reporter.moz_nick);
- sub.push(unassigned ? "Unassigned" : "Assigned: " _ bug.assigned_to.moz_nick);
+ sub.push("Reporter: " _ bug.reporter.nick);
+ sub.push(unassigned ? "Unassigned" : "Assigned: " _ bug.assigned_to.nick);
IF bug.mentors.size;
sub.push("Mentored");
END;
@@ -1023,7 +1047,7 @@
[%
sub = [];
IF bug.status_whiteboard != "";
- sub.push("Whiteboard: " _ bug.status_whiteboard.truncate(256, '&hellip;'));
+ sub.push("Whiteboard: " _ bug.status_whiteboard.truncate(256, '…'));
END;
IF bug.cf_crash_signature != "";
sub.push("crash signature");
@@ -1325,7 +1349,7 @@
<div class="dropdown">
<button type="button" id="format-btn" aria-haspopup="true" aria-label="Format [% terms.Bug %] Menu"
aria-expanded="false" aria-controls="format-menu" class="dropdown-button minor">Format [% terms.Bug %] &#9652;</button>
- <ul class="dropdown-content menu-up" id="format-menu" role="menu" style="display:none;">
+ <ul class="dropdown-content left menu-up" id="format-menu" role="menu" style="display:none;">
<li role="presentation">
<a href="show_bug.cgi?format=multiple&amp;id=[% bug.id FILTER uri %]" role="menuitem" tabindex="-1">For Printing</a>
</li>
@@ -1346,7 +1370,7 @@
<div class="dropdown">
<button type="button" id="new-bug-btn" aria-haspopup="true" aria-label="New/Clone [% terms.Bug %] Menu"
aria-expanded="false" aria-controls="new-bug-menu" class="dropdown-button minor">New/Clone [% terms.Bug %] &#9652;</button>
- <ul class="dropdown-content menu-up" id="new-bug-menu" role="menu" style="display:none;">
+ <ul class="dropdown-content left menu-up" id="new-bug-menu" role="menu" style="display:none;">
<li role="presentation">
<a href="enter_bug.cgi" role="menuitem" tabindex="-1" target="_blank">
Create a new [% terms.bug %]</a>
@@ -1355,18 +1379,20 @@
<a href="enter_bug.cgi?product=[% bug.product FILTER uri %]"
role="menuitem" tabindex="-1" target="_blank">&#8230; in this product</a>
</li>
- <li class="dropdown-separator" role="presentation">
+ <li role="presentation">
<a href="enter_bug.cgi?product=[% bug.product FILTER uri %]&amp;component=[% bug.component FILTER uri %]"
role="menuitem" tabindex="-1" target="_blank">&#8230; in this component</a>
</li>
+ <li role="separator"></li>
<li role="presentation">
<a href="enter_bug.cgi?format=__default__&amp;product=[% bug.product FILTER uri %]&amp;blocked=[% bug.id FILTER uri %]"
role="menuitem" tabindex="-1" target="_blank">&#8230; that blocks this [% terms.bug %]</a>
</li>
- <li class="dropdown-separator" role="presentation">
+ <li role="presentation">
<a href="enter_bug.cgi?format=__default__&amp;product=[% bug.product FILTER uri %]&amp;dependson=[% bug.id FILTER uri %]"
role="menuitem" tabindex="-1" target="_blank">&#8230; that depends on this [% terms.bug %]</a>
</li>
+ <li role="separator"></li>
<li role="presentation">
<a href="enter_bug.cgi?format=__default__&amp;product=[% bug.product FILTER uri %]&amp;cloned_bug_id=[% bug.id FILTER uri %]"
role="menuitem" tabindex="-1" target="_blank">&#8230; as a clone of this [% terms.bug %]</a>
diff --git a/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl
index b9a42caf3..20561c760 100644
--- a/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl
+++ b/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl
@@ -8,6 +8,7 @@
[%
PROCESS global/variables.none.tmpl;
+ USE Bugzilla;
# <title>
IF bugs.defined;
@@ -53,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",
@@ -89,6 +91,8 @@
[%# expose useful data to js %]
BUGZILLA.bug_id = [% bug.id FILTER none %];
BUGZILLA.bug_title = '[% unfiltered_title FILTER js %]';
+ BUGZILLA.bug_summary = '[% bug.short_desc FILTER js %]';
+ BUGZILLA.bug_url = '[% Bugzilla.cgi.self_url FILTER js %]';
BUGZILLA.user = {
id: [% user.id FILTER none %],
login: '[% user.login FILTER js %]',
diff --git a/extensions/BugModal/template/en/default/bug_modal/user.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/user.html.tmpl
index 9eda7b936..6a0ce4e24 100644
--- a/extensions/BugModal/template/en/default/bug_modal/user.html.tmpl
+++ b/extensions/BugModal/template/en/default/bug_modal/user.html.tmpl
@@ -30,9 +30,9 @@ END;
[% IF simple %]
[% IF user.id %]
- <span class="fn" title="[% u.identity FILTER html %]">[% u.moz_nick FILTER html %]</span>
+ <span class="fn" title="[% u.identity FILTER html %]">[% u.nick FILTER html %]</span>
[% ELSE %]
- <span class="fn">[% u.moz_nick FILTER html %]</span>
+ <span class="fn">[% u.nick FILTER html %]</span>
[% END %]
[% ELSE %]
@@ -52,7 +52,7 @@ END;
href="user_profile?user_id=[% u.id FILTER none %]"
[% END %]
>
- <span class="[% user.id ? 'fn' : 'fna' %]">[% nick_only ? u.moz_nick : (u.name || u.nick) FILTER html %]</span>
+ <span class="[% user.id ? 'fn' : 'fna' %]">[% nick_only ? u.nick : (u.name || u.nick) FILTER html %]</span>
[%~~%]
</a>
[% END %]
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 4a770e66c..a4ae83d72 100644
--- a/extensions/BugModal/web/bug_modal.js
+++ b/extensions/BugModal/web/bug_modal.js
@@ -339,10 +339,6 @@ $(function() {
// copy summary to clipboard
- function clipboardSummary() {
- return 'Bug ' + BUGZILLA.bug_id + ' - ' + $('#field-value-short_desc').text();
- }
-
if ($('#copy-summary').length) {
var hasExecCopy = false;
try {
@@ -352,11 +348,24 @@ $(function() {
}
if (hasExecCopy) {
+ const url = BUGZILLA.bug_url;
+ const text = `Bug ${BUGZILLA.bug_id} - ${BUGZILLA.bug_summary}`;
+ const html = `<a href="${url}">${text}</a>`;
+
+ document.addEventListener('copy', event => {
+ if (event.target.nodeType === 1 && event.target.matches('#clip')) {
+ event.clipboardData.setData('text/uri-list', url);
+ event.clipboardData.setData('text/plain', text);
+ event.clipboardData.setData('text/html', html);
+ event.preventDefault();
+ }
+ });
+
$('#copy-summary')
.click(function() {
// execCommand("copy") only works on selected text
$('#clip-container').show();
- $('#clip').val(clipboardSummary()).select();
+ $('#clip').val(text).select();
$('#floating-message-text')
.text(document.execCommand("copy") ? 'Bug summary copied!' : 'Couldn’t copy bug summary');
$('#floating-message').fadeIn(250).delay(2500).fadeOut();
@@ -377,27 +386,6 @@ $(function() {
lb_show(this);
});
- // when copying the bug id and summary, reformat to remove \n and alias
- $(document).on(
- 'copy', function(event) {
- var selection = document.getSelection().toString().trim();
- var match = selection.match(/^(Bug \d+)\s*\n(.+)$/) ||
- selection.match(/^(Bug \d+)\s+\([^\)]+\)\s*\n(.+)$/);
- if (match) {
- var content = match[1] + ' - ' + match[2].trim();
- if (event.originalEvent.clipboardData) {
- event.originalEvent.clipboardData.setData('text/plain', content);
- }
- else if (window.clipboardData) {
- window.clipboardData.setData('Text', content);
- }
- else {
- return;
- }
- event.preventDefault();
- }
- });
-
// action button actions
// reset
@@ -1446,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/BugmailFilter/template/en/default/account/prefs/bugmail_filter.html.tmpl b/extensions/BugmailFilter/template/en/default/account/prefs/bugmail_filter.html.tmpl
index bb1381c46..ff64f8323 100644
--- a/extensions/BugmailFilter/template/en/default/account/prefs/bugmail_filter.html.tmpl
+++ b/extensions/BugmailFilter/template/en/default/account/prefs/bugmail_filter.html.tmpl
@@ -221,7 +221,7 @@ var cpts = new Array();
[% FOREACH flag = type.flags %]
[% IF flag_count > 10 && loop.count == 10 %]
<span id="show_all">
- &hellip;
+ …
(<a href="#" onclick="showAllFlags(); return false">show all</a>)
</span>
<span id="all_flags" class="bz_default_hidden">
diff --git a/extensions/ComponentWatching/Extension.pm b/extensions/ComponentWatching/Extension.pm
index ed47e64c3..96eb877a6 100644
--- a/extensions/ComponentWatching/Extension.pm
+++ b/extensions/ComponentWatching/Extension.pm
@@ -469,15 +469,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 });
@@ -500,6 +503,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'}
@@ -565,6 +572,8 @@ sub _addProductWatch {
VALUES (?, ?)
");
$sth->execute($user->id, $product->id);
+
+ return _getWatches($user, $dbh->bz_last_key());
}
sub _addComponentWatch {
@@ -585,6 +594,8 @@ sub _addComponentWatch {
VALUES (?, ?, ?)
");
$sth->execute($user->id, $component->product_id, $component->id);
+
+ return _getWatches($user, $dbh->bz_last_key());
}
sub _addPrefixWatch {
@@ -620,8 +631,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 {
@@ -717,4 +729,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/extensions/GoogleAnalytics/web/js/analytics.js b/extensions/GoogleAnalytics/web/js/analytics.js
index 25f7d7527..86f1f2592 100644
--- a/extensions/GoogleAnalytics/web/js/analytics.js
+++ b/extensions/GoogleAnalytics/web/js/analytics.js
@@ -15,6 +15,7 @@ $(function() {
ga('set', 'anonymizeIp', true);
ga('set', 'location', meta.data('location'));
ga('set', 'title', meta.data('title'));
+ ga('set', 'transport', 'beacon');
// Track page view
ga('send', 'pageview');
}
diff --git a/extensions/Gravatar/template/en/default/hook/bug/comments-user-image.html.tmpl b/extensions/Gravatar/template/en/default/hook/bug/comments-user-image.html.tmpl
index 3788a8452..361c02d2b 100644
--- a/extensions/Gravatar/template/en/default/hook/bug/comments-user-image.html.tmpl
+++ b/extensions/Gravatar/template/en/default/hook/bug/comments-user-image.html.tmpl
@@ -8,11 +8,7 @@
[% IF user.settings.show_gravatars.value == 'On' %]
[% IF who.last_activity_ts %]
- [% IF user.id %]
- <a href="user_profile?login=[% who.login FILTER uri %]">
- [% ELSE %]
- <a href="user_profile?user_id=[% who.id FILTER uri %]">
- [% END %]
+ <a href="user_profile?user_id=[% who.id FILTER none %]">
[% END %]
<img alt="User image" align="middle" src="[% who.gravatar FILTER none %]" width="32" height="32" border="0">
[% "</a>" IF who.last_activity_ts %]
diff --git a/extensions/PhabBugz/lib/Feed.pm b/extensions/PhabBugz/lib/Feed.pm
index c46d36c13..7d6b4e0ed 100644
--- a/extensions/PhabBugz/lib/Feed.pm
+++ b/extensions/PhabBugz/lib/Feed.pm
@@ -12,7 +12,7 @@ use 5.10.1;
use IO::Async::Timer::Periodic;
use IO::Async::Loop;
use List::Util qw(first);
-use List::MoreUtils qw(any);
+use List::MoreUtils qw(any uniq);
use Moo;
use Scalar::Util qw(blessed);
use Try::Tiny;
@@ -322,11 +322,27 @@ sub group_query {
# Make sure phab-bot also a member of the new project group so that it can
# make policy changes to the private revisions
- INFO("Setting project members for " . $project->name);
- my $set_members = $self->get_group_members( $group );
- push @$set_members, $phab_user unless grep $_->phid eq $phab_user->phid, @$set_members;
- $project->set_members( $set_members );
- $project->update();
+ INFO( "Checking project members for " . $project->name );
+ my $set_members = $self->get_group_members($group);
+ my @set_member_phids = uniq map { $_->phid } ( @$set_members, $phab_user );
+ my @current_member_phids = uniq map { $_->phid } @{ $project->members };
+ my ( $removed, $added ) = diff_arrays( \@current_member_phids, \@set_member_phids );
+
+ if (@$added) {
+ INFO( 'Adding project members: ' . join( ',', @$added ) );
+ $project->add_member($_) foreach @$added;
+ }
+
+ if (@$removed) {
+ INFO( 'Removing project members: ' . join( ',', @$removed ) );
+ $project->remove_member($_) foreach @$removed;
+ }
+
+ if (@$added || @$removed) {
+ my $result = $project->update();
+ local Bugzilla::Logging->fields->{api_result} = $result;
+ INFO( "Project " . $project->name . " updated" );
+ }
}
}
@@ -424,9 +440,9 @@ sub process_revision_change {
my ($timestamp) = Bugzilla->dbh->selectrow_array("SELECT NOW()");
INFO('Checking for revision attachment');
- my $attachment = create_revision_attachment($bug, $revision, $timestamp, $revision->author->bugzilla_user);
- INFO('Attachment ' . $attachment->id . ' created or already exists.');
-
+ my $rev_attachment = create_revision_attachment($bug, $revision, $timestamp, $revision->author->bugzilla_user);
+ INFO('Attachment ' . $rev_attachment->id . ' created or already exists.');
+
# ATTACHMENT OBSOLETES
# fixup attachments on current bug
@@ -567,7 +583,7 @@ sub process_revision_change {
# Email changes for this revisions bug and also for any other
# bugs that previously had these revision attachments
foreach my $bug_id ($revision->bug_id, keys %other_bugs) {
- Bugzilla::BugMail::Send($bug_id, { changer => Bugzilla->user });
+ Bugzilla::BugMail::Send($bug_id, { changer => $rev_attachment->attacher });
}
Bugzilla->set_user($old_user);
diff --git a/extensions/PhabBugz/lib/Project.pm b/extensions/PhabBugz/lib/Project.pm
index c52e1a661..b93a6eb9e 100644
--- a/extensions/PhabBugz/lib/Project.pm
+++ b/extensions/PhabBugz/lib/Project.pm
@@ -26,7 +26,7 @@ has id => ( is => 'ro', isa => Int );
has phid => ( is => 'ro', isa => Str );
has type => ( is => 'ro', isa => Str );
has name => ( is => 'ro', isa => Str );
-has description => ( is => 'ro', isa => Str );
+has description => ( is => 'ro', isa => Maybe[Str] );
has creation_ts => ( is => 'ro', isa => Str );
has modification_ts => ( is => 'ro', isa => Str );
has view_policy => ( is => 'ro', isa => Str );
@@ -307,7 +307,7 @@ sub set_policy {
############
sub _build_members {
- my ($self) = @_;
+ my ( $self ) = @_;
return [] unless $self->members_raw;
my @phids;
@@ -317,13 +317,11 @@ sub _build_members {
return [] if !@phids;
- my $users = Bugzilla::Extension::PhabBugz::User->match(
+ return Bugzilla::Extension::PhabBugz::User->match(
{
phids => \@phids
}
);
-
- return [ map { $_->bugzilla_user } @$users ];
}
1;
diff --git a/extensions/PhabBugz/lib/Revision.pm b/extensions/PhabBugz/lib/Revision.pm
index 900454220..4e82fa500 100644
--- a/extensions/PhabBugz/lib/Revision.pm
+++ b/extensions/PhabBugz/lib/Revision.pm
@@ -93,6 +93,8 @@ sub new_from_query {
: "";
return $class->new($result);
}
+
+ return undef;
}
sub BUILDARGS {
diff --git a/extensions/PhabBugz/lib/User.pm b/extensions/PhabBugz/lib/User.pm
index 9d4e9eef4..1bf1a842d 100644
--- a/extensions/PhabBugz/lib/User.pm
+++ b/extensions/PhabBugz/lib/User.pm
@@ -131,14 +131,18 @@ sub match {
attachments => { 'external-accounts' => 1 }
};
+ # We can only fetch 100 users at a time so we need to do this in lumps
my $phab_users = [];
- my $result = request( 'user.search', $data );
-
- if ( exists $result->{result}{data} && @{ $result->{result}{data} } ) {
- foreach my $user ( @{ $result->{result}{data} } ) {
- push @$phab_users, $class->new($user);
+ my $result;
+ do {
+ $result = request( 'user.search', $data );
+ if ( exists $result->{result}{data} && @{ $result->{result}{data} } ) {
+ foreach my $user ( @{ $result->{result}{data} } ) {
+ push @$phab_users, $class->new($user);
+ }
}
- }
+ $data->{after} = $result->{cursor}->{after};
+ } while ($result->{cursor}->{after});
return $phab_users;
}
diff --git a/extensions/PhabBugz/lib/Util.pm b/extensions/PhabBugz/lib/Util.pm
index d25f62f68..091475718 100644
--- a/extensions/PhabBugz/lib/Util.pm
+++ b/extensions/PhabBugz/lib/Util.pm
@@ -146,9 +146,10 @@ sub get_attachment_revisions {
my @revisions;
foreach my $revision_id (@revision_ids) {
- push @revisions, Bugzilla::Extension::PhabBugz::Revision->new_from_query({
+ my $revision = Bugzilla::Extension::PhabBugz::Revision->new_from_query({
ids => [ $revision_id ]
});
+ push @revisions, $revision if $revision;
}
return \@revisions;
diff --git a/extensions/PhabBugz/lib/WebService.pm b/extensions/PhabBugz/lib/WebService.pm
index 0239ccf74..fa9306667 100644
--- a/extensions/PhabBugz/lib/WebService.pm
+++ b/extensions/PhabBugz/lib/WebService.pm
@@ -14,6 +14,7 @@ use warnings;
use base qw(Bugzilla::WebService);
use Bugzilla::Constants;
+use Bugzilla::Error;
use Bugzilla::User;
use Bugzilla::Util qw(detaint_natural datetime_from time_ago trick_taint);
use Bugzilla::WebService::Constants;
@@ -29,34 +30,46 @@ use List::MoreUtils qw(any);
use MIME::Base64 qw(decode_base64);
use constant READ_ONLY => qw(
+ check_user_enter_bug_permission
check_user_permission_for_bug
needs_review
);
use constant PUBLIC_METHODS => qw(
+ check_user_enter_bug_permission
check_user_permission_for_bug
needs_review
set_build_target
);
-sub check_user_permission_for_bug {
- my ($self, $params) = @_;
-
- my $user = Bugzilla->login(LOGIN_REQUIRED);
-
+sub _check_phabricator {
# Ensure PhabBugz is on
ThrowUserError('phabricator_not_enabled')
unless Bugzilla->params->{phabricator_enabled};
+}
+
+sub _validate_phab_user {
+ my ($self, $user) = @_;
+
+ $self->_check_phabricator();
# Validate that the requesting user's email matches phab-bot
ThrowUserError('phabricator_unauthorized_user')
unless $user->login eq PHAB_AUTOMATION_USER;
+}
+
+sub check_user_permission_for_bug {
+ my ($self, $params) = @_;
+
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+ $self->_validate_phab_user($user);
# Validate that a bug id and user id are provided
ThrowUserError('phabricator_invalid_request_params')
unless ($params->{bug_id} && $params->{user_id});
- # Validate that the user and bug exist
+ # Validate that the user exists
my $target_user = Bugzilla::User->check({ id => $params->{user_id}, cache => 1 });
# Send back an object which says { "result": 1|0 }
@@ -65,10 +78,32 @@ sub check_user_permission_for_bug {
};
}
+sub check_user_enter_bug_permission {
+ my ($self, $params) = @_;
+
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+ $self->_validate_phab_user($user);
+
+ # Validate that a product name and user id are provided
+ ThrowUserError('phabricator_invalid_request_params')
+ unless ($params->{product} && $params->{user_id});
+
+ # Validate that the user exists
+ my $target_user = Bugzilla::User->check({ id => $params->{user_id}, cache => 1 });
+
+ # Send back an object with the attribute "result" set to 1 if the user
+ # can enter bugs into the given product, or 0 if not.
+ return {
+ result => $target_user->can_enter_product($params->{product}) ? 1 : 0
+ };
+}
+
sub needs_review {
my ($self, $params) = @_;
- ThrowUserError('phabricator_not_enabled')
- unless Bugzilla->params->{phabricator_enabled};
+
+ $self->_check_phabricator();
+
my $user = Bugzilla->login(LOGIN_REQUIRED);
my $dbh = Bugzilla->dbh;
@@ -169,13 +204,7 @@ sub set_build_target {
my $user = Bugzilla->login(LOGIN_REQUIRED);
- # Ensure PhabBugz is on
- ThrowUserError('phabricator_not_enabled')
- unless Bugzilla->params->{phabricator_enabled};
-
- # Validate that the requesting user's email matches phab-bot
- ThrowUserError('phabricator_unauthorized_user')
- unless $user->login eq PHAB_AUTOMATION_USER;
+ $self->_validate_phab_user($user);
my $revision_id = $params->{revision_id};
my $build_target = $params->{build_target};
@@ -204,13 +233,13 @@ sub rest_resources {
POST => {
method => 'set_build_target',
params => sub {
- return {
+ return {
revision_id => $_[0],
build_target => $_[1]
};
}
}
- },
+ },
# Bug permission checks
qr{^/phabbugz/check_bug/(\d+)/(\d+)$}, {
GET => {
@@ -220,6 +249,14 @@ sub rest_resources {
}
}
},
+ qr{^/phabbugz/check_enter_bug/([^/]+)/(\d+)$}, {
+ GET => {
+ method => 'check_user_enter_bug_permission',
+ params => sub {
+ return { product => $_[0], user_id => $_[1] };
+ },
+ },
+ },
# Review requests
qw{^/phabbugz/needs_review$}, {
GET => {
diff --git a/extensions/Review/Extension.pm b/extensions/Review/Extension.pm
index 72f16e3b6..a918a5ca5 100644
--- a/extensions/Review/Extension.pm
+++ b/extensions/Review/Extension.pm
@@ -25,6 +25,8 @@ use Bugzilla::Search;
use Bugzilla::User;
use Bugzilla::User::Setting;
use Bugzilla::Util qw(clean_text datetime_from diff_arrays);
+use Bugzilla::WebService::Util qw(filter_wants);
+use Scalar::Util qw(blessed);
use constant UNAVAILABLE_RE => qr/\b(?:unavailable|pto|away)\b/i;
use constant MENTOR_LIMIT => 10;
@@ -1068,4 +1070,51 @@ sub config_modify_panels {
};
}
+#
+# hooks
+#
+
+sub webservice_user_get {
+ my ($self, $args) = @_;
+ my ($webservice, $params, $users) = @$args{qw(webservice params users)};
+
+ return unless filter_wants($params, 'requests');
+
+ my $ids = [
+ map { blessed($_->{id}) ? $_->{id}->value : $_->{id} }
+ grep { exists $_->{id} }
+ @$users
+ ];
+
+ return unless @$ids;
+
+ my %user_map = map { $_->id => $_ } @{ Bugzilla::User->new_from_list($ids) };
+
+ foreach my $user (@$users) {
+ my $id = blessed($user->{id}) ? $user->{id}->value : $user->{id};
+ my $user_obj = $user_map{$id};
+
+ $user->{requests} = {
+ review => {
+ blocked => $webservice->type('boolean', $user_obj->reviews_blocked),
+ pending => $webservice->type('int', $user_obj->{review_request_count}),
+ },
+ feedback => {
+ # reviews_blocked includes feedback as well
+ blocked => $webservice->type('boolean', $user_obj->reviews_blocked),
+ pending => $webservice->type('int', $user_obj->{feedback_request_count}),
+ },
+ needinfo => {
+ blocked => $webservice->type('boolean', $user_obj->needinfo_blocked),
+ pending => $webservice->type('int', $user_obj->{needinfo_request_count}),
+ },
+ };
+ }
+}
+
+sub webservice_user_suggest {
+ my ($self, $args) = @_;
+ $self->webservice_user_get($args);
+}
+
__PACKAGE__->NAME;
diff --git a/extensions/UserProfile/template/en/default/hook/account/prefs/account-start.html.tmpl b/extensions/UserProfile/template/en/default/hook/account/prefs/account-start.html.tmpl
index f2e3aad01..b3a2fc5ea 100644
--- a/extensions/UserProfile/template/en/default/hook/account/prefs/account-start.html.tmpl
+++ b/extensions/UserProfile/template/en/default/hook/account/prefs/account-start.html.tmpl
@@ -6,6 +6,6 @@
# defined by the Mozilla Public License, v. 2.0.
#%]
-<a href="user_profile?login=[% user.login FILTER uri %]">
+<a href="user_profile?user_id=[% user.id FILTER none %]">
[% terms.Bugzilla %] User Profile
</a><br><hr>