diff options
-rw-r--r-- | extensions/ProdCompSearch/Config.pm | 8 | ||||
-rw-r--r-- | extensions/ProdCompSearch/Extension.pm | 7 | ||||
-rw-r--r-- | extensions/ProdCompSearch/lib/WebService.pm | 94 | ||||
-rw-r--r-- | extensions/ProdCompSearch/template/en/default/pages/prodcompsearch.html.tmpl | 15 | ||||
-rw-r--r-- | extensions/ProdCompSearch/template/en/default/prodcompsearch/form.html.tmpl | 16 | ||||
-rw-r--r-- | extensions/ProdCompSearch/web/images/throbber.gif | bin | 0 -> 723 bytes | |||
-rw-r--r-- | extensions/ProdCompSearch/web/js/prod_comp_search.js | 99 | ||||
-rw-r--r-- | extensions/ProdCompSearch/web/styles/prod_comp_search.css | 16 |
8 files changed, 255 insertions, 0 deletions
diff --git a/extensions/ProdCompSearch/Config.pm b/extensions/ProdCompSearch/Config.pm new file mode 100644 index 000000000..880cb1892 --- /dev/null +++ b/extensions/ProdCompSearch/Config.pm @@ -0,0 +1,8 @@ +package Bugzilla::Extension::ProdCompSearch; +use strict; + +use constant NAME => 'ProdCompSearch'; +use constant REQUIRED_MODULES => []; +use constant OPTIONAL_MODULES => []; + +__PACKAGE__->NAME; diff --git a/extensions/ProdCompSearch/Extension.pm b/extensions/ProdCompSearch/Extension.pm new file mode 100644 index 000000000..83e6118d3 --- /dev/null +++ b/extensions/ProdCompSearch/Extension.pm @@ -0,0 +1,7 @@ +package Bugzilla::Extension::ProdCompSearch; +use strict; +use base qw(Bugzilla::Extension); + +our $VERSION = '1'; + +__PACKAGE__->NAME; diff --git a/extensions/ProdCompSearch/lib/WebService.pm b/extensions/ProdCompSearch/lib/WebService.pm new file mode 100644 index 000000000..f972b80f7 --- /dev/null +++ b/extensions/ProdCompSearch/lib/WebService.pm @@ -0,0 +1,94 @@ +package Bugzilla::Extension::ProdCompSearch::WebService; + +use strict; +use warnings; + +use base qw(Bugzilla::WebService); + +use Bugzilla::Error; +use Bugzilla::Util qw(detaint_natural trick_taint); + +sub prod_comp_search { + my ($self, $params) = @_; + my $user = Bugzilla->user; + my $dbh = Bugzilla->switch_to_shadow_db(); + + my $search = $params->{'search'}; + $search || ThrowCodeError('param_required', + { function => 'Bug.prod_comp_search', param => 'search' }); + + my $limit = detaint_natural($params->{'limit'}) + ? $dbh->sql_limit($params->{'limit'}) + : ''; + + # We do this in the DB directly as we want it to be fast and + # not have the overhead of loading full product objects + + # All products which the user has "Entry" access to. + my $enterable_ids = $dbh->selectcol_arrayref( + 'SELECT products.id FROM products + LEFT JOIN group_control_map + ON group_control_map.product_id = products.id + AND group_control_map.entry != 0 + AND group_id NOT IN (' . $user->groups_as_string . ') + WHERE group_id IS NULL + AND products.isactive = 1'); + + if (scalar @$enterable_ids) { + # And all of these products must have at least one component + # and one version. + $enterable_ids = $dbh->selectcol_arrayref( + 'SELECT DISTINCT products.id FROM products + WHERE ' . $dbh->sql_in('products.id', $enterable_ids) . + ' AND products.id IN (SELECT DISTINCT components.product_id + FROM components + WHERE components.isactive = 1) + AND products.id IN (SELECT DISTINCT versions.product_id + FROM versions + WHERE versions.isactive = 1)'); + } + + return { products => [] } if !scalar @$enterable_ids; + + my @list; + foreach my $word (split(/[\s,]+/, $search)) { + if ($word ne "") { + my $sql_word = $dbh->quote($word); + trick_taint($sql_word); + # XXX CONCAT_WS is MySQL specific + my $field = "CONCAT_WS(' ', products.name, components.name, components.description)"; + push(@list, $dbh->sql_iposition($sql_word, $field) . " > 0"); + } + } + + my $products = $dbh->selectall_arrayref(" + SELECT products.name AS product, + components.name AS component + FROM products + INNER JOIN components ON products.id = components.product_id + WHERE (" . join(" AND ", @list) . ") + AND products.id IN (" . join(",", @$enterable_ids) . ") + ORDER BY products.name $limit", + { Slice => {} }); + + # To help mozilla staff file bmo administration bugs into the right + # component, sort bmo in front of bugzilla. + if ($user->in_group('mozilla-corporation') || $user->in_group('mozilla-foundation')) { + $products = [ + sort { + return 1 if $a->{product} eq 'Bugzilla' + && $b->{product} eq 'bugzilla.mozilla.org'; + return -1 if $b->{product} eq 'Bugzilla' + && $a->{product} eq 'bugzilla.mozilla.org'; + return lc($a->{product}) cmp lc($b->{product}) + || lc($a->{component}) cmp lc($b->{component}); + } @$products + ]; + } + + return { products => $products }; +} + +1; + + diff --git a/extensions/ProdCompSearch/template/en/default/pages/prodcompsearch.html.tmpl b/extensions/ProdCompSearch/template/en/default/pages/prodcompsearch.html.tmpl new file mode 100644 index 000000000..0948fd1a0 --- /dev/null +++ b/extensions/ProdCompSearch/template/en/default/pages/prodcompsearch.html.tmpl @@ -0,0 +1,15 @@ +[% PROCESS global/variables.none.tmpl %] + +[% PROCESS global/header.html.tmpl + title = "File a $terms.Bug" + javascript_urls = [ "extensions/ProdCompSearch/web/js/prod_comp_search.js" ] + style_urls = [ "extensions/ProdCompSearch/web/styles/prod_comp_search.css" ] +%] + +<div id="prod_comp_search_main"> + [% PROCESS prodcompsearch/form.html.tmpl + query_header = "File a $terms.Bug:" + %] +</div> + +[% PROCESS global/footer.html.tmpl %] diff --git a/extensions/ProdCompSearch/template/en/default/prodcompsearch/form.html.tmpl b/extensions/ProdCompSearch/template/en/default/prodcompsearch/form.html.tmpl new file mode 100644 index 000000000..79693fcec --- /dev/null +++ b/extensions/ProdCompSearch/template/en/default/prodcompsearch/form.html.tmpl @@ -0,0 +1,16 @@ +[%# 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. + #%] + +<div id="prod_comp_search_form" class="yui3-skin-sam"> + <div id="prod_comp_search_header"> + [% input_label FILTER none %] + <img id="prod_comp_throbber" src="extensions/ProdCompSearch/web/images/throbber.gif" + class="bz_default_hidden" width="16" height="11"> + </div> + <input id="prod_comp_search" type="text" size="50"> +</div> diff --git a/extensions/ProdCompSearch/web/images/throbber.gif b/extensions/ProdCompSearch/web/images/throbber.gif Binary files differnew file mode 100644 index 000000000..bc4fa6561 --- /dev/null +++ b/extensions/ProdCompSearch/web/images/throbber.gif diff --git a/extensions/ProdCompSearch/web/js/prod_comp_search.js b/extensions/ProdCompSearch/web/js/prod_comp_search.js new file mode 100644 index 000000000..4d31437ad --- /dev/null +++ b/extensions/ProdCompSearch/web/js/prod_comp_search.js @@ -0,0 +1,99 @@ +/* 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 and component search to file a new bug + +var ProdCompSearch = { + format: null, + cloned_bug_id: null +}; + +YUI({ + base: 'js/yui3/', + combine: false +}).use("node", "json-stringify", "autocomplete", "escape", + "datasource-io", "datasource-jsonschema", "array-extras", function (Y) { + var counter = 0, + current_query = null, + dataSource = null, + autoComplete = null; + + var resultListFormat = function(query, results) { + return Y.Array.map(results, function (result) { + var data = result.raw; + return Y.Escape.html(data.product) + " :: " + + Y.Escape.html(data.component); + }); + }; + + var requestTemplate = function(query) { + counter = counter + 1; + var json_object = { + version: "1.1", + method : "MyDashboard.prod_comp_search", + id : counter, + params : { search: query } + }; + return Y.JSON.stringify(json_object); + }; + + var dataSource = new Y.DataSource.IO({ + source: 'jsonrpc.cgi', + ioConfig: { + method: "POST", + headers: { 'Content-Type': 'application/json' } + } + }); + + dataSource.plug(Y.Plugin.DataSourceJSONSchema, { + schema: { + resultListLocator : "result.products", + resultFields : [ "product", "component" ] + } + }); + + var input = Y.one('#prod_comp_search'); + + input.plug(Y.Plugin.AutoComplete, { + enableCache: true, + source: dataSource, + minQueryLength: 3, + queryDelay: 0.05, + resultFormatter: resultListFormat, + suppressInputUpdate: true, + maxResults: 25, + scrollIntoView: true, + requestTemplate: requestTemplate, + on: { + query: function(e) { + current_query = e.inputValue; + Y.one("#prod_comp_throbber").removeClass('bz_default_hidden'); + }, + results: function(e) { + Y.one("#prod_comp_throbber").addClass('bz_default_hidden'); + }, + select: function(e) { + input.value = current_query; + var data = e.result.raw; + var url = "enter_bug.cgi?product=" + encodeURIComponent(data.product) + + "&component=" + encodeURIComponent(data.component); + if (ProdCompSearch.format) + url += "&format=" + encodeURIComponent(ProdCompSearch.format); + if (ProdCompSearch.cloned_bug_id) + url += "&cloned_bug_id=" + encodeURIComponent(ProdCompSearch.cloned_bug_id); + window.location.href = url; + } + }, + }); + + input.on('focus', function (e) { + if (e.target.value && e.target.value.length > 3) { + dataSource.load(e.target.value); + } + }); +}); diff --git a/extensions/ProdCompSearch/web/styles/prod_comp_search.css b/extensions/ProdCompSearch/web/styles/prod_comp_search.css new file mode 100644 index 000000000..59927302c --- /dev/null +++ b/extensions/ProdCompSearch/web/styles/prod_comp_search.css @@ -0,0 +1,16 @@ +/* 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. */ + +#prod_comp_search_main { + width: 400px; + margin-right: auto; + margin-left: auto; +} + +#prod_comp_search_form .yui3-aclist-input { + width: 360px; +} |