diff options
Diffstat (limited to 'extensions/BugModal')
-rw-r--r-- | extensions/BugModal/lib/WebService.pm | 232 | ||||
-rw-r--r-- | extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl | 30 | ||||
-rw-r--r-- | extensions/BugModal/template/en/default/bug_modal/field.html.tmpl | 3 | ||||
-rw-r--r-- | extensions/BugModal/template/en/default/bug_modal/header.html.tmpl | 1 | ||||
-rw-r--r-- | extensions/BugModal/template/en/default/bug_modal/new_product_groups.html.tmpl | 59 | ||||
-rw-r--r-- | extensions/BugModal/web/bug_modal.css | 49 | ||||
-rw-r--r-- | extensions/BugModal/web/bug_modal.js | 163 | ||||
-rw-r--r-- | extensions/BugModal/web/error.png | bin | 0 -> 1179 bytes |
8 files changed, 499 insertions, 38 deletions
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">▸</span> <div class="spin-toggle" data-latch="#product-latch" data-for="#product-info"> - <span class="spin-latch" id="product-latch">▸</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">▸</span> <div class="spin-toggle" data-latch="#component-latch" data-for="#component-info"> - <span class="spin-latch" id="component-latch">▸</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 Binary files differnew file mode 100644 index 000000000..14776e2d6 --- /dev/null +++ b/extensions/BugModal/web/error.png |