diff options
15 files changed, 1693 insertions, 0 deletions
diff --git a/extensions/MyDashboard/Config.pm b/extensions/MyDashboard/Config.pm new file mode 100644 index 000000000..7c14936ff --- /dev/null +++ b/extensions/MyDashboard/Config.pm @@ -0,0 +1,14 @@ +# 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::MyDashboard; + +use strict; + +use constant NAME => 'MyDashboard'; + +__PACKAGE__->NAME; diff --git a/extensions/MyDashboard/Extension.pm b/extensions/MyDashboard/Extension.pm new file mode 100644 index 000000000..869d3c81e --- /dev/null +++ b/extensions/MyDashboard/Extension.pm @@ -0,0 +1,379 @@ +# 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::MyDashboard; + +use strict; + +use base qw(Bugzilla::Extension); + +use Bugzilla; +use Bugzilla::Constants; +use Bugzilla::Search; +use Bugzilla::Util; +use Bugzilla::Status; +use Bugzilla::Field; +use Bugzilla::Search::Saved; + +use Bugzilla::Extension::MyDashboard::TimeAgo qw(time_ago); + +use DateTime; + +our $VERSION = BUGZILLA_VERSION; + +sub QUERY_DEFS { + my $user = Bugzilla->user; + + my @query_defs = ( + { + name => 'assignedbugs', + heading => 'Assigned to You', + description => 'The bug has been assigned to you and it is not resolved or closed yet.', + params => { + 'bug_status' => ['__open__'], + 'emailassigned_to1' => 1, + 'emailtype1' => 'exact', + 'email1' => $user->login + } + }, + { + name => 'newbugs', + heading => 'New Reported by You', + description => 'You reported the bug but nobody has accepted it yet.', + params => { + 'bug_status' => ['NEW'], + 'emailreporter1' => 1, + 'emailtype1' => 'exact', + 'email1' => $user->login + } + }, + { + name => 'inprogressbugs', + heading => "In Progress Reported by You", + description => 'You reported the bug, the developer accepted the bug and is hopefully working on it.', + params => { + 'bug_status' => [ map { $_->name } grep($_->name ne 'NEW' && $_->name ne 'MODIFIED', _open_states()) ], + 'emailreporter1' => 1, + 'emailtype1' => 'exact', + 'email1' => $user->login + } + }, + { + name => 'openccbugs', + heading => "You Are CC'd On", + description => 'You are in the CC list of the bug, so you are watching it.', + params => { + 'bug_status' => ['__open__'], + 'emailcc1' => 1, + 'emailtype1' => 'exact', + 'email1' => $user->login + } + }, + ); + + if (Bugzilla->params->{'useqacontact'}) { + push(@query_defs, { + name => 'qacontactbugs', + heading => 'You Are QA Contact', + description => 'You are the qa contact on this bug and it is not resolved or closed yet.', + params => { + 'bug_status' => ['__open__'], + 'emailqa_contact1' => 1, + 'emailtype1' => 'exact', + 'email1' => $user->login + } + }); + } + + return @query_defs; +} + +################ +# Installation # +################ + +sub db_schema_abstract_schema { + my ($self, $args) = @_; + + my $schema = $args->{schema}; + + $schema->{'mydashboard'} = { + FIELDS => [ + namedquery_id => {TYPE => 'INT3', NOTNULL => 1, + REFERENCES => {TABLE => 'namedqueries', + COLUMN => 'id', + DELETE => 'CASCADE'}}, + user_id => {TYPE => 'INT3', NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', + COLUMN => 'userid', + DELETE => 'CASCADE'}}, + ], + INDEXES => [ + mydashboard_namedquery_id_idx => {FIELDS => [qw(namedquery_id user_id)], + TYPE => 'UNIQUE'}, + mydashboard_user_id_idx => ['user_id'], + ], + }; +} + +########### +# Objects # +########### + +BEGIN { + *Bugzilla::Search::Saved::in_mydashboard = \&_in_mydashboard; +} + +sub _in_mydashboard { + my ($self) = @_; + my $dbh = Bugzilla->dbh; + return $self->{'in_mydashboard'} if exists $self->{'in_mydashboard'}; + $self->{'in_mydashboard'} = $dbh->selectrow_array(" + SELECT 1 FROM mydashboard WHERE namedquery_id = ? AND user_id = ?", + undef, $self->id, $self->user->id); + return $self->{'in_mydashboard'}; +} + +############# +# Templates # +############# + +sub page_before_template { + my ($self, $args) = @_; + my $page = $args->{'page_id'}; + my $vars = $args->{'vars'}; + + return if $page ne 'mydashboard.html'; + + # If we're using bug groups to restrict bug entry, we need to know who the + # user is right from the start. + my $user = Bugzilla->login(LOGIN_REQUIRED); + + # Switch to shadow db since we are just reading information + Bugzilla->switch_to_shadow_db(); + + _active_product_counts($vars); + _standard_saved_queries($vars); + _flags_requested($vars); + + $vars->{'severities'} = get_legal_field_values('bug_severity'); +} + +our $_open_states; +sub _open_states { + $_open_states ||= Bugzilla::Status->match({ is_open => 1, isactive => 1 }); + return wantarray ? @$_open_states : $_open_states; +} + +our $_quoted_open_states; +sub _quoted_open_states { + my $dbh = Bugzilla->dbh; + $_quoted_open_states ||= [ map { $dbh->quote($_->name) } _open_states() ]; + return wantarray ? @$_quoted_open_states : $_quoted_open_states; +} + +sub _active_product_counts { + my ($vars) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + my @enterable_products = @{$user->get_enterable_products()}; + $vars->{'products'} + = $dbh->selectall_arrayref("SELECT products.name AS product, count(*) AS count + FROM bugs,products + WHERE bugs.product_id=products.id + AND products.isactive = 1 + AND bugs.bug_status IN (" . join(',', _quoted_open_states()) . ") + AND products.id IN (" . join(',', map { $_->id } @enterable_products) . ") + GROUP BY products.name ORDER BY count DESC", { Slice => {} }); + + $vars->{'products_buffer'} = "&" . join('&', map { "bug_status=" . $_->name } _open_states()); +} + +sub _standard_saved_queries { + my ($vars) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + # Default sort order + my $order = ["bug_id"]; + + # List of columns that we will be selecting. In the future this should be configurable + # Share with buglist.cgi? + my @select_columns = ('bug_id','product','bug_status','bug_severity','version', 'component','short_desc', 'changeddate'); + + # Define the columns that can be selected in a query + my $columns = Bugzilla::Search::COLUMNS; + + # Weed out columns that don't actually exist and detaint along the way. + @select_columns = grep($columns->{$_} && trick_taint($_), @select_columns); + + ### Standard query definitions + my @query_defs = QUERY_DEFS; + + ### Saved query definitions + ### These are enabled through the userprefs.cgi UI + foreach my $q (@{$user->queries}) { + next if !$q->in_mydashboard; + push(@query_defs, { name => $q->name, + heading => $q->name, + saved => 1, + params => $q->url }); + } + + my $date_now = DateTime->now(time_zone => Bugzilla->local_timezone); + + ### Collect the query results for display in the template + + my @results; + foreach my $qdef (@query_defs) { + my $params = new Bugzilla::CGI($qdef->{params}); + + my $search = new Bugzilla::Search( fields => \@select_columns, + params => $params, + order => $order ); + my $query = $search->sql(); + + my $sth = $dbh->prepare($query); + $sth->execute(); + + my $rows = $sth->fetchall_arrayref(); + + my @bugs; + foreach my $row (@$rows) { + my $bug = {}; + foreach my $column (@select_columns) { + $bug->{$column} = shift @$row; + if ($column eq 'changeddate') { + my $date_then = datetime_from($bug->{$column}); + $bug->{'updated'} = time_ago($date_then, $date_now); + } + } + push(@bugs, $bug); + } + + $qdef->{bugs} = \@bugs; + $qdef->{buffer} = $params->canonicalise_query(); + + push(@results, $qdef); + } + + $vars->{'results'} = \@results; +} + +sub _flags_requested { + my ($vars) = @_; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + + my $attach_join_clause = "flags.attach_id = attachments.attach_id"; + if (Bugzilla->params->{insidergroup} && !$user->in_group(Bugzilla->params->{insidergroup})) { + $attach_join_clause .= " AND attachments.isprivate < 1"; + } + + my $query = + # Select columns describing each flag, the bug/attachment on which + # it has been set, who set it, and of whom they are requesting it. + " SELECT flags.id AS id, + flagtypes.name AS type, + flags.status AS status, + flags.bug_id AS bug_id, + bugs.short_desc AS bug_summary, + flags.attach_id AS attach_id, + attachments.description AS attach_summary, + requesters.realname AS requester, + requestees.realname AS requestee, + " . $dbh->sql_date_format('flags.creation_date', '%Y.%m.%d %H:%i') . " AS created + FROM flags + LEFT JOIN attachments + ON ($attach_join_clause) + INNER JOIN flagtypes + ON flags.type_id = flagtypes.id + INNER JOIN bugs + ON flags.bug_id = bugs.bug_id + LEFT JOIN profiles AS requesters + ON flags.setter_id = requesters.userid + LEFT JOIN profiles AS requestees + ON flags.requestee_id = requestees.userid + LEFT JOIN bug_group_map AS bgmap + ON bgmap.bug_id = bugs.bug_id + LEFT JOIN cc AS ccmap + ON ccmap.who = " . $user->id . " + AND ccmap.bug_id = bugs.bug_id "; + + # Limit query to pending requests and open bugs only + $query .= " WHERE bugs.bug_status IN (" . join(',', _quoted_open_states()) . ") + AND flags.status = '?' "; + + # Weed out bug the user does not have access to + $query .= " AND ((bgmap.group_id IS NULL) + OR bgmap.group_id IN (" . $user->groups_as_string . ") + OR (ccmap.who IS NOT NULL AND cclist_accessible = 1) + OR (bugs.reporter = " . $user->id . " AND bugs.reporter_accessible = 1) + OR (bugs.assigned_to = " . $user->id .") "; + if (Bugzilla->params->{useqacontact}) { + $query .= " OR (bugs.qa_contact = " . $user->id . ") "; + } + $query .= ") "; + + # Order the records (within each group). + my $group_order_by = " GROUP BY flags.bug_id ORDER BY flagtypes.name, flags.creation_date"; + + my $requestee_list = $dbh->selectall_arrayref($query . + " AND requestees.login_name = ? " . + $group_order_by, + { Slice => {} }, $user->login); + $vars->{'requestee_list'} = $requestee_list; + my $requester_list = $dbh->selectall_arrayref($query . + " AND requesters.login_name = ? " . + $group_order_by, + { Slice => {} }, $user->login); + $vars->{'requester_list'} = $requester_list; +} + +######### +# Hooks # +######### + +sub user_preferences { + my ($self, $args) = @_; + my $tab = $args->{'current_tab'}; + return unless $tab eq 'saved-searches'; + + my $save = $args->{'save_changes'}; + my $handled = $args->{'handled'}; + my $vars = $args->{'vars'}; + + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my $params = Bugzilla->input_params; + + if ($save) { + my $sth_insert_fp = $dbh->prepare('INSERT INTO mydashboard + (namedquery_id, user_id) + VALUES (?, ?)'); + my $sth_delete_fp = $dbh->prepare('DELETE FROM mydashboard + WHERE namedquery_id = ? + AND user_id = ?'); + foreach my $q (@{$user->queries}, @{$user->queries_available}) { + if (defined $params->{'in_mydashboard_' . $q->id}) { + $sth_insert_fp->execute($q->id, $q->user->id) if !$q->in_mydashboard; + } + else { + $sth_delete_fp->execute($q->id, $q->user->id) if $q->in_mydashboard; + } + } + } +} + +sub webservice { + my ($self, $args) = @_; + my $dispatch = $args->{dispatch}; + $dispatch->{MyDashboard} = "Bugzilla::Extension::MyDashboard::WebService"; +} + +__PACKAGE__->NAME; diff --git a/extensions/MyDashboard/lib/TimeAgo.pm b/extensions/MyDashboard/lib/TimeAgo.pm new file mode 100644 index 000000000..f213986d6 --- /dev/null +++ b/extensions/MyDashboard/lib/TimeAgo.pm @@ -0,0 +1,182 @@ +package Bugzilla::Extension::MyDashboard::TimeAgo; + +use strict; +use utf8; +use DateTime; +use Carp; +use Exporter qw(import); + +use if $ENV{ARCH_64BIT}, 'integer'; + +our @EXPORT_OK = qw(time_ago); + +our $VERSION = '0.06'; + +my @ranges = ( + [ -1, 'in the future' ], + [ 60, 'just now' ], + [ 900, 'a few minutes ago'], # 15*60 + [ 3000, 'less than an hour ago'], # 50*60 + [ 4500, 'about an hour ago'], # 75*60 + [ 7200, 'more than an hour ago'], # 2*60*60 + [ 21600, 'several hours ago'], # 6*60*60 + [ 86400, 'today', sub { # 24*60*60 + my $time = shift; + my $now = shift; + if ( $time->day < $now->day + or $time->month < $now->month + or $time->year < $now->year + ) { + return 'yesterday' + } + if ($time->hour < 5) { + return 'tonight' + } + if ($time->hour < 10) { + return 'this morning' + } + if ($time->hour < 15) { + return 'today' + } + if ($time->hour < 19) { + return 'this afternoon' + } + return 'this evening' + }], + [ 172800, 'yesterday'], # 2*24*60*60 + [ 604800, 'this week'], # 7*24*60*60 + [ 1209600, 'last week'], # 2*7*24*60*60 + [ 2678400, 'this month', sub { # 31*24*60*60 + my $time = shift; + my $now = shift; + if ($time->year == $now->year and $time->month == $now->month) { + return 'this month' + } + return 'last month' + }], + [ 5356800, 'last month'], # 2*31*24*60*60 + [ 24105600, 'several months ago'], # 9*31*24*60*60 + [ 31536000, 'about a year ago'], # 365*24*60*60 + [ 34214400, 'last year'], # (365+31)*24*60*60 + [ 63072000, 'more than a year ago'], # 2*365*24*60*60 + [ 283824000, 'several years ago'], # 9*365*24*60*60 + [ 315360000, 'about a decade ago'], # 10*365*24*60*60 + [ 630720000, 'last decade'], # 20*365*24*60*60 + [ 2838240000, 'several decades ago'], # 90*365*24*60*60 + [ 3153600000, 'about a century ago'], # 100*365*24*60*60 + [ 6307200000, 'last century'], # 200*365*24*60*60 + [ 6622560000, 'more than a century ago'], # 210*365*24*60*60 + [ 28382400000, 'several centuries ago'], # 900*365*24*60*60 + [ 31536000000, 'about a millenium ago'], # 1000*365*24*60*60 + [ 63072000000, 'more than a millenium ago'], # 2000*365*24*60*60 +); + +sub time_ago { + my ($time, $now) = @_; + + if (not defined $time or not $time->isa('DateTime')) { + croak('DateTime::Duration::Fuzzy::time_ago needs a DateTime object as first parameter') + } + if (not defined $now) { + $now = DateTime->now(); + } + if (not $now->isa('DateTime')) { + croak('Invalid second parameter provided to DateTime::Duration::Fuzzy::time_ago; it must be a DateTime object if provided') + } + + # Use clones in UTC for safe date calculation + my $now_clone = $now->clone->set_time_zone('UTC'); + my $time_clone = $time->clone->set_time_zone('UTC'); + my $dur = $now_clone->subtract_datetime_absolute( $time_clone )->in_units('seconds'); + + foreach my $range ( @ranges ) { + if ( $dur <= $range->[0] ) { + if ( $range->[2] ) { + return $range->[2]->( $time_clone, $now_clone ) + } + return $range->[1] + } + } + + return 'millenia ago' +} + +1 + +__END__ + +=head1 NAME + +DateTime::Duration::Fuzzy -- express dates as fuzzy human-friendly strings + +=head1 SYNOPSIS + + use DateTime::Duration::Fuzzy qw(time_ago); + use DateTime; + + my $now = DateTime->new( + year => 2010, month => 12, day => 12, + hour => 19, minute => 59, + ); + my $then = DateTime->new( + year => 2010, month => 12, day => 12, + hour => 15, + ); + print time_ago($then, $now); + # outputs 'several hours ago' + + print time_ago($then); + # $now taken from C<time> function + +=head1 DESCRIPTION + +DateTime::Duration::Fuzzy is inspired from the timeAgo jQuery module +L<http://timeago.yarp.com/>. + +It takes two DateTime objects -- first one representing a moment in the past +and second optional one representine the present, and returns a human-friendly +fuzzy expression of the time gone. + +=head2 functions + +=over 4 + +=item time_ago($then, $now) + +The only exportable function. + +First obligatory parameter is a DateTime object. + +Second optional parameter is also a DateTime object. +If it's not provided, then I<now> as the C<time> function returns is +substituted. + +Returns a string expression of the interval between the two DateTime +objects, like C<several hours ago>, C<yesterday> or <last century>. + +=back + +=head2 performance + +On 64bit machines, it is asvisable to 'use integer', which makes +the calculations faster. You can turn this on by setting the +C<ARCH_64BIT> environmental variable to a true value. + +If you do this on a 32bit machine, you will get wrong results for +intervals starting with "several decades ago". + +=head1 AUTHOR + +Jan Oldrich Kruza, C<< <sixtease at cpan.org> >> + +=head1 LICENSE AND COPYRIGHT + +Copyright 2010 Jan Oldrich Kruza. + +This program is free software; you can redistribute it and/or modify it +under the terms of either: the GNU General Public License as published +by the Free Software Foundation; or the Artistic License. + +See http://dev.perl.org/licenses/ for more information. + +=cut diff --git a/extensions/MyDashboard/lib/WebService.pm b/extensions/MyDashboard/lib/WebService.pm new file mode 100644 index 000000000..78285ca06 --- /dev/null +++ b/extensions/MyDashboard/lib/WebService.pm @@ -0,0 +1,98 @@ +# 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::MyDashboard::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 => {} }); + + return { products => $products }; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::Extension::MyDashboard::Webservice - The MyDashboard WebServices API + +=head1 DESCRIPTION + +This module contains API methods that are useful to user's of bugzilla.mozilla.org. + +=head1 METHODS + +See L<Bugzilla::WebService> for a description of how parameters are passed, +and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean. diff --git a/extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-header.html.tmpl b/extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-header.html.tmpl new file mode 100644 index 000000000..c822ab040 --- /dev/null +++ b/extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-header.html.tmpl @@ -0,0 +1,11 @@ +[%# 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. + #%] + +<th> + My Dashboard +</th> diff --git a/extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-row.html.tmpl b/extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-row.html.tmpl new file mode 100644 index 000000000..cd6a36705 --- /dev/null +++ b/extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-row.html.tmpl @@ -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. + #%] + +<td align="center"> + <input type="checkbox" + name="in_mydashboard_[% q.id FILTER html %]" + value="1" + alt="[% q.name FILTER html %]" + [% " checked" IF q.in_mydashboard %]> +</td> diff --git a/extensions/MyDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl b/extensions/MyDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl new file mode 100644 index 000000000..d97c0a4ac --- /dev/null +++ b/extensions/MyDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl @@ -0,0 +1,11 @@ +[%# 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. + #%] + +[% IF user.login %] + <li><span class="separator"> | </span><a href="[% urlbase %]page.cgi?id=mydashboard.html">My Dashboard</a></li> +[% END %] diff --git a/extensions/MyDashboard/template/en/default/mydashboard/prod-comp-search.html.tmpl b/extensions/MyDashboard/template/en/default/mydashboard/prod-comp-search.html.tmpl new file mode 100644 index 000000000..b7e4d990c --- /dev/null +++ b/extensions/MyDashboard/template/en/default/mydashboard/prod-comp-search.html.tmpl @@ -0,0 +1,43 @@ +[%# 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_main"> + <div id="prod_comp_search_autocomplete"> + <div id="prod_comp_search_label"> + File Bug: + <img id="prod_comp_throbber" src="extensions/BMO/web/images/throbber.gif" + class="hidden" width="16" height="11"> + </div> + <input id="prod_comp_search" type="text" size="60"> + <div id="prod_comp_search_autocomplete_container"></div> + </div> +</div> +<script type="text/javascript"> + if(typeof(YAHOO.bugzilla.prodCompSearch) !== 'undefined' + && YAHOO.bugzilla.prodCompSearch != null) + { + YAHOO.bugzilla.prodCompSearch.init( + "prod_comp_search", + "prod_comp_search_autocomplete_container", + "[% format FILTER js %]", + "[% cloned_bug_id FILTER js %]"); + [% IF target == "describecomponents.cgi" %] + YAHOO.bugzilla.prodCompSearch.autoComplete.itemSelectEvent.subscribe(function (e, args) { + var oData = args[2]; + var url = "describecomponents.cgi?product=" + encodeURIComponent(oData[0]) + + "&component=" + encodeURIComponent(oData[1]) + + "#" + encodeURIComponent(oData[1]); + var format = YAHOO.bugzilla.prodCompSearch.format; + if (format) { + url += "&format=" + encodeURIComponent(format); + } + window.location.href = url; + }); + [% END %] + } +</script> diff --git a/extensions/MyDashboard/template/en/default/pages/mydashboard.html.tmpl b/extensions/MyDashboard/template/en/default/pages/mydashboard.html.tmpl new file mode 100644 index 000000000..23c478e3f --- /dev/null +++ b/extensions/MyDashboard/template/en/default/pages/mydashboard.html.tmpl @@ -0,0 +1,289 @@ +[%# 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 = "My Dashboard" + style_urls = [ "skins/standard/buglist.css", + "js/yui/assets/skins/sam/paginator.css", + "extensions/MyDashboard/web/styles/mydashboard.css", + "extensions/MyDashboard/web/styles/prod_comp_search.css" ] + yui = [ "datatable", "paginator", "autocomplete" ] + javascript_urls = [ "extensions/MyDashboard/web/js/mydashboard.js", + "extensions/MyDashboard/web/js/prod_comp_search.js" ] + onload = "showQuerySection();" +%] + +<script type="text/javascript"> +<!-- + [%# Set up severities list for proper sorting %] + var severities = new Array(); + [% sort_count = 0 %] + [% FOREACH s = severities %] + severities['[% s FILTER js %]'] = [% sort_count FILTER js %]; + [% sort_count = sort_count + 1 %] + [% END %] + + var full_query_list = []; + [% FOREACH r = results %] + full_query_list.push('[% r.name FILTER js %]'); + [% END %] +--> +</script> + +[% standard_results = [] %] +[% saved_results = [] %] +[% FOREACH r = results %] + [% standard_results.push(r) IF !r.saved %] + [% saved_results.push(r) IF r.saved %] +[% END %] + +<div id="mydashboard"> + <div class="yui-skin-sam"> + <div id="left"> + <div id="query_list_container"> + <strong>Choose query:</strong> + <select id="query" name="query" onchange="showQuerySection();"> + <optgroup id="standard_queries" label="Standard"> + [% FOREACH r = standard_results %] + <option value="[% r.name FILTER html %]">[% r.heading FILTER html %]</option> + [% END%] + </optgroup> + <optgroup id="saved_queries" label="Saved"> + [% FOREACH r = saved_results %] + <option value="[% r.name FILTER html %]">[% r.heading FILTER html %]</option> + [% END %] + </optgroup> + </select> + [% IF NOT saved_results.size %] + <smaller> + (<a href="userprefs.cgi?tab=saved-searches">add or remove saved searches</a>) + </smaller> + [% END %] + </div> + + [% FOREACH r = standard_results %] + [% PROCESS query_results r = r %] + [% END %] + + [% FOREACH r = saved_results %] + [% PROCESS query_results r = r %] + [% END %] + </div> + + <div id="right"> + <div id="file_bug_container"> + [% PROCESS "mydashboard/prod-comp-search.html.tmpl" %] + </div> + + <div id="requestee_container"> + <div class="query_heading"> + Flags Requested of You + </div> + <span class="flags_found"> + [% requestee_list.size FILTER html %] flags found + </span> + <div id="requestee_table_container"> + <table id="requestee_table" cellspacing="0" cellpadding="3" width="100%"> + <thead> + <tr bgcolor="#dedede"> + <th>Requester</th> + <th>Flag</th> + <th>[% terms.Bug %]</th> + <th>Created</th> + </tr> + </thead> + <tbody> + [% FOREACH request = requestee_list %] + <tr class="bz_bugitem [%+ loop.count() % 2 == 0 ? "bz_row_odd" : "bz_row_even" %]"> + <td>[% request.requester FILTER html %]</td> + <td>[% request.type FILTER html %][% request.status FILTER html %]</td> + <td> + [% IF request.attach_id %] + <a href="[% urlbase FILTER none %]attachment.cgi?action=edit&id=[% request.attach_id FILTER uri %]"> + [% request.attach_id FILTER html %]: [%+ request.attach_summary FILTER html %]</a> + [% ELSE %] + <a href="[% urlbase FILTER none %]show_bug.cgi?id=[% request.bug_id FILTER uri %]"> + [% request.bug_id FILTER html %]: [%+ request.bug_summary FILTER html %]</a> + [% END %] + </td> + <td>[% request.created FILTER time('%Y.%m.%d') FILTER html %]</td> + </tr> + [% END %] + </tbody> + </table> + </div> + </div> + <script> + <!-- + var requestee_column_defs = [ + { key:"requester", label:"Requester", sortable:true }, + { key:"flag", label:"Flag", sortable:true }, + { key:"bug", label:"Bug", sortable:true }, + { key:"created", label:"Created", sortable:true } + ]; + var requestee_fields = [ + { key:"requester" }, + { key:"flag" }, + { key:"bug" }, + { key:"created" } + ]; + addStatListener("requestee_table_container", "requestee_table", requestee_column_defs, requestee_fields, { + paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25, alwaysVisible: false }) + }); + --> + </script> + + <div id="requester_container"> + <div class="query_heading"> + Flags You Have Requested + </div> + <span class="flags_found"> + [% requester_list.size FILTER html %] flags found + </span> + <div id="requester_table_container"> + <table id="requester_table" cellspacing="0" cellpadding="3" width="100%"> + <thead bgcolor="#dedede"> + <tr> + <th>Requestee</th> + <th>Flag</th> + <th>[% terms.Bug %]</th> + <th>Created</th> + </tr> + </thead> + <tbody> + [% FOREACH request = requester_list %] + <tr class="bz_bugitem [%+ loop.count() % 2 == 0 ? "bz_row_odd" : "bz_row_even" %]"> + <td>[% request.requestee FILTER html %]</td> + <td>[% request.type FILTER html %][% request.status FILTER html %]</td> + <td> + [% IF request.attach_id %] + <a href="[% urlbase FILTER none %]attachment.cgi?action=edit&id=[% request.attach_id FILTER uri %]"> + [% request.attach_id FILTER html %]: [%+ request.attach_summary FILTER html %]</a> + [% ELSE %] + <a href="[% urlbase FILTER none %]show_bug.cgi?id=[% request.bug_id FILTER uri %]"> + [% request.bug_id FILTER html %]: [%+ request.bug_summary FILTER html %]</a> + [% END %] + </td> + <td>[% request.created FILTER time('%Y.%m.%d') FILTER html %]</td> + </tr> + [% END %] + </tbody> + </table> + </div> + </div> + <script> + <!-- + var requester_column_defs = [ + { key:"requestee", label:"Requestee", sortable:true }, + { key:"flag", label:"Flag", sortable:true }, + { key:"bug", label:"Bug", sortable:true }, + { key:"created", label:"Created", sortable:true } + ]; + var requester_fields = [ + { key:"requestee" }, + { key:"flag" }, + { key:"bug" }, + { key:"created" } + ]; + addStatListener("requester_table_container", "requester_table", requester_column_defs, requester_fields, { + paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25, alwaysVisible: false }) + }); + --> + </script> + + <div id="activeproducts"> + <div class="query_heading"> + Products with Open [% terms.Bugs %] + </div> + <div id="activeproducts_table_container"> + <table id="activeproducts_table" cellspacing="0" cellpadding="4" width="100%"> + <thead> + <tr bgcolor="#dedede"> + <th>Count</th> + <th>Product</th> + </tr> + </thead> + <tbody> + [% FOREACH product = products %] + <tr class="bz_bugitem [%+ loop.count() % 2 == 0 ? "bz_row_odd" : "bz_row_even" %]"> + <td align="right">[% product.count FILTER html %]</td> + <td><a href="buglist.cgi?product=[% product.product FILTER uri %][% products_buffer FILTER none %]"> + [% product.product FILTER html %]</a></td> + </tr> + [% END %] + </tbody> + </table> + </div> + </div> + <script> + <!-- + var product_column_defs = [ + { key:"count", label:"Count", sortable:true }, + { key:"product", label:"Product", sortable:true } + ]; + var product_fields = [ + { key:"count", parser:"number" }, + { key:"product" } + ]; + addStatListener("activeproducts_table_container", "activeproducts_table", product_column_defs, product_fields, { + paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25, alwaysVisible: false }) + }); + --> + </script> + + </div> + <div style="clear:both;"></div> + </div> +</div> + +[% PROCESS global/footer.html.tmpl %] + +[% BLOCK query_results %] + <div id="[% r.name FILTER html %]_container" class="bz_default_hidden"> + [% IF r.description %] + <div class="query_description"> + [% r.description FILTER html %] + </div> + [% END %] + <span class="bugs_found"> + <a href="[% urlbase FILTER none %]buglist.cgi?[% r.buffer FILTER none %]"> + [% r.bugs.size FILTER html %] [% terms.bugs %] found</a> + </span> + <div id="[% r.name FILTER html %]_table_container"> + <table id="[% r.name FILTER html %]_table" cellspacing="0" cellpadding="3" width="100%"> + <thead> + <tr> + <th>ID</th> + <th>Updated</th> + <th>Status</th> + <th>Summary</th> + </tr> + </thead> + <tbody> + [% FOREACH bug = r.bugs %] + <tr class="bz_bugitem [%+ loop.count() % 2 == 0 ? "bz_row_odd" : "bz_row_even" %]"> + <td align="center"><a href="show_bug.cgi?id=[% bug.bug_id FILTER uri %]">[% bug.bug_id FILTER html %]</a></td> + <td align="center">[% bug.updated FILTER html %]</td> + <td align="center">[% bug.bug_status FILTER html %]</td> + <td>[% bug.short_desc FILTER html %]</td> + </tr> + [% END %] + </tbody> + </table> + </div> + <script> + <!-- + addStatListener("[% r.name FILTER js %]_table_container", "[% r.name FILTER js %]_table", query_column_defs, query_fields, { + paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25, alwaysVisible: false }) + }); + --> + </script> + </div> +[% END %] diff --git a/extensions/MyDashboard/template/en/default/pages/mydashboard_old.html.tmpl b/extensions/MyDashboard/template/en/default/pages/mydashboard_old.html.tmpl new file mode 100644 index 000000000..b7f34a183 --- /dev/null +++ b/extensions/MyDashboard/template/en/default/pages/mydashboard_old.html.tmpl @@ -0,0 +1,326 @@ +[%# 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 = "My Dashboard" + style_urls = [ "skins/standard/buglist.css", + "js/yui/assets/skins/sam/paginator.css", + "extensions/MyDashboard/web/styles/mydashboard.css" ] + yui = [ "datatable", "paginator" ] + javascript_urls = [ "extensions/MyDashboard/web/js/mydashboard.js" ] +%] + +<script type="text/javascript"> +<!-- + [%# Set up severities list for proper sorting %] + var severities = new Array(); + [% sort_count = 0 %] + [% FOREACH s = severities %] + severities['[% s FILTER js %]'] = [% sort_count FILTER js %]; + [% sort_count = sort_count + 1 %] + [% END %] +--> +</script> + +[% standard_results = [] %] +[% saved_results = [] %] +[% FOREACH r = results %] + [% standard_results.push(r) IF !r.saved %] + [% saved_results.push(r) IF r.saved %] +[% END %] + +<a name="top"></a> +<div id="mydashboard"> + <div class="yui-skin-sam"> + <ul id="query-links"> + <li id="links-standard"> + <div class="label">Standard Queries:</div> + <ul class="links"> + [% FOREACH r = standard_results %] + <li> + <a href="#[% r.name FILTER uri %]">[% r.heading FILTER html %]</a></li> + <span class="separator">| </span> + </li> + [% END%] + <li> + <a href="#requestee">Open Issues with Flags Requested of You</a> + <span class="separator"> | </span> + </li> + <li> + <a href="#requester">Open Issues with Flags You Have Requested</a> + <span class="separator"> | </span> + </li> + <li> + <a href="#products">Active Products with Open Issues</a> + </li> + </ul> + </li> + <li id="links-saved"> + <div class="label">Saved Queries + (<a title="Click to add/remove saved searches from my dashboard" + href="[% urlbase FILTER none %]userprefs.cgi?tab=saved-searches">edit</a>):</div> + <ul class="links"> + [% FOREACH r = saved_results %] + <li> + <a href="#[% r.name FILTER uri %]">[% r.heading FILTER html %]</a> + [% '<span class="separator"> | </span>' IF !loop.last() %] + </li> + [% END %] + [% IF NOT saved_results.size %] + <li>Click edit to add or remove saved searches from my dashboard</li> + [% END %] + </ul> + </li> + </ul> + + <hr> + + <script type="text/javascript"> + <!-- + var query_column_defs = [ + { key:"id", label:"ID", sortable:true, sortOptions:{ sortFunction:sortBugIdLinks } }, + { key:"product", label:"Product", sortable:true }, + { key:"version", label:"Version", sortable:true }, + { key:"component", label:"Component", sortable:true }, + { key:"bug_status", label:"Status", sortable:true }, + { key:"bug_severity", label:"Severity", sortable:true, sortOptions:{ sortFunction:sortBugSeverity } }, + { key:"summary", label:"Summary", sortable:true }, + ]; + var query_fields = [ + { key:"id" }, + { key:"product" }, + { key:"version" }, + { key:"component" }, + { key:"bug_status" }, + { key:"bug_severity" }, + { key:"summary" } + ]; + var requester_column_defs = [ + { key:"requestee", label:"Requestee", sortable:true }, + { key:"flag", label:"Flag", sortable:true }, + { key:"bug", label:"Bug", sortable:true }, + { key:"created", label:"Created", sortable:true } + ]; + var requester_fields = [ + { key:"requestee" }, + { key:"flag" }, + { key:"bug" }, + { key:"created" } + ]; + var requestee_column_defs = [ + { key:"requester", label:"Requester", sortable:true }, + { key:"flag", label:"Flag", sortable:true }, + { key:"bug", label:"Bug", sortable:true }, + { key:"created", label:"Created", sortable:true } + ]; + var requestee_fields = [ + { key:"requester" }, + { key:"flag" }, + { key:"bug" }, + { key:"created" } + ]; + var product_column_defs = [ + { key:"count", label:"Count", sortable:true }, + { key:"product", label:"Product", sortable:true } + ]; + var product_fields = [ + { key:"count", parser:"number" }, + { key:"product" } + ]; + addStatListener("requestee_container", "requestee_table", requestee_column_defs, requestee_fields, { + paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25, alwaysVisible: false }) + }); + addStatListener("requester_container", "requester_table", requester_column_defs, requester_fields, { + paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25, alwaysVisible: false }) + }); + addStatListener("activeproducts", "activeproducts_table", product_column_defs, product_fields, { + paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25, alwaysVisible: false }) + }); + --> + </script> + + [% FOREACH r = standard_results %] + [% PROCESS query_results r = r %] + [% END %] + + <div class="query_heading"> + <a name="requestee">Open Issues with Flags Requested of You</a> + </div> + <span class="back_top"> + (<a href="#top">back to top</a>) + </span> + <br> + <div id="requestee_container"> + <table id="requestee_table" cellspacing="0" cellpadding="3" width="100%"> + <thead> + <tr bgcolor="#dedede"> + <th>Requester</th> + <th>Flag</th> + <th>[% terms.Bug %]</th> + <th>Created</th> + </tr> + </thead> + <tbody> + [% FOREACH request = requestee_list %] + <tr class="bz_bugitem [%+ loop.count() % 2 == 0 ? "bz_row_odd" : "bz_row_even" %]"> + <td>[% request.requester FILTER html %]</td> + <td>[% request.type FILTER html %][% request.status FILTER html %]</td> + <td> + [% IF request.attach_id %] + <a href="[% urlbase FILTER none %]attachment.cgi?action=edit&id=[% request.attach_id FILTER uri %]"> + [% request.attach_id FILTER html %]: [%+ request.attach_summary FILTER html %]</a> + + [% ELSE %] + <a href="[% urlbase FILTER none %]show_bug.cgi?id=[% request.bug_id FILTER uri %]"> + [% request.bug_id FILTER html %]: [%+ request.bug_summary FILTER html %]</a> + [% END %] + </td> + <td>[% request.created FILTER html %]</td> + </tr> + [% END %] + </tbody> + </table> + </div> + + <hr> + + <div class="query_heading"> + <a name="requester">Open Issues with Flags You Have Requested</a> + </div> + <span class="back_top"> + (<a href="#top">back to top</a>) + </span> + <div id="requester_container"> + <table id="requester_table" cellspacing="0" cellpadding="3" width="100%"> + <thead bgcolor="#dedede"> + <tr> + <th>Requestee</th> + <th>Flag</th> + <th>[% terms.Bug %]</th> + <th>Created</th> + </tr> + </thead> + <tbody> + [% FOREACH request = requester_list %] + <tr class="bz_bugitem [%+ loop.count() % 2 == 0 ? "bz_row_odd" : "bz_row_even" %]"> + <td>[% request.requestee FILTER html %]</td> + <td>[% request.type FILTER html %][% request.status FILTER html %]</td> + <td> + [% IF request.attach_id %] + <a href="[% urlbase FILTER none %]attachment.cgi?action=edit&id=[% request.attach_id FILTER uri %]"> + [% request.attach_id FILTER html %]: [%+ request.attach_summary FILTER html %]</a> + [% ELSE %] + <a href="[% urlbase FILTER none %]show_bug.cgi?id=[% request.bug_id FILTER uri %]"> + [% request.bug_id FILTER html %]: [%+ request.bug_summary FILTER html %]</a> + [% END %] + </td> + <td>[% request.created FILTER html %]</td> + </tr> + [% END %] + </table> + </tbody> + </div> + + <hr> + + <div class="query_heading"> + <a name="products">Active Products with Open [% terms.Bugs %]</a> + </div> + <span class="back_top"> + (<a href="#top">back to top</a>) + </span> + <br> + <div id="activeproducts"> + <table id="activeproducts_table" cellspacing="0" cellpadding="4" width="100%"> + <thead> + <tr bgcolor="#dedede"> + <th>Count</th> + <th>Product</th> + </tr> + </thead> + <tbody> + [% FOREACH product = products %] + <tr class="bz_bugitem [%+ loop.count() % 2 == 0 ? "bz_row_odd" : "bz_row_even" %]"> + <td align="right">[% product.count FILTER html %]</td> + <td><a href="buglist.cgi?product=[% product.product FILTER uri %][% products_buffer FILTER none %]"> + [% product.product FILTER html %]</a></td> + </tr> + [% END %] + </tbody> + </table> + </div> + + <hr> + + [% FOREACH r = saved_results %] + [% PROCESS query_results r = r %] + [% END %] + </div> +</div> + +[% PROCESS global/footer.html.tmpl %] + +[% BLOCK query_results %] + <script> + <!-- + addStatListener("[% r.name FILTER js %]_container", "[% r.name FILTER js %]_table", query_column_defs, query_fields, { + paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25, alwaysVisible: false }) + }); + --> + </script> + + <div class="query_heading"> + <a name="[% r.name FILTER uri %]">[% r.heading FILTER html %]</a> + </div> + [% IF r.description %] + <div class="query_description"> + [% r.description FILTER html %] + </div> + [% END %] + <span class="bugs_found"> + [% r.bugs.size FILTER html %] [% terms.bugs %] found. + </span> + <span class="bug_list"> + (<a href="[% urlbase FILTER none %]buglist.cgi?[% r.buffer FILTER none %]">show list</a>) + </span> + <span class="back_top"> + (<a href="#top">back to top</a>) + </span> + <br> + <div id="[% r.name FILTER html %]_container"> + <table id="[% r.name FILTER html %]_table" cellspacing="0" cellpadding="3" width="100%"> + <thead> + <tr> + <th>ID</th> + <th>Product</th> + <th>Version</th> + <th>Component</th> + <th>Status</th> + <th>Severity</th> + <th>Summary</th> + </tr> + </thead> + <tbody> + [% FOREACH bug = r.bugs %] + <tr class="bz_bugitem [%+ loop.count() % 2 == 0 ? "bz_row_odd" : "bz_row_even" %]"> + <td align="center"><a href="show_bug.cgi?id=[% bug.bug_id FILTER uri %]">[% bug.bug_id FILTER html %]</a></td> + <td><a href="buglist.cgi?product=[% bug.product FILTER uri %][% products_buffer FILTER none %]">[% bug.product FILTER html %]</a></td> + <td>[% bug.version FILTER html %]</td> + <td>[% bug.component FILTER html %]</td> + <td align="center">[% bug.bug_status FILTER html %]</td> + <td align="center">[% bug.bug_severity FILTER html %]</td> + <td>[% bug.short_desc FILTER html %]</td> + </tr> + [% END %] + </tbody> + </table> + </div> + <hr> +[% END %] diff --git a/extensions/MyDashboard/web/js/mydashboard.js b/extensions/MyDashboard/web/js/mydashboard.js new file mode 100644 index 000000000..64a421113 --- /dev/null +++ b/extensions/MyDashboard/web/js/mydashboard.js @@ -0,0 +1,126 @@ +/* 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. + */ + +var showQuerySection = function () { + var query_select = YAHOO.util.Dom.get('query'); + var selected_value = ''; + for (var i = 0, l = query_select.options.length; i < l; i++) { + if (query_select.options[i].selected) { + selected_value = query_select.options[i].value; + } + } + for (var i = 0, l = full_query_list.length; i < l; i++) { + var query = full_query_list[i]; + if (selected_value == full_query_list[i]) { + YAHOO.util.Dom.removeClass(query + '_container', 'bz_default_hidden'); + } + else { + YAHOO.util.Dom.addClass(query + '_container', 'bz_default_hidden'); + } + } +} + +var query_column_defs = [ + { key:"id", label:"ID", sortable:true, sortOptions:{ sortFunction:sortBugIdLinks } }, + { key:"updated", label:"Updated", sortable:false }, + { key:"bug_status", label:"Status", sortable:true }, + { key:"summary", label:"Summary", sortable:true }, +]; +var query_fields = [ + { key:"id" }, + { key:"updated" }, + { key:"bug_status" }, + { key:"summary" } +]; + +function addStatListener (div_name, table_name, column_defs, fields, options) { + YAHOO.util.Event.addListener(window, "load", function() { + YAHOO.example.StatsFromMarkup = new function() { + this.myDataSource = new YAHOO.util.DataSource(YAHOO.util.Dom.get(table_name)); + this.myDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE; + this.myDataSource.responseSchema = { fields:fields }; + this.myDataTable = new YAHOO.widget.DataTable(div_name, column_defs, this.myDataSource, options); + this.myDataTable.subscribe("rowMouseoverEvent", this.myDataTable.onEventHighlightRow); + this.myDataTable.subscribe("rowMouseoutEvent", this.myDataTable.onEventUnhighlightRow); + }; + }); +} + +// Custom sort handler to sort by bug id inside an anchor tag +var sortBugIdLinks = function(a, b, desc) { + // Deal with empty values + if (!YAHOO.lang.isValue(a)) { + return (!YAHOO.lang.isValue(b)) ? 0 : 1; + } + else if(!YAHOO.lang.isValue(b)) { + return -1; + } + // Now we need to pull out the ID text and convert to Numbers + // First we do 'a' + var container = document.createElement("bug_id_link"); + container.innerHTML = a.getData("id"); + var anchors = container.getElementsByTagName("a"); + var text = anchors[0].textContent; + if (text === undefined) text = anchors[0].innerText; + var new_a = new Number(text); + // Then we do 'b' + container.innerHTML = b.getData("id"); + anchors = container.getElementsByTagName("a"); + text = anchors[0].textContent; + if (text == undefined) text = anchors[0].innerText; + var new_b = new Number(text); + + if (!desc) { + return YAHOO.util.Sort.compare(new_a, new_b); + } + else { + return YAHOO.util.Sort.compare(new_b, new_a); + } +} + +// Custom sort handler for bug severities +var sortBugSeverity = function(a, b, desc) { + // Deal with empty values + if (!YAHOO.lang.isValue(a)) { + return (!YAHOO.lang.isValue(b)) ? 0 : 1; + } + else if(!YAHOO.lang.isValue(b)) { + return -1; + } + + var new_a = new Number(severities[YAHOO.lang.trim(a.getData('bug_severity'))]); + var new_b = new Number(severities[YAHOO.lang.trim(b.getData('bug_severity'))]); + + if (!desc) { + return YAHOO.util.Sort.compare(new_a, new_b); + } + else { + return YAHOO.util.Sort.compare(new_b, new_a); + } +} + +// Custom sort handler for bug priorities +var sortBugPriority = function(a, b, desc) { + // Deal with empty values + if (!YAHOO.lang.isValue(a)) { + return (!YAHOO.lang.isValue(b)) ? 0 : 1; + } + else if(!YAHOO.lang.isValue(b)) { + return -1; + } + + var new_a = new Number(priorities[YAHOO.lang.trim(a.getData('priority'))]); + var new_b = new Number(priorities[YAHOO.lang.trim(b.getData('priority'))]); + + if (!desc) { + return YAHOO.util.Sort.compare(new_a, new_b); + } + else { + return YAHOO.util.Sort.compare(new_b, new_a); + } +} diff --git a/extensions/MyDashboard/web/js/prod_comp_search.js b/extensions/MyDashboard/web/js/prod_comp_search.js new file mode 100644 index 000000000..06b4c601f --- /dev/null +++ b/extensions/MyDashboard/web/js/prod_comp_search.js @@ -0,0 +1,85 @@ +/* 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. + */ + +YAHOO.bugzilla.prodCompSearch = { + counter : 0, + format : '', + cloned_bug_id : '', + dataSource : null, + autoComplete: null, + generateRequest : function (enteredText) { + YAHOO.bugzilla.prodCompSearch.counter = YAHOO.bugzilla.prodCompSearch.counter + 1; + YAHOO.util.Connect.setDefaultPostHeader('application/json', true); + var json_object = { + method : "MyDashboard.prod_comp_search", + id : YAHOO.bugzilla.prodCompSearch.counter, + params : [ { + search : decodeURIComponent(enteredText) + } ] + }; + YAHOO.util.Dom.removeClass('prod_comp_throbber', 'hidden'); + return YAHOO.lang.JSON.stringify(json_object); + }, + resultListFormat : function(oResultData, enteredText, sResultMatch) { + return YAHOO.lang.escapeHTML(oResultData[0]) + " :: " + + YAHOO.lang.escapeHTML(oResultData[1]); + }, + init_ds : function(){ + this.dataSource = new YAHOO.util.XHRDataSource("jsonrpc.cgi"); + this.dataSource.connTimeout = 30000; + this.dataSource.connMethodPost = true; + this.dataSource.connXhrMode = "cancelStaleRequests"; + this.dataSource.maxCacheEntries = 5; + this.dataSource.responseType = YAHOO.util.DataSource.TYPE_JSON; + this.dataSource.responseSchema = { + resultsList : "result.products", + metaFields : { error: "error", jsonRpcId: "id"}, + fields : [ "product", "component" ] + }; + }, + init : function(field, container, format, cloned_bug_id) { + if (this.dataSource == null) + this.init_ds(); + this.format = format; + this.cloned_bug_id = cloned_bug_id; + this.autoComplete = new YAHOO.widget.AutoComplete(field, container, this.dataSource); + this.autoComplete.generateRequest = this.generateRequest; + this.autoComplete.formatResult = this.resultListFormat; + this.autoComplete.minQueryLength = 3; + this.autoComplete.autoHighlight = false; + this.autoComplete.queryDelay = 0.05; + this.autoComplete.useIFrame = true; + this.autoComplete.maxResultsDisplayed = 25; + this.autoComplete.suppressInputUpdate = true; + this.autoComplete.doBeforeLoadData = function(sQuery, oResponse, oPayload) { + YAHOO.util.Dom.addClass('prod_comp_throbber', 'hidden'); + return true; + }; + this.autoComplete.textboxFocusEvent.subscribe(function () { + var input = YAHOO.util.Dom.get(field); + if (input.value && input.value.length > 3) { + this.sendQuery(input.value); + } + }); + this.autoComplete.itemSelectEvent.subscribe(function (e, args) { + var oData = args[2]; + var url = "enter_bug.cgi?product=" + encodeURIComponent(oData[0]) + + "&component=" + encodeURIComponent(oData[1]); + var format = YAHOO.bugzilla.prodCompSearch.format; + if (format) + url += "&format=" + encodeURIComponent(format); + var cloned_bug_id = YAHOO.bugzilla.prodCompSearch.cloned_bug_id; + if (cloned_bug_id) + url += "&cloned_bug_id=" + encodeURIComponent(cloned_bug_id); + window.location.href = url; + }); + this.autoComplete.dataReturnEvent.subscribe(function(type, args) { + args[0].autoHighlight = args[2].length == 1; + }); + } +} diff --git a/extensions/MyDashboard/web/styles/mydashboard.css b/extensions/MyDashboard/web/styles/mydashboard.css new file mode 100644 index 000000000..7000afa65 --- /dev/null +++ b/extensions/MyDashboard/web/styles/mydashboard.css @@ -0,0 +1,90 @@ +/* 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. */ + +#mydashboard .yui-skin-sam .yui-dt table { + width:100%; +} +#mydashboard #query-links { + display: table; + padding-left: 1ex; + padding-right:1ex; +} +#mydashboard #links-standard, +#mydashboard #links-saved, +#mydashboard #links-special { + display: table-row; + list-style-type: none; +} +#mydashboard .label { + display: table-cell; + white-space: nowrap; + vertical-align: top; + padding-right: 1ex; + font-weight: bold; +} +#mydashboard .links { + display: table-cell; + vertical-align: top; +} +#mydashboard .separator { + color: #000000; +} +#mydashboard .query_heading { + font-size: 18px; + font-weight: strong; + color: rgb(72, 72, 72); +} +#mydashboard .query_description { + font-size: 90%; + font-style: italic; + padding-bottom: 5px; + color: rgb(109, 117, 129); +} +#mydashboard .bug_list, +#mydashboard .back_top { + font-size: 80%; +} +#mydashboard table { + margin-bottom: 10px; + +} +#mydashboard hr { + color: #000; + background-color: #000; + border: 0; + height: 1px; + width: 100%; +} + +#mydashboard_container { + margin: 0 auto; +} + +#left { + float: left; + width: 58%; +} + +#right { + float: right; + width: 40%; +} + +#file_bug_container { + text-align: left; +} + +#query_list_container { + text-align:center; +} + +#file_bug_container, +#query_list_container { + margin-bottom: 10px; + border: 1px solid rgb(116,126,147); + padding: 10px; +} diff --git a/extensions/MyDashboard/web/styles/prod_comp_search.css b/extensions/MyDashboard/web/styles/prod_comp_search.css new file mode 100644 index 000000000..24c0a2cf8 --- /dev/null +++ b/extensions/MyDashboard/web/styles/prod_comp_search.css @@ -0,0 +1,22 @@ +/* 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_main .hidden { + display: none; +} + +#prod_comp_search_main li.yui-ac-highlight a { + text-decoration: none; + color: #FFFFFF; + display: block; +} diff --git a/template/en/default/account/prefs/saved-searches.html.tmpl b/template/en/default/account/prefs/saved-searches.html.tmpl index 1b78592ca..ce9623372 100644 --- a/template/en/default/account/prefs/saved-searches.html.tmpl +++ b/template/en/default/account/prefs/saved-searches.html.tmpl @@ -67,6 +67,7 @@ Share With a Group </th> [% END %] + [% Hook.process('saved-header') %] </tr> <tr> <td>My [% terms.Bugs %]</td> @@ -145,6 +146,7 @@ [% END %] </td> [% END %] + [% Hook.process('saved-row') %] </tr> [% END %] </table> |