diff options
Diffstat (limited to 'extensions/ProdCompSearch')
-rw-r--r-- | extensions/ProdCompSearch/Config.pm | 15 | ||||
-rw-r--r-- | extensions/ProdCompSearch/Extension.pm | 21 | ||||
-rw-r--r-- | extensions/ProdCompSearch/lib/WebService.pm | 120 | ||||
-rw-r--r-- | extensions/ProdCompSearch/template/en/default/pages/prodcompsearch.html.tmpl | 25 | ||||
-rw-r--r-- | extensions/ProdCompSearch/template/en/default/prodcompsearch/form.html.tmpl | 36 | ||||
-rw-r--r-- | extensions/ProdCompSearch/web/images/throbber.gif | bin | 0 -> 723 bytes | |||
-rw-r--r-- | extensions/ProdCompSearch/web/js/prod_comp_search.js | 139 | ||||
-rw-r--r-- | extensions/ProdCompSearch/web/styles/prod_comp_search.css | 20 |
8 files changed, 376 insertions, 0 deletions
diff --git a/extensions/ProdCompSearch/Config.pm b/extensions/ProdCompSearch/Config.pm new file mode 100644 index 000000000..c28b6d8f6 --- /dev/null +++ b/extensions/ProdCompSearch/Config.pm @@ -0,0 +1,15 @@ +# 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::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..a5955fd8b --- /dev/null +++ b/extensions/ProdCompSearch/Extension.pm @@ -0,0 +1,21 @@ +# 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::ProdCompSearch; +use strict; +use base qw(Bugzilla::Extension); + +our $VERSION = '1'; + +sub webservice { + my ($self, $args) = @_; + my $dispatch = $args->{dispatch}; + $dispatch->{PCS} = "Bugzilla::Extension::ProdCompSearch::WebService"; +} + + +__PACKAGE__->NAME; diff --git a/extensions/ProdCompSearch/lib/WebService.pm b/extensions/ProdCompSearch/lib/WebService.pm new file mode 100644 index 000000000..3f8f0b75e --- /dev/null +++ b/extensions/ProdCompSearch/lib/WebService.pm @@ -0,0 +1,120 @@ +# 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::ProdCompSearch::WebService; + +use strict; +use warnings; + +use base qw(Bugzilla::WebService); + +use Bugzilla::Error; +use Bugzilla::Util qw(detaint_natural trick_taint trim); + +sub prod_comp_search { + my ($self, $params) = @_; + my $user = Bugzilla->user; + my $dbh = Bugzilla->switch_to_shadow_db(); + + my $search = trim($params->{'search'} || ''); + $search || ThrowCodeError('param_required', + { function => 'PCS.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; + + trick_taint($search); + my @terms; + my @order; + + if ($search =~ /^(.*?)::(.*)$/) { + my ($product, $component) = (trim($1), trim($2)); + push @terms, _build_terms($product, 1, 0); + push @terms, _build_terms($component, 0, 1); + push @order, "products.name != " . $dbh->quote($product) if $product ne ''; + push @order, "components.name != " . $dbh->quote($component) if $component ne ''; + push @order, "products.name"; + push @order, "components.name"; + } else { + push @terms, _build_terms($search, 1, 1); + push @order, "products.name != " . $dbh->quote($search); + push @order, "components.name != " . $dbh->quote($search); + push @order, "products.name"; + push @order, "components.name"; + } + return { products => [] } if !scalar @terms; + + # To help mozilla staff file bmo administration bugs into the right + # component, sort bmo first when searching for 'bugzilla' + if ($search =~ /bugzilla/i && $search !~ /^bugzilla\s*::/i + && ($user->in_group('mozilla-corporation') || $user->in_group('mozilla-foundation'))) + { + unshift @order, "products.name != 'bugzilla.mozilla.org'"; + } + + 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 ", @terms) . ") + AND products.id IN (" . join(",", @$enterable_ids) . ") + ORDER BY " . join(", ", @order) . " $limit", + { Slice => {} }); + + return { products => $products }; +} + +sub _build_terms { + my ($query, $product, $component) = @_; + my $dbh = Bugzilla->dbh(); + + my @fields; + push @fields, 'products.name', 'products.description' if $product; + push @fields, 'components.name', 'components.description' if $component; + # note: CONCAT_WS is MySQL specific + my $field = "CONCAT_WS(' ', ". join(',', @fields) . ")"; + + my @terms; + foreach my $word (split(/[\s,]+/, $query)) { + push(@terms, $dbh->sql_iposition($dbh->quote($word), $field) . " > 0") + if $word ne ''; + } + return @terms; +} + +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..5b39315b5 --- /dev/null +++ b/extensions/ProdCompSearch/template/en/default/pages/prodcompsearch.html.tmpl @@ -0,0 +1,25 @@ +[%# 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. + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% PROCESS global/header.html.tmpl + title = "File a $terms.Bug" + javascript_urls = [ "js/yui3/yui/yui-min.js", + "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:" + script_name = "enter_bug.cgi" + %] +</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..b3d1deab1 --- /dev/null +++ b/extensions/ProdCompSearch/template/en/default/prodcompsearch/form.html.tmpl @@ -0,0 +1,36 @@ +[%# 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. + #%] + +<script type="text/javascript"> + [% IF script_name %] + ProdCompSearch.script_name = '[% script_name FILTER js %]'; + [% END %] + [% IF format %] + ProdCompSearch.format = '[% format FILTER js %]'; + [% END %] + [% IF cloned_bug_id %] + ProdCompSearch.cloned_bug_id = '[% cloned_bug_id FILTER js %]'; + [% END %] + [% IF new_tab %] + ProdCompSearch.new_tab = true; + [% END %] +</script> + +<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"> + <span id="prod_comp_no_components" class="bz_default_hidden"> + No components found</span> + <span id="prod_comp_error" class="bz_default_hidden"> + An error occured</span> + </div> + <input id="prod_comp_search" type="text" size="50" + placeholder="Search by product and component keywords"> +</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..cb4a50ccc --- /dev/null +++ b/extensions/ProdCompSearch/web/js/prod_comp_search.js @@ -0,0 +1,139 @@ +/* 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 = { + script_name: 'enter_bug.cgi', + script_choices: ['enter_bug.cgi', 'describecomponents.cgi'], + format: null, + cloned_bug_id: null, + new_tab: null +}; + +YUI({ + base: 'js/yui3/', + combine: false +}).use("node", "json-stringify", "autocomplete", "escape", + "datasource-io", "datasource-jsonschema", function(Y) { + Y.on("domready", function() { + var counter = 0, + dataSource = null, + autoComplete = null; + + var resultListFormat = function(query, results) { + return Y.Array.map(results, function(result) { + var data = result.raw; + result.text = data.product + ' :: ' + data.component; + return Y.Escape.html(result.text); + }); + }; + + var requestTemplate = function(query) { + counter = counter + 1; + var json_object = { + version: "1.1", + method : "PCS.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' } + }, + on: { + error: function(e) { + if (console.error && e.response.meta.error) { + console.error(e.response.meta.error.message); + } + Y.one("#prod_comp_throbber").addClass('bz_default_hidden'); + Y.one("#prod_comp_error").removeClass('bz_default_hidden'); + } + } + }); + + dataSource.plug(Y.Plugin.DataSourceJSONSchema, { + schema: { + resultListLocator : "result.products", + resultFields : [ "product", "component" ], + metaFields : { error : 'error' } + } + }); + + var input = Y.one('#prod_comp_search'); + + input.plug(Y.Plugin.AutoComplete, { + activateFirstItem: false, + enableCache: true, + source: dataSource, + minQueryLength: 3, + queryDelay: 0.05, + resultFormatter: resultListFormat, + suppressInputUpdate: true, + maxResults: 25, + scrollIntoView: true, + requestTemplate: requestTemplate, + on: { + query: function(e) { + Y.one("#prod_comp_throbber").removeClass('bz_default_hidden'); + Y.one("#prod_comp_no_components").addClass('bz_default_hidden'); + Y.one("#prod_comp_error").addClass('bz_default_hidden'); + }, + results: function(e) { + Y.one("#prod_comp_throbber").addClass('bz_default_hidden'); + input.ac.set('activateFirstItem', e.results.length == 1); + if (e.results.length == 0) { + Y.one("#prod_comp_no_components").removeClass('bz_default_hidden'); + } + }, + select: function(e) { + // Only redirect if the script_name is a valid choice + if (Y.Array.indexOf(ProdCompSearch.script_choices, ProdCompSearch.script_name) == -1) + return; + + var data = e.result.raw; + var url = ProdCompSearch.script_name + + "?product=" + encodeURIComponent(data.product) + + "&component=" + encodeURIComponent(data.component); + if (ProdCompSearch.script_name == 'enter_bug.cgi') { + if (ProdCompSearch.format) + url += "&format=" + encodeURIComponent(ProdCompSearch.format); + if (ProdCompSearch.cloned_bug_id) + url += "&cloned_bug_id=" + encodeURIComponent(ProdCompSearch.cloned_bug_id); + } + if (ProdCompSearch.script_name == 'describecomponents.cgi') { + url += "#" + encodeURIComponent(data.component); + } + if (ProdCompSearch.new_tab) { + window.open(url, '_blank'); + } + else { + window.location.href = url; + } + } + }, + after: { + select: function(e) { + if (ProdCompSearch.new_tab) { + input.set('value',''); + } + } + } + }); + + 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..a89856e22 --- /dev/null +++ b/extensions/ProdCompSearch/web/styles/prod_comp_search.css @@ -0,0 +1,20 @@ +/* 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; +} + +#prod_comp_no_components, #prod_comp_error { + color: red; +} |