summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--extensions/MyDashboard/Config.pm14
-rw-r--r--extensions/MyDashboard/Extension.pm379
-rw-r--r--extensions/MyDashboard/lib/TimeAgo.pm182
-rw-r--r--extensions/MyDashboard/lib/WebService.pm98
-rw-r--r--extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-header.html.tmpl11
-rw-r--r--extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-row.html.tmpl15
-rw-r--r--extensions/MyDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl11
-rw-r--r--extensions/MyDashboard/template/en/default/mydashboard/prod-comp-search.html.tmpl43
-rw-r--r--extensions/MyDashboard/template/en/default/pages/mydashboard.html.tmpl289
-rw-r--r--extensions/MyDashboard/template/en/default/pages/mydashboard_old.html.tmpl326
-rw-r--r--extensions/MyDashboard/web/js/mydashboard.js126
-rw-r--r--extensions/MyDashboard/web/js/prod_comp_search.js85
-rw-r--r--extensions/MyDashboard/web/styles/mydashboard.css90
-rw-r--r--extensions/MyDashboard/web/styles/prod_comp_search.css22
-rw-r--r--template/en/default/account/prefs/saved-searches.html.tmpl2
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 %]&nbsp;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 %]&nbsp;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 %]&nbsp;[% 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 %]&nbsp;[% 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>