summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorByron Jones <glob@mozilla.com>2015-04-30 08:06:06 +0200
committerByron Jones <glob@mozilla.com>2015-04-30 08:06:06 +0200
commitdac9873a61b108133ee00bda5b1862404712dd63 (patch)
treee54a93f442672073ce6ee48e0979580ca0e2e1e1
parent0d154533510e72467e1073c52095a1622f04f334 (diff)
downloadbugzilla-dac9873a61b108133ee00bda5b1862404712dd63.tar.gz
bugzilla-dac9873a61b108133ee00bda5b1862404712dd63.tar.xz
Bug 1151745: add ui to minimise steps required to move bugs between products
-rw-r--r--Bugzilla/Bug.pm72
-rw-r--r--extensions/BugModal/lib/WebService.pm232
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl30
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/field.html.tmpl3
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/header.html.tmpl1
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/new_product_groups.html.tmpl59
-rw-r--r--extensions/BugModal/web/bug_modal.css49
-rw-r--r--extensions/BugModal/web/bug_modal.js163
-rw-r--r--extensions/BugModal/web/error.pngbin0 -> 1179 bytes
-rw-r--r--extensions/ProdCompSearch/template/en/default/prodcompsearch/form.html.tmpl30
-rw-r--r--extensions/ProdCompSearch/web/js/prod_comp_search.js34
11 files changed, 591 insertions, 82 deletions
diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm
index 6dbcffe34..78bb1dff4 100644
--- a/Bugzilla/Bug.pm
+++ b/Bugzilla/Bug.pm
@@ -52,7 +52,7 @@ use Bugzilla::Comment;
use Bugzilla::BugUrl;
use Bugzilla::BugUserLastVisit;
-use List::MoreUtils qw(firstidx uniq part);
+use List::MoreUtils qw(firstidx uniq part any);
use List::Util qw(min max first);
use Storable qw(dclone);
use URI;
@@ -2689,7 +2689,32 @@ sub _set_product {
# other part of Bugzilla that checks $@.
undef $@;
Bugzilla->error_mode($old_error_mode);
-
+
+ my $invalid_groups;
+ my @idlist = ($self->id);
+ push(@idlist, map { $_->id } @{ $params->{other_bugs} })
+ if $params->{other_bugs};
+ @idlist = uniq @idlist;
+
+ # BMO - if everything is ok then we can skip the verfication page
+ if ($component_ok && $version_ok && $milestone_ok) {
+ $invalid_groups = $self->get_invalid_groups({ bug_ids => \@idlist, product => $product });
+ my $has_invalid_group = 0;
+ foreach my $group (@$invalid_groups) {
+ if (any { $_ eq $group->name } @{ $params->{groups}->{add} }) {
+ $has_invalid_group = 1;
+ last;
+ }
+ }
+ $params->{product_change_confirmed} =
+ # always check for invalid groups
+ !$has_invalid_group
+ # never skip verification when changing multiple bugs
+ && scalar(@idlist) == 1
+ # ensure the user has seen the group ui for private bugs
+ && (!@{ $self->groups_in } || Bugzilla->input_params->{group_verified});
+ }
+
my $verified = $params->{product_change_confirmed};
my %vars;
if (!$verified || !$component_ok || !$version_ok || !$milestone_ok) {
@@ -2709,27 +2734,9 @@ sub _set_product {
if (!$verified) {
$vars{verify_bug_groups} = 1;
- my $dbh = Bugzilla->dbh;
- my @idlist = ($self->id);
- push(@idlist, map {$_->id} @{ $params->{other_bugs} })
- if $params->{other_bugs};
- # Get the ID of groups which are no longer valid in the new product.
- my $gids = $dbh->selectcol_arrayref(
- 'SELECT bgm.group_id
- FROM bug_group_map AS bgm
- WHERE bgm.bug_id IN (' . join(',', ('?') x @idlist) . ')
- AND bgm.group_id NOT IN
- (SELECT gcm.group_id
- FROM group_control_map AS gcm
- WHERE gcm.product_id = ?
- AND ( (gcm.membercontrol != ?
- AND gcm.group_id IN ('
- . Bugzilla->user->groups_as_string . '))
- OR gcm.othercontrol != ?) )',
- undef, (@idlist, $product->id, CONTROLMAPNA, CONTROLMAPNA));
- $vars{'old_groups'} = Bugzilla::Group->new_from_list($gids);
+ $vars{old_groups} = $invalid_groups || $self->get_invalid_groups({ bug_ids => \@idlist, product => $product });
}
-
+
if (%vars) {
$vars{product} = $product;
$vars{bug} = $self;
@@ -4301,6 +4308,27 @@ sub map_fields {
return \%field_values;
}
+# Return the groups which are no longer valid in the specified product
+sub get_invalid_groups {
+ my ($invocant, $params) = @_;
+ my @idlist = @{ $params->{bug_ids} };
+ my $product = $params->{product};
+ my $gids = Bugzilla->dbh->selectcol_arrayref(
+ 'SELECT bgm.group_id
+ FROM bug_group_map AS bgm
+ WHERE bgm.bug_id IN (' . join(',', ('?') x @idlist) . ')
+ AND bgm.group_id NOT IN
+ (SELECT gcm.group_id
+ FROM group_control_map AS gcm
+ WHERE gcm.product_id = ?
+ AND ( (gcm.membercontrol != ?
+ AND gcm.group_id IN ('
+ . Bugzilla->user->groups_as_string . '))
+ OR gcm.othercontrol != ?) )',
+ undef, (@idlist, $product->id, CONTROLMAPNA, CONTROLMAPNA));
+ return Bugzilla::Group->new_from_list($gids);
+}
+
################################################################################
# check_can_change_field() defines what users are allowed to change. You
# can add code here for site-specific policy changes, according to the
diff --git a/extensions/BugModal/lib/WebService.pm b/extensions/BugModal/lib/WebService.pm
index 4c8b6b001..5a1ec15c0 100644
--- a/extensions/BugModal/lib/WebService.pm
+++ b/extensions/BugModal/lib/WebService.pm
@@ -15,9 +15,12 @@ use Bugzilla::Bug;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Field;
+use Bugzilla::Group;
use Bugzilla::Keyword;
use Bugzilla::Milestone;
+use Bugzilla::Product;
use Bugzilla::Version;
+use List::MoreUtils qw(any first_value);
# these methods are much lighter than our public API calls
@@ -33,6 +36,7 @@ sub rest_resources {
},
},
},
+
# returns pre-formatted html, enabling reuse of the user template
qr{^/bug_modal/cc/(\d+)$}, {
GET => {
@@ -42,6 +46,18 @@ sub rest_resources {
},
},
},
+
+ # returns fields that require touching when the product is changed
+ qw{^/bug_modal/new_product/(\d+)$}, {
+ GET => {
+ method => 'new_product',
+ params => sub {
+ # products with slashes in their name means we have to grab
+ # the product from the query-string instead of the path
+ return { id => $_[0], product_name => Bugzilla->input_params->{product} }
+ },
+ },
+ },
]
}
@@ -59,15 +75,15 @@ sub edit {
unless (grep { $_->id == $bug->product_id } @products) {
unshift @products, $bug->product_obj;
}
- $options{product} = [ map { { name => $_->name, description => $_->description } } @products ];
+ $options{product} = [ map { { name => $_->name } } @products ];
- $options{component} = _name_desc($bug->component, $bug->product_obj->components);
- $options{version} = _name($bug->version, $bug->product_obj->versions);
- $options{target_milestone} = _name($bug->target_milestone, $bug->product_obj->milestones);
- $options{priority} = _name($bug->priority, 'priority');
- $options{bug_severity} = _name($bug->bug_severity, 'bug_severity');
- $options{rep_platform} = _name($bug->rep_platform, 'rep_platform');
- $options{op_sys} = _name($bug->op_sys, 'op_sys');
+ $options{component} = _name($bug->product_obj->components, $bug->component);
+ $options{version} = _name($bug->product_obj->versions, $bug->version);
+ $options{target_milestone} = _name($bug->product_obj->milestones, $bug->target_milestone);
+ $options{priority} = _name('priority', $bug->priority);
+ $options{bug_severity} = _name('bug_severity', $bug->bug_severity);
+ $options{rep_platform} = _name('rep_platform', $bug->rep_platform);
+ $options{op_sys} = _name('op_sys', $bug->op_sys);
# custom select fields
my @custom_fields =
@@ -93,7 +109,7 @@ sub edit {
}
sub _name {
- my ($current, $values) = @_;
+ my ($values, $current) = @_;
# values can either be an array-ref of values, or a field name, which
# result in that field's legal-values being used.
if (!ref($values)) {
@@ -101,19 +117,7 @@ sub _name {
}
return [
map { { name => $_->name } }
- grep { $_->name eq $current || $_->is_active }
- @$values
- ];
-}
-
-sub _name_desc {
- my ($current, $values) = @_;
- if (!ref($values)) {
- $values = Bugzilla::Field->new({ name => $values, cache => 1 })->legal_values;
- }
- return [
- map { { name => $_->name, description => $_->description } }
- grep { $_->name eq $current || $_->is_active }
+ grep { (defined $current && $_->name eq $current) || $_->is_active }
@$values
];
}
@@ -135,4 +139,188 @@ sub cc {
return { html => $html };
}
+sub new_product {
+ my ($self, $params) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
+ my $bug = Bugzilla::Bug->check({ id => $params->{id} });
+ my $product = Bugzilla::Product->check({ name => $params->{product_name}, cache => 1 });
+ my $true = $self->type('boolean', 1);
+ my %result;
+
+ # components
+
+ my $components = _name($product->components);
+ my $current_component = $bug->component;
+ if (my $component = first_value { $_->{name} eq $current_component} @$components) {
+ # identical component in both products
+ $component->{selected} = $true;
+ }
+ else {
+ # default to a blank value
+ unshift @$components, {
+ name => '',
+ selected => $true,
+ };
+ }
+ $result{component} = $components;
+
+ # milestones
+
+ my $milestones = _name($product->milestones);
+ my $current_milestone = $bug->target_milestone;
+ if ($bug->check_can_change_field('target_milestone', 0, 1)
+ && (my $milestone = first_value { $_->{name} eq $current_milestone} @$milestones))
+ {
+ # identical milestone in both products
+ $milestone->{selected} = $true;
+ }
+ else {
+ # use default milestone
+ my $default_milestone = $product->default_milestone;
+ my $milestone = first_value { $_->{name} eq $default_milestone } @$milestones;
+ $milestone->{selected} = $true;
+ }
+ $result{target_milestone} = $milestones;
+
+ # versions
+
+ my $versions = _name($product->versions);
+ my $current_version = $bug->version;
+ my $selected_version;
+ if (my $version = first_value { $_->{name} eq $current_version } @$versions) {
+ # identical version in both products
+ $version->{selected} = $true;
+ $selected_version = $version;
+ }
+ elsif (
+ $current_version =~ /^(\d+) Branch$/
+ || $current_version =~ /^Firefox (\d+)$/
+ || $current_version =~ /^(\d+)$/)
+ {
+ # firefox, with its three version naming schemes
+ my $branch = $1;
+ foreach my $test_version ("$branch Branch", "Firefox $branch", $branch) {
+ if (my $version = first_value { $_->{name} eq $test_version } @$versions) {
+ $version->{selected} = $true;
+ $selected_version = $version;
+ last;
+ }
+ }
+ }
+ if (!$selected_version) {
+ # "unspecified", "other"
+ foreach my $test_version ("unspecified", "other") {
+ if (my $version = first_value { lc($_->{name}) eq $test_version } @$versions) {
+ $version->{selected} = $true;
+ $selected_version = $version;
+ last;
+ }
+ }
+ }
+ if (!$selected_version) {
+ # default to a blank value
+ unshift @$versions, {
+ name => '',
+ selected => $true,
+ };
+ }
+ $result{version} = $versions;
+
+ # groups
+
+ my @groups;
+
+ # find invalid groups
+ push @groups,
+ map {{
+ type => 'invalid',
+ group => $_,
+ checked => 0,
+ }}
+ @{ Bugzilla::Bug->get_invalid_groups({ bug_ids => [ $bug->id ], product => $product }) };
+
+ # logic lifted from bug/process/verify-new-product.html.tmpl
+ my $current_groups = $bug->groups_in;
+ my $group_controls = $product->group_controls;
+ foreach my $group_id (keys %$group_controls) {
+ my $group_control = $group_controls->{$group_id};
+ if ($group_control->{membercontrol} == CONTROLMAPMANDATORY
+ || ($group_control->{othercontrol} == CONTROLMAPMANDATORY && !$user->in_group($group_control->{name})))
+ {
+ # mandatory, always checked
+ push @groups, {
+ type => 'mandatory',
+ group => $group_control->{group},
+ checked => 1,
+ };
+ }
+ elsif (
+ ($group_control->{membercontrol} != CONTROLMAPNA && $user->in_group($group_control->{name}))
+ || $group_control->{othercontrol} != CONTROLMAPNA)
+ {
+ # optional, checked if..
+ my $group = $group_control->{group};
+ my $checked =
+ # same group as current product
+ (any { $_->id == $group->id } @$current_groups)
+ # member default
+ || $group_control->{membercontrol} == CONTROLMAPDEFAULT && $user->in_group($group_control->{name})
+ # or other default
+ || $group_control->{othercontrol} == CONTROLMAPDEFAULT && !$user->in_group($group_control->{name})
+ ;
+ push @groups, {
+ type => 'optional',
+ group => $group_control->{group},
+ checked => $checked || 0,
+ };
+ }
+ }
+
+ my $default_group_name = $product->default_security_group;
+ if (my $default_group = first_value { $_->{group}->name eq $default_group_name } @groups) {
+ # because we always allow the default product group to be selected, it's never invalid
+ $default_group->{type} = 'optional' if $default_group->{type} eq 'invalid';
+ }
+ else {
+ # add the product's default group if it's missing
+ unshift @groups, {
+ type => 'optional',
+ group => $product->default_security_group_obj,
+ checked => 0,
+ };
+ }
+
+ # if the bug is currently in a group, ensure a group is checked by default
+ # by checking the product's default group if no other groups apply
+ if (@$current_groups && !any { $_->{checked} } @groups) {
+ foreach my $g (@groups) {
+ next unless $g->{group}->name eq $default_group_name;
+ $g->{checked} = 1;
+ last;
+ }
+ }
+
+ # group by type and flatten
+ my $vars = {
+ product => $product,
+ groups => { invalid => [], mandatory => [], optional => [] },
+ };
+ foreach my $g (@groups) {
+ push @{ $vars->{groups}->{$g->{type}} }, {
+ id => $g->{group}->id,
+ name => $g->{group}->name,
+ description => $g->{group}->description,
+ checked => $g->{checked},
+ };
+ }
+
+ # build group selection html
+ my $template = Bugzilla->template;
+ $template->process('bug_modal/new_product_groups.html.tmpl', $vars, \$result{groups})
+ || ThrowTemplateError($template->error);
+
+ return \%result;
+}
+
1;
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 da7f2f294..63bd72dc6 100644
--- a/extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl
+++ b/extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl
@@ -233,7 +233,7 @@
Fetching
</span>
</button>
- <button type="submit" id="commit-btn" class="major" style="display:none">Save Changes</button>
+ <button type="submit" id="commit-btn" class="save-btn major" style="display:none">Save Changes</button>
</div>
[% END %]
<div class="button-row">
@@ -265,26 +265,50 @@
[% WRAPPER fields_lhs %]
[%# product %]
+ [% can_edit_product = bug.check_can_change_field("product", 0, 1) %]
[% WRAPPER bug_modal/field.html.tmpl
field = bug_fields.product
field_type = constants.FIELD_TYPE_SINGLE_SELECT
+ hide_on_edit = can_edit_product
%]
+ <span class="spin-latch" id="product-latch" data-latch="#product-latch" data-for="#product-info">&#9656;</span>
<div class="spin-toggle" data-latch="#product-latch" data-for="#product-info">
- <span class="spin-latch" id="product-latch">&#9656;</span>
[% bug.product FILTER html %]
</div>
<div id="product-info" style="display:none">
[% bug.product_obj.description FILTER html_light %]
</div>
[% END %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ field = bug_fields.product
+ field_type = constants.FIELD_TYPE_SINGLE_SELECT
+ hide_on_view = 1
+ hide_on_edit = !can_edit_product
+ append_content = 1
+ %]
+ <span id="product-search-container">
+ [% PROCESS prodcompsearch/form.html.tmpl
+ id = "pcs"
+ custom_select = 1
+ hidden = 1
+ throbber = "product-throbber"
+ %]
+ <button id="product-search" type="button" class="minor">Search</button>
+ <button id="product-search-cancel" type="button" class="minor" style="display:none">X</button>
+ <img id="product-throbber" src="extensions/BugModal/web/throbber.gif"
+ width="16" height="11" style="display:none">
+ <img id="product-search-error" class="tt" src="extensions/BugModal/web/error.png"
+ width="16" height="16" style="display:none">
+ </span>
+ [% END %]
[%# component %]
[% WRAPPER bug_modal/field.html.tmpl
field = bug_fields.component
field_type = constants.FIELD_TYPE_SINGLE_SELECT
%]
+ <span class="spin-latch" id="component-latch" data-latch="#component-latch" data-for="#component-info">&#9656;</span>
<div class="spin-toggle" data-latch="#component-latch" data-for="#component-info">
- <span class="spin-latch" id="component-latch">&#9656;</span>
[% bug.component FILTER html %]
</div>
<div id="component-info" style="display:none">
diff --git a/extensions/BugModal/template/en/default/bug_modal/field.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/field.html.tmpl
index 089543c39..9327069da 100644
--- a/extensions/BugModal/template/en/default/bug_modal/field.html.tmpl
+++ b/extensions/BugModal/template/en/default/bug_modal/field.html.tmpl
@@ -74,6 +74,9 @@ END;
IF !editable && edit_only;
edit_only = 0;
END;
+IF hide_on_view && hide_on_edit;
+ RETURN;
+END;
%]
<div class="field
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 6ba7ede8b..1fc00e82f 100644
--- a/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl
+++ b/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl
@@ -49,6 +49,7 @@
# assets
javascript_urls.push(
+ "extensions/ProdCompSearch/web/js/prod_comp_search.js",
"extensions/BugModal/web/time_ago.js",
"extensions/BugModal/web/bug_modal.js",
"extensions/BugModal/web/ZeroClipboard/ZeroClipboard.min.js",
diff --git a/extensions/BugModal/template/en/default/bug_modal/new_product_groups.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/new_product_groups.html.tmpl
new file mode 100644
index 000000000..29658faed
--- /dev/null
+++ b/extensions/BugModal/template/en/default/bug_modal/new_product_groups.html.tmpl
@@ -0,0 +1,59 @@
+[%# 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: product object
+ # groups:
+ # invalid => [{ id, name, description, checked }...]
+ # mandatory => "
+ # optional => "
+ #%]
+
+[% PROCESS group_list
+ group_set = groups.invalid
+ label = "Groups that are not valid for the '" _ product.name _ "' product:"
+ enabled = 0
+ disabled = 1
+%]
+[% PROCESS group_list
+ group_set = groups.mandatory
+ label = "Mandatory '" _ product.name _ "' groups:"
+ enabled = 1
+ disabled = 1
+%]
+[% PROCESS group_list
+ group_set = groups.optional
+ label = "Optional '" _ product.name _ "' groups:"
+ enabled = 1
+ disabled = 0
+%]
+<input type="hidden" name="group_verified" value="1">
+
+[% BLOCK group_list %]
+ [% RETURN UNLESS group_set.size %]
+ [% FILTER collapse %]
+ <div>
+ <div>
+ [% label FILTER html %]
+ </div>
+
+ [% FOREACH g = group_set %]
+ <div>
+ <input type="checkbox" value="[% g.name FILTER html %]"
+ id="group_[% g.id FILTER none %]"
+ [%= IF enabled %]name="groups"[% END %]
+ [%= "checked" IF g.checked %]
+ [%= "disabled" IF disabled %]>
+ <label for="group_[% g.id FILTER none %]" [%= IF disabled %]class="group-disabled"[% END %]>
+ [% g.description FILTER html %]
+ </label>
+ </div>
+ [% END %]
+ </div>
+ [% END %]
+[% END %]
diff --git a/extensions/BugModal/web/bug_modal.css b/extensions/BugModal/web/bug_modal.css
index f35ac415d..910199a53 100644
--- a/extensions/BugModal/web/bug_modal.css
+++ b/extensions/BugModal/web/bug_modal.css
@@ -69,6 +69,7 @@ select[multiple], .text_input, .yui-ac-input, input {
.spin-toggle {
cursor: pointer;
+ display: inline;
}
.spin-toggle:hover {
@@ -80,6 +81,12 @@ select[multiple], .text_input, .yui-ac-input, input {
padding-right: 5px;
}
+.attention {
+ -webkit-box-shadow: 0 0 2px 2px #f88;
+ -moz-box-shadow: 0 0 2px 2px #f88;
+ box-shadow: 0 0 2px 2px #f88;
+}
+
/* modules */
.module {
@@ -272,6 +279,11 @@ input[type="number"] {
color: #484;
}
+#product-latch, #component-latch {
+ padding-right: 0;
+ cursor: pointer;
+}
+
#cc-latch {
color: #999;
}
@@ -432,6 +444,12 @@ td.flag-requestee {
white-space: nowrap;
}
+/* groups */
+
+.group-disabled {
+ color: #888;
+}
+
/* comments and activity */
.change-set {
@@ -609,6 +627,10 @@ td.flag-requestee {
margin-right: 5px;
}
+#product-throbber {
+ margin-left: 8px;
+}
+
#commit {
margin: 5px;
}
@@ -718,3 +740,30 @@ div.ui-tooltip {
top: 8px;
right: 8px;
}
+
+/* product search */
+
+#product-search-container {
+ white-space: nowrap;
+}
+
+#product-search, #product-search-cancel {
+ margin-left: 8px;
+}
+
+#product-search-error {
+ margin-left: 8px;
+ vertical-align: middle;
+}
+
+.pcs-form {
+ display: inline;
+}
+
+.pcs-header {
+ display: none;
+}
+
+#pcs {
+ width: 235px;
+}
diff --git a/extensions/BugModal/web/bug_modal.js b/extensions/BugModal/web/bug_modal.js
index c96267e14..dba3abc85 100644
--- a/extensions/BugModal/web/bug_modal.js
+++ b/extensions/BugModal/web/bug_modal.js
@@ -52,8 +52,9 @@ $(function() {
.click(function(event) {
event.preventDefault();
var btn = $(event.target);
+ var modules;
if (btn.data('expanded-modules')) {
- var modules = btn.data('expanded-modules');
+ modules = btn.data('expanded-modules');
btn.data('expanded-modules', false);
modules.each(function() {
slide_module($(this).parent('.module'));
@@ -61,7 +62,7 @@ $(function() {
btn.text('Expand All');
}
else {
- var modules = $('.module-content:hidden');
+ modules = $('.module-content:hidden');
btn.data('expanded-modules', modules);
modules.each(function() {
slide_module($(this).parent('.module'));
@@ -134,7 +135,7 @@ $(function() {
});
// use non-native tooltips for relative times and bug summaries
- $('.rel-time, .rel-time-title, .bz_bug_link').tooltip({
+ $('.rel-time, .rel-time-title, .bz_bug_link, .tt').tooltip({
position: { my: "left top+8", at: "left bottom", collision: "flipfit" },
show: { effect: 'none' },
hide: { effect: 'none' }
@@ -146,7 +147,7 @@ $(function() {
$('.ui-helper-hidden-accessible').remove();
// product/component info
- $('.spin-toggle')
+ $('.spin-toggle, #product-latch, #component-latch')
.click(function(event) {
event.preventDefault();
var latch = $($(event.target).data('latch'));
@@ -222,8 +223,8 @@ $(function() {
$(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(.+)$/);
+ 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) {
@@ -352,6 +353,15 @@ $(function() {
});
$('#mode-btn').prop('disabled', false);
+ // disable the save buttons while posting
+ $('.save-btn')
+ .click(function(event) {
+ if (document.changeform.checkValidity && !document.changeform.checkValidity())
+ return;
+ $('.save-btn').attr('disabled', true);
+ })
+ .attr('disabled', false);
+
// cc toggle (follow/stop following)
$('#cc-btn')
.click(function(event) {
@@ -724,10 +734,6 @@ $(function() {
var cb = $(event.target);
var input = $('#' + cb.data('for'));
input.attr('disabled', cb.prop('checked'));
- if (!cb.prop('checked')) {
- input.focus();
- input.select();
- }
})
.change();
@@ -740,7 +746,7 @@ $(function() {
if (document.activeElement.nodeNode == 'INPUT' || document.activeElement.nodeName == 'TEXTAREA')
return;
if (String.fromCharCode(event.which).toLowerCase() == 'e') {
- if ($('#cancel-btn:visible').length == 0) {
+ if ($('#cancel-btn:visible').length === 0) {
event.preventDefault();
$('#mode-btn').click();
}
@@ -756,6 +762,136 @@ $(function() {
$('#top-save-btn').show();
$('#add-cc').focus();
});
+
+
+ // product change --> load components, versions, milestones, groups
+ $('#product').data('default', $('#product').val());
+ $('#component, #version, #target_milestone').each(function() {
+ $(this).data('default', $(this).val());
+ });
+ $('#product')
+ .change(function(event) {
+ $('#product-throbber').show();
+ $('#component, #version, #target_milestone').attr('disabled', true);
+
+ slide_module($('#module-tracking'), 'show');
+
+ $.each($('input[name=groups]'), function() {
+ if (this.checked) {
+ slide_module($('#module-security'), 'show');
+ return false;
+ }
+ });
+
+ bugzilla_ajax(
+ {
+ url: 'rest/bug_modal/new_product/' + BUGZILLA.bug_id + '?product=' + encodeURIComponent($('#product').val())
+ },
+ function(data) {
+ $('#product-throbber').hide();
+ $('#component, #version, #target_milestone').attr('disabled', false);
+ var is_default = $('#product').val() == $('#product').data('default');
+
+ // populate selects
+ $.each(data, function(key, value) {
+ if (key == 'groups') return;
+ var el = $('#' + key);
+ if (!el) return;
+ el.empty();
+ var selected = el.data('preselect');
+ $(value).each(function(i, v) {
+ el.append($('<option>', { value: v.name, text: v.name }));
+ if (typeof selected === 'undefined' && v.selected)
+ selected = v.name;
+ });
+ el.val(selected);
+ el.prop('required', true);
+ if (is_default) {
+ el.removeClass('attention');
+ el.val(el.data('default'));
+ }
+ else {
+ el.addClass('attention');
+ }
+ });
+
+ // update groups
+ $('#module-security .module-content')
+ .html(data.groups)
+ .addClass('attention');
+ },
+ function() {
+ $('#product-throbber').hide();
+ $('#component, #version, #target_milestone').attr('disabled', false);
+ }
+ );
+ });
+
+ // product/component search
+ $('#product-search')
+ .click(function(event) {
+ event.preventDefault();
+ $('#product').hide();
+ $('#product-search').hide();
+ $('#product-search-cancel').show();
+ $('.pcs-form').show();
+ $('#pcs').val('').focus();
+ });
+ $('#product-search-cancel')
+ .click(function(event) {
+ event.preventDefault();
+ $('#product-search-error').hide();
+ $('.pcs-form').hide();
+ $('#product').show();
+ $('#product-search-cancel').hide();
+ $('#product-search').show();
+ });
+ $('#pcs')
+ .on('autocompleteselect', function(event, ui) {
+ $('#product-search-error').hide();
+ $('.pcs-form').hide();
+ $('#product-search-cancel').hide();
+ $('#product-search').show();
+ if ($('#product').val() != ui.item.product) {
+ $('#component').data('preselect', ui.item.component);
+ $('#product').val(ui.item.product).change();
+ }
+ else {
+ $('#component').val(ui.item.component);
+ }
+ $('#product').show();
+ })
+ .autocomplete('option', 'autoFocus', true)
+ .keydown(function(event) {
+ if (event.which == 13) {
+ event.preventDefault();
+ var enterKeyEvent = $.Event("keydown");
+ enterKeyEvent.keyCode = $.ui.keyCode.ENTER;
+ $('#pcs').trigger(enterKeyEvent);
+ }
+ });
+ $(document)
+ .on('pcs:search', function(event) {
+ $('#product-search-error').hide();
+ })
+ .on('pcs:results', function(event) {
+ $('#product-search-error').hide();
+ })
+ .on('pcs:no_results', function(event) {
+ $('#product-search-error')
+ .prop('title', 'No components found')
+ .show();
+ })
+ .on('pcs:too_many_results', function(event, el) {
+ $('#product-search-error')
+ .prop('title', 'Results limited to ' + el.data('max_results') + ' components')
+ .show();
+ })
+ .on('pcs:error', function(event, message) {
+ $('#product-search-error')
+ .prop('title', message)
+ .show();
+ });
});
function confirmUnsafeURL(url) {
@@ -799,10 +935,11 @@ function bugzilla_ajax(request, done_fn, error_fn) {
}
})
.error(function(data) {
- $('#xhr-error').html(data.responseJSON.message);
+ var message = data.responseJSON ? data.responseJSON.message : 'Unexpected Error'; // all errors are unexpected :)
+ $('#xhr-error').html(message);
$('#xhr-error').show('fast');
if (error_fn)
- error_fn(data.responseJSON.message);
+ error_fn(message);
});
}
diff --git a/extensions/BugModal/web/error.png b/extensions/BugModal/web/error.png
new file mode 100644
index 000000000..14776e2d6
--- /dev/null
+++ b/extensions/BugModal/web/error.png
Binary files differ
diff --git a/extensions/ProdCompSearch/template/en/default/prodcompsearch/form.html.tmpl b/extensions/ProdCompSearch/template/en/default/prodcompsearch/form.html.tmpl
index 4239a9738..39919510c 100644
--- a/extensions/ProdCompSearch/template/en/default/prodcompsearch/form.html.tmpl
+++ b/extensions/ProdCompSearch/template/en/default/prodcompsearch/form.html.tmpl
@@ -8,24 +8,28 @@
[%#
# parameters (all are optional, defaults below)
- # id : id and prefix of elements
- # script_name : .cgi to redirect to
- # max_results : maximum results displayed
- # input_label : input field label
- # auto_focus : focus the search form on page load
- # format : format parameter passed to cgi
- # cloned_bug_id : cloned_bug_id parameter
- # new_tab : open in a new tab
- # anchor_component : append #component to url
+ # id: id and prefix of elements
+ # script_name: .cgi to redirect to
+ # max_results: maximum results displayed
+ # input_label: input field label
+ # auto_focus: focus the search form on page load
+ # format: format parameter passed to cgi
+ # cloned_bug_id: cloned_bug_id parameter
+ # new_tab: open in a new tab
+ # anchor_component: append #component to url
+ # custom_select: when true don't manage menu-item selects
+ # hidden: initialise container as display:none
+ # throbber: id of the throbber element
#%]
[%
- DEFAULT id = "pcs";
+ DEFAULT id = "pcs";
DEFAULT max_results = 100;
DEFAULT script_name = "enter_bug.cgi";
+ DEFAULT throbber = id _ "-throbber";
%]
-<div class="pcs-form">
+<div class="pcs-form" [%= 'style="display:none"' IF hidden %]>
<div class="pcs-header">
[% input_label FILTER none %]&nbsp;
<img id="[% id FILTER html %]-throbber"
@@ -42,12 +46,14 @@
</span>
</div>
<input type="text" class="prod_comp_search" id="[% id FILTER html %]" size="50"
- placeholder="Search by product and component keywords"
+ placeholder="Search by product and component"
data-script_name="[% script_name FILTER html %]"
data-format="[% format FILTER html %]"
data-cloned_bug_id="[% cloned_bug_id FILTER html %]"
data-new_tab="[% new_tab ? "1" : "0" %]"
data-anchor_component="[% anchor_component ? "1" : "0" %]"
data-max_results="[% max_results FILTER html %]"
+ data-ignore-select="[% custom_select ? "1" : "0" %]"
+ data-throbber="[% throbber FILTER html %]"
[% "autofocus" IF auto_focus %]>
</div>
diff --git a/extensions/ProdCompSearch/web/js/prod_comp_search.js b/extensions/ProdCompSearch/web/js/prod_comp_search.js
index ae7353779..2c9516967 100644
--- a/extensions/ProdCompSearch/web/js/prod_comp_search.js
+++ b/extensions/ProdCompSearch/web/js/prod_comp_search.js
@@ -14,8 +14,10 @@ $(function() {
delay: 500,
source: function(request, response) {
var el = this.element;
+ $(document).trigger('pcs:search', [ el ]);
var id = '#' + el.prop('id');
- $(id + '-throbber').show();
+ var throbber = $('#' + $(el).data('throbber'));
+ throbber.show();
$(id + '-no_components').hide();
$(id + '-too_many_components').hide();
$(id + '-error').hide();
@@ -29,17 +31,22 @@ $(function() {
contentType: 'application/json'
})
.done(function(data) {
- $(id + '-throbber').hide();
+ throbber.hide();
if (data.error) {
$(id + '-error').show();
console.log(data.message);
return false;
}
if (data.products.length === 0) {
- $(id + '-no_components').show();
+ $(id + '-no_results').show();
+ $(document).trigger('pcs:no_results', [ el ]);
}
else if (data.products.length > el.data('max_results')) {
- $(id + '-too_many_components').show();
+ $(id + '-too_many_results').show();
+ $(document).trigger('pcs:too_many_results', [ el ]);
+ }
+ else {
+ $(document).trigger('pcs:results', [ el, data ]);
}
var current_product = "";
var prod_comp_array = [];
@@ -55,8 +62,9 @@ $(function() {
params.push('product=' + encodeURIComponent(this.product));
if (this.product != current_product) {
prod_comp_array.push({
- label: this.product,
- url: el.data('script_name') + '?' + params.join('&')
+ label: this.product,
+ product: this.product,
+ url: el.data('script_name') + '?' + params.join('&')
});
current_product = this.product;
}
@@ -66,18 +74,21 @@ $(function() {
url += "#" + encodeURIComponent(this.component);
}
prod_comp_array.push({
- label: this.product + ' :: ' + this.component,
- url: url
+ label: this.product + ' :: ' + this.component,
+ product: this.product,
+ component: this.component,
+ url: url
});
});
response(prod_comp_array);
})
.fail(function(xhr, error_text) {
- if (xhr.responseJSON.error) {
+ if (xhr.responseJSON && xhr.responseJSON.error) {
error_text = xhr.responseJSON.message;
}
- $(id + '-throbber').hide();
+ throbber.hide();
$(id + '-comp_error').show();
+ $(document).trigger('pcs:error', [ el, error_text ]);
console.log(error_text);
});
},
@@ -88,6 +99,9 @@ $(function() {
event.preventDefault();
var el = $(this);
el.val(ui.item.label);
+ if (el.data('ignore-select')) {
+ return;
+ }
if (el.data('new_tab')) {
window.open(ui.item.url, '_blank');
}