summaryrefslogtreecommitdiffstats
path: root/extensions
diff options
context:
space:
mode:
Diffstat (limited to 'extensions')
-rw-r--r--extensions/BMO/web/js/edituser_menu.js75
-rw-r--r--extensions/BugModal/Config.pm21
-rw-r--r--extensions/BugModal/Extension.pm242
-rw-r--r--extensions/BugModal/lib/ActivityStream.pm284
-rw-r--r--extensions/BugModal/lib/MonkeyPatches.pm38
-rw-r--r--extensions/BugModal/lib/WebService.pm138
-rw-r--r--extensions/BugModal/template/en/default/bug/show-modal.html.tmpl16
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl284
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/attachments.html.tmpl60
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/cc_list.html.tmpl16
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl918
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/field.html.tmpl275
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/flags.html.tmpl162
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/groups.html.tmpl68
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/header.html.tmpl100
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/module.html.tmpl38
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/new_comment.html.tmpl29
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/rel_time.html.tmpl21
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/tracking_flags.html.tmpl96
-rw-r--r--extensions/BugModal/template/en/default/bug_modal/user.html.tmpl53
-rw-r--r--extensions/BugModal/template/en/default/hook/global/setting-descs-settings.none.tmpl11
-rw-r--r--extensions/BugModal/template/en/default/hook/global/variables-end.none.tmpl13
-rw-r--r--extensions/BugModal/web/ZeroClipboard/ZeroClipboard.min.js9
-rw-r--r--extensions/BugModal/web/ZeroClipboard/ZeroClipboard.swfbin0 -> 6580 bytes
-rw-r--r--extensions/BugModal/web/bug_modal.css666
-rw-r--r--extensions/BugModal/web/bug_modal.js730
-rw-r--r--extensions/BugModal/web/calendar.pngbin0 -> 904 bytes
-rw-r--r--extensions/BugModal/web/throbber.gifbin0 -> 723 bytes
-rw-r--r--extensions/TrackingFlags/web/js/tracking_flags.js10
29 files changed, 4331 insertions, 42 deletions
diff --git a/extensions/BMO/web/js/edituser_menu.js b/extensions/BMO/web/js/edituser_menu.js
index 85d933220..707e35b6e 100644
--- a/extensions/BMO/web/js/edituser_menu.js
+++ b/extensions/BMO/web/js/edituser_menu.js
@@ -6,43 +6,42 @@
* defined by the Mozilla Public License, v. 2.0. */
function show_usermenu(id, email, show_edit) {
- var items = {
- profile: {
- name: "Profile",
- callback: function () {
- var href = "user_profile?login=" + encodeURIComponent(email);
- window.open(href, "_blank");
- }
- },
- activity: {
- name: "Activity",
- callback: function () {
- var href = "page.cgi?id=user_activity.html&action=run&from=-14d&who="
- + encodeURIComponent(email);
- window.open(href, "_blank");
- }
- },
- mail: {
- name: "Mail",
- callback: function () {
- var href = "mailto:" + encodeURIComponent(email);
- window.open(href, "_blank");
- }
- },
- };
- if (show_edit) {
- items.edit = {
- name: "Edit",
- callback: function () {
- var href = "editusers.cgi?action=edit&userid=" + id;
- window.open(href, "_blank");
- }
- };
- }
- $.contextMenu({
- selector: ".vcard_" + id,
- trigger: "left",
- items: items
- });
+ var items = [
+ {
+ name: "Profile",
+ callback: function () {
+ var href = "user_profile?login=" + encodeURIComponent(email);
+ window.open(href, "_blank");
+ }
+ },
+ {
+ name: "Activity",
+ callback: function () {
+ var href = "page.cgi?id=user_activity.html&action=run&from=-14d&who=" + encodeURIComponent(email);
+ window.open(href, "_blank");
+ }
+ },
+ {
+ name: "Mail",
+ callback: function () {
+ var href = "mailto:" + encodeURIComponent(email);
+ window.open(href, "_blank");
+ }
+ }
+ ];
+ if (show_edit) {
+ items.push({
+ name: "Edit",
+ callback: function () {
+ var href = "editusers.cgi?action=edit&userid=" + id;
+ window.open(href, "_blank");
+ }
+ });
+ }
+ $.contextMenu({
+ selector: ".vcard_" + id,
+ trigger: "left",
+ items: items
+ });
}
diff --git a/extensions/BugModal/Config.pm b/extensions/BugModal/Config.pm
new file mode 100644
index 000000000..b4242753a
--- /dev/null
+++ b/extensions/BugModal/Config.pm
@@ -0,0 +1,21 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::BugModal;
+use strict;
+
+use constant NAME => 'BugModal';
+use constant REQUIRED_MODULES => [
+ {
+ package => 'Time-Duration',
+ module => 'Time::Duration',
+ version => 0
+ },
+];
+use constant OPTIONAL_MODULES => [ ];
+
+__PACKAGE__->NAME;
diff --git a/extensions/BugModal/Extension.pm b/extensions/BugModal/Extension.pm
new file mode 100644
index 000000000..d3dac3976
--- /dev/null
+++ b/extensions/BugModal/Extension.pm
@@ -0,0 +1,242 @@
+# 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::BugModal;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Extension::BugModal::ActivityStream;
+use Bugzilla::Extension::BugModal::MonkeyPatches;
+use Bugzilla::Constants;
+use Bugzilla::User::Setting;
+use Bugzilla::Util qw(trick_taint datetime_from html_quote);
+use List::MoreUtils qw(any);
+use Template::Stash;
+use Time::Duration;
+
+our $VERSION = '1';
+
+# force skin to mozilla
+sub setting_set_value {
+ my ($self, $args) = @_;
+ return unless $args->{setting} eq 'ui_experiments' && $args->{value} ne 'on';
+ my $settings = Bugzilla->user->settings;
+ return if $settings->{skin}->{value} =~ /^Mozilla/;
+ $settings->{skin}->set('Mozilla');
+}
+
+sub show_bug_format {
+ my ($self, $args) = @_;
+ $args->{format} = _alternative_show_bug_format();
+}
+
+sub edit_bug_format {
+ my ($self, $args) = @_;
+ $args->{format} = _alternative_show_bug_format();
+}
+
+sub _alternative_show_bug_format {
+ my $user = Bugzilla->user;
+ if (my $format = Bugzilla->cgi->param('format')) {
+ return uc($format) eq '__DEFAULT__' ? undef : $format;
+ }
+ return $user->setting('ui_experiments') eq 'on' ? 'modal' : undef;
+}
+
+sub template_after_create {
+ my ($self, $args) = @_;
+ my $context = $args->{template}->context;
+
+ # wrapper around Time::Duration::ago()
+ $context->define_filter(
+ time_duration => sub {
+ my ($context) = @_;
+ return sub {
+ my ($timestamp) = @_;
+ my $datetime = datetime_from($timestamp)
+ // return $timestamp;
+ return ago(time() - $datetime->epoch);
+ };
+ }, 1
+ );
+
+ # morph a string into one which is suitable to use as an element's id
+ $context->define_filter(
+ id => sub {
+ my ($context) = @_;
+ return sub {
+ my ($id) = @_;
+ $id //= '';
+ $id = lc($id);
+ while ($id ne '' && $id !~ /^[a-z]/) {
+ $id = substr($id, 1);
+ }
+ $id =~ tr/ /-/;
+ $id =~ s/[^a-z\d\-_:\.]/_/g;
+ return $id;
+ };
+ }, 1
+ );
+
+ # flatten a list of hashrefs to a list of values
+ # eg. logins = users.pluck("login")
+ $context->define_vmethod(
+ list => pluck => sub {
+ my ($list, $field) = @_;
+ return [ map { $_->$field } @$list ];
+ }
+ );
+
+ # returns array where the value in $field does not equal $value
+ # opposite of "only"
+ # eg. not_byron = users.skip("name", "Byron")
+ $context->define_vmethod(
+ list => skip => sub {
+ my ($list, $field, $value) = @_;
+ return [ grep { $_->$field ne $value } @$list ];
+ }
+ );
+
+ # returns array where the value in $field equals $value
+ # opposite of "skip"
+ # eg. byrons_only = users.only("name", "Byron")
+ $context->define_vmethod(
+ list => only => sub {
+ my ($list, $field, $value) = @_;
+ return [ grep { $_->$field eq $value } @$list ];
+ }
+ );
+
+ # returns boolean indicating if the value exists in the list
+ # eg. has_byron = user_names.exists("byron")
+ $context->define_vmethod(
+ list => exists => sub {
+ my ($list, $value) = @_;
+ return any { $_ eq $value } @$list;
+ }
+ );
+
+ # ucfirst is only available in new template::toolkit versions
+ $context->define_vmethod(
+ item => ucfirst => sub {
+ my ($text) = @_;
+ return ucfirst($text);
+ }
+ );
+}
+
+sub template_before_process {
+ my ($self, $args) = @_;
+ my $file = $args->{file};
+ my $vars = $args->{vars};
+
+ if ($file eq 'bug/process/header.html.tmpl'
+ || $file eq 'bug/create/created.html.tmpl'
+ || $file eq 'attachment/created.html.tmpl'
+ || $file eq 'attachment/updated.html.tmpl')
+ {
+ if (_alternative_show_bug_format()) {
+ $vars->{alt_ui_header} = 'bug_modal/header.html.tmpl';
+ $vars->{alt_ui_show} = 'bug/show-modal.html.tmpl';
+ $vars->{alt_ui_edit} = 'bug_modal/edit.html.tmpl';
+ }
+ return;
+ }
+
+ return unless $file =~ m#^bug/show-([^\.]+)\.html\.tmpl$#;
+ my $format = $1;
+ my $alt = _alternative_show_bug_format() // return;
+ return unless $alt eq $format;
+
+ return unless
+ $vars->{bugs}
+ && ref($vars->{bugs}) eq 'ARRAY'
+ && scalar(@{ $vars->{bugs} }) == 1;
+ my $bug = $vars->{bugs}->[0];
+
+ # trigger loading of tracking flags
+ Bugzilla::Extension::TrackingFlags->template_before_process({
+ file => 'bug/edit.html.tmpl',
+ vars => $vars,
+ });
+
+ # bug->choices loads a lot of data that we want to lazy-load
+ # just load the status and resolutions and perform extra checks here
+ # upstream does these checks in the bug/fields template
+ my $perms = $bug->user;
+ my @resolutions;
+ foreach my $r (@{ Bugzilla::Field->new({ name => 'resolution', cache => 1 })->legal_values }) {
+ my $resolution = $r->name;
+ next unless $resolution;
+ next unless $r->is_active || $resolution eq $bug->resolution;
+
+ if ($perms->{canconfirm}
+ && !($perms->{canedit} || $perms->{isreporter}))
+ {
+ next if
+ $resolution ne 'WORKSFORME'
+ && $resolution ne 'INCOMPLETE'
+ && $resolution ne 'DUPLICATE';
+ }
+ if ($perms->{isreporter}
+ && !($perms->{canconfirm} || $perms->{canedit}))
+ {
+ next if $resolution eq 'INCOMPLETE';
+ }
+ next if $resolution eq 'EXPIRED';
+
+ push @resolutions, $r;
+ }
+ $bug->{choices} = {
+ bug_status => [
+ grep { $_->is_active || $_->name eq $bug->bug_status }
+ @{ $bug->statuses_available }
+ ],
+ resolution => \@resolutions,
+ };
+
+ # group tracking flags by version to allow for a better tabular output
+ my @tracking_table;
+ my $tracking_flags = $vars->{tracking_flags};
+ foreach my $flag (@$tracking_flags) {
+ my $flag_type = $flag->flag_type;
+ my $type = 'status';
+ my $name = $flag->description;
+ if ($flag_type eq 'tracking' && $name =~ /^(tracking|status)-(.+)/) {
+ ($type, $name) = ($1, $2);
+ }
+
+ my ($existing) = grep { $_->{type} eq $flag_type && $_->{name} eq $name } @tracking_table;
+ if ($existing) {
+ $existing->{$type} = $flag;
+ }
+ else {
+ push @tracking_table, {
+ $type => $flag,
+ name => $name,
+ type => $flag_type,
+ };
+ }
+ }
+ $vars->{tracking_flags_table} = \@tracking_table;
+}
+
+sub webservice {
+ my ($self, $args) = @_;
+ my $dispatch = $args->{dispatch};
+ $dispatch->{bug_modal} = 'Bugzilla::Extension::BugModal::WebService';
+}
+
+sub install_before_final_checks {
+ my ($self, $args) = @_;
+ add_setting('ui_experiments', ['on', 'off'], 'off');
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/BugModal/lib/ActivityStream.pm b/extensions/BugModal/lib/ActivityStream.pm
new file mode 100644
index 000000000..dae6b8ba2
--- /dev/null
+++ b/extensions/BugModal/lib/ActivityStream.pm
@@ -0,0 +1,284 @@
+# 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::BugModal::ActivityStream;
+1;
+
+package Bugzilla::Bug;
+use strict;
+use warnings;
+
+use Bugzilla::User;
+use Bugzilla::Constants;
+use Time::Local;
+
+# returns an arrayref containing all changes to the bug - comments, field
+# changes, and duplicates
+# [
+# {
+# time => $unix_timestamp,
+# user_id => actor user-id
+# comment => optional, comment added
+# id => unique identifier for this change-set
+# activty => [
+# {
+# who => user object
+# when => time (string)
+# changes => [
+# {
+# fieldname => field name :)
+# added => string
+# removed => string
+# }
+# ...
+# ]
+# }
+# ...
+# ]
+# },
+# ...
+# ]
+
+sub activity_stream {
+ my ($self) = @_;
+ if (!$self->{activity_stream}) {
+ my $stream = [];
+ _add_comments_to_stream($self, $stream);
+ _add_activities_to_stream($self, $stream);
+ _add_duplicates_to_stream($self, $stream);
+
+ my $base_time = _sql_date_to_time($self->creation_ts);
+ foreach my $change_set (@$stream) {
+ $change_set->{id} = $change_set->{comment}
+ ? 'c' . $change_set->{comment}->count
+ : 'a' . ($change_set->{time} - $base_time) . '.' . $change_set->{user_id};
+ $change_set->{activity} = [
+ sort { $a->{fieldname} cmp $b->{fieldname} }
+ @{ $change_set->{activity} }
+ ];
+ }
+ $self->{activity_stream} = [ sort { $a->{time} <=> $b->{time} } @$stream ];
+ }
+ return $self->{activity_stream};
+}
+
+# comments are processed first, so there's no need to merge into existing entries
+sub _add_comment_to_stream {
+ my ($stream, $time, $user_id, $comment) = @_;
+ push @$stream, {
+ time => $time,
+ user_id => $user_id,
+ comment => $comment,
+ activity => [],
+ };
+}
+
+sub _add_activity_to_stream {
+ my ($stream, $time, $user_id, $data) = @_;
+ foreach my $entry (@$stream) {
+ next unless $entry->{time} == $time && $entry->{user_id} == $user_id;
+ push @{ $entry->{activity} }, $data;
+ return;
+ }
+ push @$stream, {
+ time => $time,
+ user_id => $user_id,
+ comment => undef,
+ activity => [ $data ],
+ };
+}
+
+sub _add_comments_to_stream {
+ my ($bug, $stream) = @_;
+ my $user = Bugzilla->user;
+
+ my $raw_comments = $bug->comments();
+ foreach my $comment (@$raw_comments) {
+ next if $comment->type == CMT_HAS_DUPE;
+ next if $comment->is_private && !($user->is_insider || $user->id == $comment->author->id);
+ next if $comment->body eq '' && ($comment->work_time - 0) != 0 && !$user->is_timetracker;
+ _add_comment_to_stream($stream, _sql_date_to_time($comment->creation_ts), $comment->author->id, $comment);
+ }
+}
+
+sub _add_activities_to_stream {
+ my ($bug, $stream) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
+
+ # build bug activity
+ my ($raw_activity) = $bug->can('get_activity')
+ ? $bug->get_activity()
+ : Bugzilla::Bug::GetBugActivity($bug->id);
+
+ # allow other extensions to alter history
+ Bugzilla::Hook::process('inline_history_activitiy', { activity => $raw_activity });
+
+ my %attachment_cache;
+ foreach my $attachment (@{$bug->attachments}) {
+ $attachment_cache{$attachment->id} = $attachment;
+ }
+
+ # build a list of bugs we need to check visibility of, so we can check with a single query
+ my %visible_bug_ids;
+
+ # envelope, augment and tweak
+ foreach my $operation (@$raw_activity) {
+ # until we can toggle their visibility, skip CC changes
+ $operation->{changes} = [ grep { $_->{fieldname} ne 'cc' } @{ $operation->{changes} } ];
+ next unless @{ $operation->{changes} };
+
+ # make operation.who an object
+ $operation->{who} = Bugzilla::User->new({ name => $operation->{who}, cache => 1 });
+
+ for (my $i = 0; $i < scalar(@{$operation->{changes}}); $i++) {
+ my $change = $operation->{changes}->[$i];
+
+ # make an attachment object
+ if ($change->{attachid}) {
+ $change->{attach} = $attachment_cache{$change->{attachid}};
+ }
+
+ # empty resolutions are displayed as --- by default
+ # make it explicit here to enable correct display of the change
+ if ($change->{fieldname} eq 'resolution') {
+ $change->{removed} = '---' if $change->{removed} eq '';
+ $change->{added} = '---' if $change->{added} eq '';
+ }
+
+ # make boolean fields true/false instead of 1/0
+ my ($table, $field) = ('bugs', $change->{fieldname});
+ if ($field =~ /^([^\.]+)\.(.+)$/) {
+ ($table, $field) = ($1, $2);
+ }
+ my $column = $dbh->bz_column_info($table, $field);
+ if ($column && $column->{TYPE} eq 'BOOLEAN') {
+ $change->{removed} = '';
+ $change->{added} = $change->{added} ? 'true' : 'false';
+ }
+
+ # load field object (only required for custom fields), and set the
+ # field type for custom fields
+ my $field_obj;
+ if ($change->{fieldname} =~ /^cf_/) {
+ $field_obj = Bugzilla::Field->new({ name => $change->{fieldname}, cache => 1 });
+ $change->{fieldtype} = $field_obj->type;
+ }
+
+ # identify buglist changes
+ if ($change->{fieldname} eq 'blocked' ||
+ $change->{fieldname} eq 'dependson' ||
+ $change->{fieldname} eq 'dupe' ||
+ ($field_obj && $field_obj->type == FIELD_TYPE_BUG_ID)
+ ) {
+ $change->{buglist} = 1;
+ foreach my $what (qw(removed added)) {
+ my @buglist = split(/[\s,]+/, $change->{$what});
+ foreach my $id (@buglist) {
+ if ($id && $id =~ /^\d+$/) {
+ $visible_bug_ids{$id} = 1;
+ }
+ }
+ }
+ }
+
+ # split see-also
+ if ($change->{fieldname} eq 'see_also') {
+ my $url_base = correct_urlbase();
+ foreach my $f (qw( added removed )) {
+ my @values;
+ foreach my $value (split(/, /, $change->{$f})) {
+ my ($bug_id) = substr($value, 0, length($url_base)) eq $url_base
+ ? $value =~ /id=(\d+)$/
+ : undef;
+ push @values, {
+ url => $value,
+ bug_id => $bug_id,
+ };
+ }
+ $change->{$f} = \@values;
+ }
+ }
+
+ # split multiple flag changes (must be processed last)
+ if ($change->{fieldname} eq 'flagtypes.name') {
+ my @added = split(/, /, $change->{added});
+ my @removed = split(/, /, $change->{removed});
+ next if scalar(@added) <= 1 && scalar(@removed) <= 1;
+ # remove current change
+ splice(@{$operation->{changes}}, $i, 1);
+ # restructure into added/removed for each flag
+ my %flags;
+ foreach my $added (@added) {
+ my ($value, $name) = $added =~ /^((.+).)$/;
+ $flags{$name}{added} = $value;
+ $flags{$name}{removed} |= '';
+ }
+ foreach my $removed (@removed) {
+ my ($value, $name) = $removed =~ /^((.+).)$/;
+ $flags{$name}{added} |= '';
+ $flags{$name}{removed} = $value;
+ }
+ # clone current change, modify and insert
+ foreach my $flag (sort keys %flags) {
+ my $flag_change = {};
+ foreach my $key (keys %$change) {
+ $flag_change->{$key} = $change->{$key};
+ }
+ $flag_change->{removed} = $flags{$flag}{removed};
+ $flag_change->{added} = $flags{$flag}{added};
+ splice(@{$operation->{changes}}, $i, 0, $flag_change);
+ }
+ $i--;
+ }
+ }
+
+ _add_activity_to_stream($stream, _sql_date_to_time($operation->{when}), $operation->{who}->id, $operation);
+ }
+
+ # prime the visible-bugs cache
+ $user->visible_bugs([keys %visible_bug_ids]);
+}
+
+# display 'duplicate of this bug' as an activity entry, not a comment
+sub _add_duplicates_to_stream {
+ my ($bug, $stream) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $sth = $dbh->prepare("
+ SELECT longdescs.who,
+ UNIX_TIMESTAMP(bug_when), " .
+ $dbh->sql_date_format('bug_when') . ",
+ extra_data
+ FROM longdescs
+ INNER JOIN profiles ON profiles.userid = longdescs.who
+ WHERE bug_id = ? AND type = ?
+ ORDER BY bug_when
+ ");
+ $sth->execute($bug->id, CMT_HAS_DUPE);
+
+ while (my($who, $time, $when, $dupe_id) = $sth->fetchrow_array) {
+ _add_activity_to_stream($stream, $time, $who, {
+ who => Bugzilla::User->new({ id => $who, cache => 1 }),
+ when => $when,
+ changes => [{
+ fieldname => 'duplicate',
+ added => $dupe_id,
+ buglist => 1,
+ }],
+ });
+ }
+}
+
+sub _sql_date_to_time {
+ my ($date) = @_;
+ $date =~ /^(\d{4})[\.\-](\d{2})[\.\-](\d{2}) (\d{2}):(\d{2}):(\d{2})$/
+ or die "internal error: invalid date '$date'";
+ return timelocal($6, $5, $4, $3, $2 - 1, $1 - 1900);
+}
+
+1;
diff --git a/extensions/BugModal/lib/MonkeyPatches.pm b/extensions/BugModal/lib/MonkeyPatches.pm
new file mode 100644
index 000000000..1d902b2a9
--- /dev/null
+++ b/extensions/BugModal/lib/MonkeyPatches.pm
@@ -0,0 +1,38 @@
+# 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::BugModal::MonkeyPatches;
+1;
+
+package Bugzilla::Bug;
+use strict;
+use warnings;
+
+use Bugzilla::Attachment;
+
+sub active_attachments {
+ my ($self) = @_;
+ return [] if $self->{error};
+ return $self->{active_attachments} //= Bugzilla::Attachment->get_attachments_by_bug(
+ $self, { exclude_obsolete => 1, preload => 1 });
+}
+
+1;
+
+package Bugzilla::User;
+use strict;
+use warnings;
+
+sub moz_nick {
+ my ($self) = @_;
+ return $1 if $self->name =~ /:(.+?)\b/;
+ return $self->name if $self->name;
+ $self->login =~ /^([^\@]+)\@/;
+ return $1;
+}
+
+1;
diff --git a/extensions/BugModal/lib/WebService.pm b/extensions/BugModal/lib/WebService.pm
new file mode 100644
index 000000000..4c8b6b001
--- /dev/null
+++ b/extensions/BugModal/lib/WebService.pm
@@ -0,0 +1,138 @@
+# 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::BugModal::WebService;
+use strict;
+use warnings;
+
+use base qw(Bugzilla::WebService);
+
+use Bugzilla::Bug;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Field;
+use Bugzilla::Keyword;
+use Bugzilla::Milestone;
+use Bugzilla::Version;
+
+# these methods are much lighter than our public API calls
+
+sub rest_resources {
+ return [
+ # return all the lazy-loaded data; kept in sync with the UI's
+ # requirements.
+ qr{^/bug_modal/edit/(\d+)$}, {
+ GET => {
+ method => 'edit',
+ params => sub {
+ return { id => $_[0] }
+ },
+ },
+ },
+ # returns pre-formatted html, enabling reuse of the user template
+ qr{^/bug_modal/cc/(\d+)$}, {
+ GET => {
+ method => 'cc',
+ params => sub {
+ return { id => $_[0] }
+ },
+ },
+ },
+ ]
+}
+
+# everything we need for edit mode in a single call, returning just the fields
+# that the ui requires.
+sub edit {
+ my ($self, $params) = @_;
+ my $user = Bugzilla->user;
+ my $bug = Bugzilla::Bug->check({ id => $params->{id} });
+
+ # the keys of the options hash must match the field id in the ui
+ my %options;
+
+ my @products = @{ $user->get_enterable_products };
+ unless (grep { $_->id == $bug->product_id } @products) {
+ unshift @products, $bug->product_obj;
+ }
+ $options{product} = [ map { { name => $_->name, description => $_->description } } @products ];
+
+ $options{component} = _name_desc($bug->component, $bug->product_obj->components);
+ $options{version} = _name($bug->version, $bug->product_obj->versions);
+ $options{target_milestone} = _name($bug->target_milestone, $bug->product_obj->milestones);
+ $options{priority} = _name($bug->priority, 'priority');
+ $options{bug_severity} = _name($bug->bug_severity, 'bug_severity');
+ $options{rep_platform} = _name($bug->rep_platform, 'rep_platform');
+ $options{op_sys} = _name($bug->op_sys, 'op_sys');
+
+ # custom select fields
+ my @custom_fields =
+ grep { $_->type == FIELD_TYPE_SINGLE_SELECT || $_->type == FIELD_TYPE_MULTI_SELECT }
+ Bugzilla->active_custom_fields({ product => $bug->product_obj, component => $bug->component_obj });
+ foreach my $field (@custom_fields) {
+ my $field_name = $field->name;
+ $options{$field_name} = [
+ map { { name => $_->name } }
+ grep { $bug->$field_name eq $_->name || $_->is_active }
+ @{ $field->legal_values }
+ ];
+ }
+
+ # keywords
+ my @keywords = Bugzilla::Keyword->get_all();
+
+ # results
+ return {
+ options => \%options,
+ keywords => [ map { $_->name } @keywords ],
+ };
+}
+
+sub _name {
+ my ($current, $values) = @_;
+ # values can either be an array-ref of values, or a field name, which
+ # result in that field's legal-values being used.
+ if (!ref($values)) {
+ $values = Bugzilla::Field->new({ name => $values, cache => 1 })->legal_values;
+ }
+ return [
+ map { { name => $_->name } }
+ grep { $_->name eq $current || $_->is_active }
+ @$values
+ ];
+}
+
+sub _name_desc {
+ my ($current, $values) = @_;
+ if (!ref($values)) {
+ $values = Bugzilla::Field->new({ name => $values, cache => 1 })->legal_values;
+ }
+ return [
+ map { { name => $_->name, description => $_->description } }
+ grep { $_->name eq $current || $_->is_active }
+ @$values
+ ];
+}
+
+sub cc {
+ my ($self, $params) = @_;
+ my $template = Bugzilla->template;
+ my $bug = Bugzilla::Bug->check({ id => $params->{id} });
+ my $vars = {
+ cc_list => [
+ sort { lc($a->moz_nick) cmp lc($b->moz_nick) }
+ @{ $bug->cc_users }
+ ]
+ };
+
+ my $html = '';
+ $template->process('bug_modal/cc_list.html.tmpl', $vars, \$html)
+ || ThrowTemplateError($template->error);
+ return { html => $html };
+}
+
+1;
diff --git a/extensions/BugModal/template/en/default/bug/show-modal.html.tmpl b/extensions/BugModal/template/en/default/bug/show-modal.html.tmpl
new file mode 100644
index 000000000..f49b87435
--- /dev/null
+++ b/extensions/BugModal/template/en/default/bug/show-modal.html.tmpl
@@ -0,0 +1,16 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+[% IF !header_done %]
+ [% PROCESS bug_modal/header.html.tmpl %]
+ [% PROCESS global/header.html.tmpl %]
+ [% header_done = 1 %]
+[% END %]
+[% INCLUDE bug_modal/edit.html.tmpl %]
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl
new file mode 100644
index 000000000..881ed927f
--- /dev/null
+++ b/extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl
@@ -0,0 +1,284 @@
+[%# 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.
+ #%]
+
+[%
+ FOREACH change_set IN bug.activity_stream;
+ '<div class="change-set" id="' _ change_set.id _ '">';
+
+ extra_class = "";
+ IF change_set.user_id == bug.assigned_to.id;
+ extra_class = "assignee";
+ ELSIF change_set.user_id == bug.reporter.id;
+ extra_class = "reporter";
+ END;
+
+ IF change_set.comment;
+ PROCESS comment_header comment=change_set.comment;
+ ELSE;
+ PROCESS activity_header activities=change_set.activity id=change_set.id;
+ END;
+
+ IF change_set.comment;
+ PROCESS comment_body comment=change_set.comment;
+ END;
+ FOREACH activity IN change_set.activity;
+ PROCESS activity_body activity=activity;
+ END;
+
+ '</div>';
+ END;
+%]
+
+[% BLOCK comment_header %]
+ <div class="comment">
+ [%# normal comment header %]
+ <table class="layout-table change-head [% extra_class FILTER none %]" id="ch-[% comment.count FILTER none %]"
+ [% IF comment.collapsed +%] style="display:none"[% END %]>
+ <tr>
+ <td rowspan="2" class="change-gravatar">
+ [% INCLUDE bug_modal/user.html.tmpl
+ u = comment.author
+ gravatar_size = 32
+ gravatar_only = 1
+ %]
+ </td>
+ <td class="change-author">
+ [% INCLUDE bug_modal/user.html.tmpl
+ u = comment.author
+ %]
+ [% IF extra_class %]
+ <span class="user-role">([% extra_class.ucfirst FILTER none %])</span>
+ [% END %]
+ [% Hook.process('user', 'bug/comments.html.tmpl') %]
+ </td>
+ <td class="comment-actions">
+ [% IF user.is_insider && bug.check_can_change_field('longdesc', 0, 1) %]
+ [% IF comment.is_private %]
+ <div class="comment-private edit-hide bz_private">
+ Private
+ </div>
+ [% END %]
+ <div class="comment-private edit-show" style="display:none">
+ <input type="hidden" value="1" name="defined_isprivate_[% comment.id FILTER none %]">
+ <input type="checkbox" name="isprivate_[% comment.id FILTER none %]"
+ id="is-private-[% comment.id FILTER none %]"
+ class="is-private" value="1" [%= "checked" IF comment.is_private %]>
+ <label for="is-private-[% comment.id FILTER none %]">Private</label>
+ </div>
+ [% END %]
+ [% IF user.id %]
+ <button class="reply-btn in-page"
+ data-reply-id="[% comment.count FILTER none %]"
+ data-reply-name="[% comment.author.name || comment.author.moz_nick FILTER html %]"
+ >Reply</button>
+ [% END %]
+ <button class="comment-spinner in-page" id="cs-[% comment.count FILTER none%]">-</button>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="2">
+ <div class="change-name">
+ <a href="show_bug.cgi?id=[% bug.bug_id FILTER none %]#c[% comment.count FILTER none %]">
+ [% comment.count == 0 ? "Description" : "Comment " _ comment.count ~%]
+ </a>
+ </div>
+ &bull;
+ <div class="change-time">
+ [% INCLUDE bug_modal/rel_time.html.tmpl ts=comment.creation_ts %]
+ </div>
+ </td>
+ </tr>
+ [% IF comment.tags.size %]
+ <tr>
+ <td colspan="2" class="comment-tags">
+ [% FOREACH tag IN comment.tags %]
+ <span class="comment-tag">[% tag FILTER html %]</span>
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+ </table>
+
+ [%# default-collapsed comment header %]
+ [% IF comment.collapsed %]
+ <table class="layout-table change-head default-collapsed" id="cc-[% comment.count FILTER none %]">
+ <tr>
+ <td class="comment-collapse-reason"
+ title="[% comment.author.moz_nick FILTER html %] [[% comment.creation_ts FILTER time_duration FILTER html %]]">
+ Comment hidden ([% comment.tags.join(', ') FILTER html %])
+ </td>
+ <td class="comment-actions">
+ <button class="comment-spinner in-page" id="ccs-[% comment.count FILTER none%]">
+ [%~ comment.collapsed ? "+" : "-" ~%]
+ </button>
+ </td>
+ </tr>
+ </table>
+ [% END %]
+ </div>
+[% END %]
+
+[% BLOCK activity_header %]
+ [% action = activities.0 %]
+ <div id="[% id FILTER none %]" class="change">
+ <table class="layout-table change-head [% extra_class FILTER none %]">
+ <tr>
+ <td rowspan="2" class="change-gravatar">
+ [% INCLUDE bug_modal/user.html.tmpl
+ u = action.who
+ gravatar_size = 32
+ gravatar_only = 1
+ %]
+ </td>
+ <td class="change-author">
+ [% INCLUDE bug_modal/user.html.tmpl
+ u = action.who
+ %]
+ [% IF extra_class %]
+ <span class="user-role">([% extra_class.ucfirst FILTER none %])</span>
+ [% END %]
+ </td>
+ </tr>
+ <tr>
+ <td colspan="2">
+ <div class="change-name">
+ <a href="show_bug.cgi?id=[% bug.bug_id FILTER none %]#[% id FILTER none %]">Updated</a>
+ </div>
+ &bull;
+ <div class="change-time">
+ [% INCLUDE bug_modal/rel_time.html.tmpl ts=action.when %]
+ </div>
+ </td>
+ </tr>
+ </table>
+ </div>
+[% END %]
+
+[% BLOCK comment_body %]
+ <pre class="comment-text [%= "bz_private" IF comment.is_private %]" id="ct-[% comment.count FILTER none %]"
+ [% IF comment.collapsed +%] style="display:none"[% END ~%]
+ >[% comment.body_full FILTER quoteUrls(bug, comment) %]</pre>
+[% END %]
+
+[%
+ BLOCK activity_body;
+ '<div class="activity">';
+ has_cc = 0;
+
+ FOREACH change IN activity.changes;
+ '<div class="change">';
+ class = "";
+
+ IF change.fieldname == 'cc';
+ has_cc = 1;
+ class = "activity-cc";
+ END;
+
+ IF change.attachid;
+ %]
+ <a href="attachment.cgi?id=[% change.attachid FILTER none %]&amp;action=edit"
+ title="[% change.attach.description FILTER html %]"
+ class="[% "bz_obsolete" IF change.attach.isobsoletee %]"
+ >Attachment #[% change.attachid FILTER none %]</a> -
+ [%+
+ END;
+
+ IF change.buglist;
+ IF change.fieldname == 'duplicate';
+ label = "Duplicate of this " _ terms.bug;
+ ELSE;
+ label = field_descs.${change.fieldname};
+ END;
+ IF change.added != '';
+ label _ ": " FILTER html;
+ PROCESS add_change value=change.added;
+ END;
+ IF change.removed != '';
+ IF change.added != '';
+ "<br>";
+ END;
+ "No longer ";
+ label FILTER lcfirst;
+ ": ";
+ PROCESS add_change value=change.removed;
+ END;
+
+ ELSE;
+ IF change.fieldname == 'longdescs.isprivate';
+ # reference the comment that was made private/public in the field label
+ %]
+ <a href="#c[% change.comment.count FILTER none %]">
+ Comment [% change.comment.count FILTER none %]</a> is private:
+ [%+
+ ELSE;
+ field_descs.${change.fieldname} _ ": " FILTER html;
+ END;
+
+ IF change.removed != '';
+ IF change.added == '';
+ '<span class="activity-deleted">';
+ END;
+ PROCESS add_change value=change.removed;
+ IF change.added == '';
+ '</span>';
+ ELSE;
+ ' &rarr; ';
+ END;
+ END;
+ PROCESS add_change value=change.added;
+ END;
+
+ '</div>';
+ END;
+ '</div>';
+ END;
+
+ BLOCK add_change;
+ SWITCH change.fieldname;
+
+ CASE [ 'estimated_time', 'remaining_time', 'work_time' ];
+ PROCESS formattimeunit time_unit=value;
+
+ CASE 'bug_file_loc';
+ %]
+ <a href="[% value FILTER html %]" target="_blank" rel="noreferrer"
+ [% UNLESS is_safe_url(value) %]
+ onclick="return confirmUnsafeURL(this.href)"
+ [% END %]
+ >[% value FILTER truncate(256, '&hellip;') FILTER html %]</a>
+ [%
+
+ CASE 'see_also';
+ FOREACH see_also IN value;
+ IF see_also.bug_id;
+ "$terms.bug $see_also.bug_id" FILTER bug_link(see_also.bug_id);
+ ELSE;
+ %]
+ <a href="[% see_also.url FILTER html %]" target="_blank">[% see_also.url FILTER html %]</a>
+ [%
+ END;
+ ", " UNLESS loop.last;
+ END;
+
+ CASE [ 'assigned_to', 'reporter', 'qa_contact', 'cc', 'flagtypes.name' ];
+ value FILTER email;
+
+ CASE;
+ IF change.fieldtype == constants.FIELD_TYPE_DATETIME;
+ INCLUDE bug_modal/rel_time.html.tmpl ts=value;
+
+ ELSIF change.buglist;
+ value FILTER bug_list_link;
+
+ ELSE;
+ value FILTER truncate(256, '&hellip;') FILTER html;
+
+ END;
+ END;
+ END;
+%]
diff --git a/extensions/BugModal/template/en/default/bug_modal/attachments.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/attachments.html.tmpl
new file mode 100644
index 000000000..3055cc861
--- /dev/null
+++ b/extensions/BugModal/template/en/default/bug_modal/attachments.html.tmpl
@@ -0,0 +1,60 @@
+[%# 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.
+ #%]
+
+[%#
+ # bug: (bug object) the main bug object
+ #%]
+
+<table class="layout-table" id="attachments">
+ [% FOREACH attachment IN bug.attachments %]
+ [% NEXT IF attachment.isprivate && !(user.is_insider || attachment.attacher.id == user.id) %]
+ <tr class="
+ [%~ " bz_private" IF attachment.isprivate %]
+ [%~ " attach-obsolete" IF attachment.isobsolete %]
+ [%~ " attach-patch" IF attachment.ispatch %]
+ " [% IF attachment.isobsolete %]style="display:none"[% END %]>
+ <td class="attach-desc-td">
+ <div class="attach-desc">
+ <a href="attachment.cgi?id=[% attachment.id FILTER none %]">
+ [%~ attachment.description FILTER html %]</a>
+ </div>
+ <div>
+ <span class="attach-time">[% INCLUDE bug_modal/rel_time.html.tmpl ts=attachment.attached %]</span>
+ <span class="attach-author">[% INCLUDE bug_modal/user.html.tmpl u=attachment.attacher %]</span>
+ </div>
+ <div class="attach-info">
+ [% IF attachment.datasize %]
+ [%- attachment.datasize FILTER unitconvert %]
+ [% ELSE %]
+ (deleted)
+ [% END %],
+ [%+ attachment.ispatch ? "patch" : attachment.contenttype FILTER html -%]
+ </div>
+ </td>
+ <td>
+ [% FOREACH flag IN attachment.flags %]
+ <div class="attach-flag">
+ [% INCLUDE bug_modal/user.html.tmpl u=flag.setter simple=1 %]:
+ <span class="flag-name-status">
+ [%+ flag.type.name FILTER html %][% flag.status FILTER none %]
+ </span>
+ [% IF flag.requestee %]
+ [%+ INCLUDE bug_modal/user.html.tmpl u=flag.requestee simple=1 %]
+ [% END %]
+ </div>
+ [% END %]
+ </td>
+ <td class="attach-actions">
+ <a href="attachment.cgi?id=[% attachment.id FILTER none %]&amp;action=edit">Details</a>
+ [% IF attachment.ispatch %]
+ | <a href="attachment.cgi?id=[% attachment.id FILTER none %]&amp;action=diff">Diff</a>
+ [% END %]
+ [% Hook.process("action", "attachment/list.html.tmpl") %]
+ </tr>
+ [% END %]
+</table>
diff --git a/extensions/BugModal/template/en/default/bug_modal/cc_list.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/cc_list.html.tmpl
new file mode 100644
index 000000000..37f582e0e
--- /dev/null
+++ b/extensions/BugModal/template/en/default/bug_modal/cc_list.html.tmpl
@@ -0,0 +1,16 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%
+ UNLESS cc_list.size;
+ 'None';
+ END;
+ FOREACH cc IN cc_list;
+ INCLUDE bug_modal/user.html.tmpl u=cc;
+ END;
+%]
diff --git a/extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl
new file mode 100644
index 000000000..a16bfdd9b
--- /dev/null
+++ b/extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl
@@ -0,0 +1,918 @@
+[%# 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.
+ #%]
+
+[%
+ USE Bugzilla;
+
+ # only edit one bug
+ UNLESS bug.defined;
+ bug = bugs.0;
+ END;
+ bugid = bug.id;
+
+ # this is used in a few places
+ is_cced = bug.cc.contains(user.login);
+
+ # custom fields that have custom rendering, or should not be rendered
+ rendered_custom_fields = [
+ 'cf_user_story',
+ 'cf_last_resolved',
+ ];
+
+ # all custom fields
+ custom_fields = Bugzilla.active_custom_fields(product => bug.product_obj, component => bug.component_obj, bug_id => bug.id);
+
+ # extract needinfo flags
+ needinfo = [];
+ FOREACH flag_type IN bug.flag_types;
+ IF flag_type.name == 'needinfo';
+ needinfo_flag_type = flag_type;
+ FOREACH flag IN flag_type.flags;
+ IF flag.status == '?';
+ needinfo.push(flag);
+ END;
+ END;
+ END;
+ END;
+
+ # count attachments
+ active_attachments = 0;
+ obsolete_attachments = 0;
+ FOREACH attachment IN bug.attachments;
+ NEXT IF attachment.isprivate && !(user.is_insider || attachment.attacher.id == user.id);
+ IF attachment.isobsolete;
+ obsolete_attachments = obsolete_attachments + 1;
+ ELSE;
+ active_attachments = active_attachments + 1;
+ END;
+ END;
+
+ # count set bug flags (excluding needinfo)
+ has_bug_flags = 0;
+ FOREACH flag IN bug.flags;
+ NEXT IF flag.name == 'needinfo';
+ has_bug_flags = 1;
+ LAST;
+ END;
+
+ # count set project/tracking flags
+ set_project_flags = [];
+ set_tracking_flags = [];
+ FOREACH flag IN tracking_flags;
+ NEXT IF flag.bug_flag(bug.id).value == "---";
+ IF flag.flag_type == "project";
+ set_project_flags.push(flag);
+ END;
+ IF flag.flag_type == "tracking";
+ set_tracking_flags.push(flag);
+ END;
+ END;
+
+ # build firefox flags subtitle
+ firefox_flags = [];
+ firefox_fixed_version = "";
+ tracking_flags_title = "Firefox Tracking Flags";
+ # project flags
+ FOREACH row IN tracking_flags_table;
+ NEXT UNLESS row.type == "project";
+ status_value = row.status.bug_flag(bug.id).value;
+ NEXT IF status_value == "---";
+ firefox_flags.push(row.name _ ":" _ status_value);
+ END;
+ # tracking flags title and subtitle
+ FOREACH row IN tracking_flags_table;
+ NEXT UNLESS row.type == "tracking";
+ tracking_value = row.tracking ? row.tracking.bug_flag(bug.id).value : "---";
+ status_value = row.status.bug_flag(bug.id).value || "---";
+ NEXT IF tracking_value == "---" && status_value == "---";
+ blurb = row.name;
+ IF tracking_value != "---";
+ blurb = blurb _ tracking_value;
+ END;
+ IF status_value != "---";
+ blurb = blurb _ " " _ status_value;
+ IF firefox_fixed_version == "" && status_value == "fixed";
+ firefox_fixed_version = row.name.ucfirst.replace('^(\D+)(\d)', '$1 $2');
+ END;
+ END;
+ firefox_flags.push(blurb);
+ IF row.name.search("^thunderbird");
+ tracking_flags_title = "Thunderbird Tracking Flags";
+ ELSIF row.name.search("^seamonkey");
+ tracking_flags_title = "SeaMonkey Tracking Flags";
+ END;
+ END;
+ IF firefox_flags.size;
+ firefox_flags_subtitle = firefox_flags.join(", ");
+ ELSE;
+ firefox_flags_subtitle = "Not tracked";
+ END;
+%]
+
+[% IF user.id %]
+ <form name="changeform" id="changeform" method="post" action="process_bug.cgi">
+ <input type="hidden" name="delta_ts" value="[% bug.delta_ts FILTER html %]">
+ <input type="hidden" name="longdesclength" value="[% bug.comments.size FILTER html %]">
+ <input type="hidden" name="id" id="bug_id" value="[% bug.bug_id FILTER html %]">
+ <input type="hidden" name="token" value="[% issue_hash_token([bug.id, bug.delta_ts]) FILTER html %]">
+[% END %]
+
+[%# === header === %]
+
+<div id="xhr-error" style="display:none"></div>
+
+[% WRAPPER bug_modal/module.html.tmpl
+ title = ""
+%]
+ <div id="summary-container">
+ [%# bug id, alias, and summary %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ container = 1
+ no_label = 1
+ view_only = 1
+ %]
+ <div id="field-value-bug_id">
+ <a id="this-bug" href="show_bug.cgi?id=[% bug.id FILTER none %]
+ [%~ '&amp;format=' _ cgi.param("format") IF cgi.param("format") %]"
+ >
+ [%~ terms.Bug _ " " _ bug.id FILTER none ~%]
+ </a>
+ [% IF bug.alias %]
+ <span class="edit-hide">
+ ([% bug.alias FILTER html %])
+ </span>
+ [% END %]
+ </div>
+ [% END %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ container = 1
+ no_label = 1
+ hide_on_edit = 1
+ %]
+ <div id="field-value-short_desc">[% bug.short_desc FILTER html %]</div>
+ [% END %]
+
+ [%# alias %]
+ [% INCLUDE bug_modal/field.html.tmpl
+ field = bug_fields.alias
+ field_type = constants.FIELD_TYPE_FREETEXT
+ hide_on_view = 1
+ short_width = 1
+ %]
+
+ [%# summary %]
+ [% INCLUDE bug_modal/field.html.tmpl
+ field = bug_fields.short_desc
+ field_type = constants.FIELD_TYPE_FREETEXT
+ hide_on_view = 1
+ %]
+
+ [%# status summary %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ name = "status_summary"
+ no_label = 1
+ hide_on_edit = 1
+ %]
+ <b>
+ [% bug.bug_status FILTER html %]
+ [%+ bug.resolution FILTER html IF bug.resolution %]
+ </b>
+ [% IF bug.resolution == "FIXED"
+ && bug.target_milestone
+ && bug.target_milestone != "---"
+ %]
+ in [% firefox_fixed_version || bug.target_milestone FILTER html %]
+ [% ELSIF bug.dup_id %]
+ of [% terms.bug _ " $bug.dup_id" FILTER bug_link(bug.dup_id) FILTER none %]
+ [% END %]
+ [% IF needinfo.size %]
+ <div id="status-needinfo">
+ (NeedInfo from
+ [%+
+ IF needinfo.size == 1;
+ INCLUDE bug_modal/user.html.tmpl u=needinfo.0.requestee nick_only=1;
+ ELSE;
+ " " _ needinfo.size _ " people";
+ END;
+ ~%]
+ )
+ </div>
+ [% END %]
+ [% END %]
+ </div>
+
+ [%# buttons %]
+
+ <div id="mode-container">
+ [% IF user.id %]
+ <div>
+ <button type="button" class="minor" id="cc-btn" data-is-cced="[% is_cced ? 1 : 0 %]">
+ [% is_cced ? "Stop following" : "Follow" %]
+ </button>
+ <button type="button" id="cancel-btn" class="minor" style="display:none">Cancel</button>
+ <button type="button" id="mode-btn">
+ <span id="mode-btn-readonly">Edit</span>
+ <span id="mode-btn-loading">
+ <img id="edit-throbber" src="extensions/BugModal/web/throbber.gif" width="16" height="11">
+ Fetching
+ </span>
+ </button>
+ <button type="submit" id="commit-btn" style="display:none">Save Changes</button>
+ </div>
+ [% END %]
+ <div class="button-row">
+ [% IF user.id %]
+ <button type="button" class="comment-btn in-page">Add Comment</button>
+ [% END %]
+ <button type="button" id="last-comment-btn" class="in-page">Last Comment &darr;</button>
+ </div>
+ <div class="button-row">
+ [% IF bug.assigned_to.id == user.id || user.in_group("editbugs") %]
+ <button type="button" id="copy-summary" class="in-page"
+ title="Copy [% terms.bug %] number and summary to your clipboard">Copy Summary</button>
+ [% END %]
+ <button type="button" id="expand-all-btn" class="in-page">Expand All</button>
+ </div>
+ </div>
+[% END %]
+
+[%# === status === %]
+
+[% WRAPPER bug_modal/module.html.tmpl
+ title = "Status"
+%]
+ [% WRAPPER fields_lhs %]
+
+ [%# product %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ field = bug_fields.product
+ field_type = constants.FIELD_TYPE_SINGLE_SELECT
+ %]
+ <div class="spin-toggle" data-latch="#product-latch" data-for="#product-info">
+ <span class="spin-latch" id="product-latch">&#9656;</span>
+ [% bug.product FILTER html %]
+ </div>
+ <div id="product-info" style="display:none">
+ [% bug.product_obj.description FILTER html_light %]
+ </div>
+ [% END %]
+
+ [%# component %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ field = bug_fields.component
+ field_type = constants.FIELD_TYPE_SINGLE_SELECT
+ %]
+ <div class="spin-toggle" data-latch="#component-latch" data-for="#component-info">
+ <span class="spin-latch" id="component-latch">&#9656;</span>
+ [% bug.component FILTER html %]
+ </div>
+ <div id="component-info" style="display:none">
+ <div>[% bug.component_obj.description FILTER html_light %]</div>
+ <a href="buglist.cgi?component=[% bug.component FILTER uri %]&amp;
+ [%~ %]product=[% bug.product FILTER uri %]&amp;
+ [%~ %]bug_status=__open__" target="_blank">Other [% terms.Bugs %]</a>
+ </div>
+ [% END %]
+
+ [%# importance %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ label = "Importance"
+ container = 1
+ hide_on_view = bug.priority == "--" && bug.bug_severity == "normal"
+ %]
+ [% INCLUDE bug_modal/field.html.tmpl
+ field = bug_fields.priority
+ field_type = constants.FIELD_TYPE_SINGLE_SELECT
+ no_indent = 1
+ inline = 1
+ %]
+ [% INCLUDE bug_modal/field.html.tmpl
+ field = bug_fields.bug_severity
+ field_type = constants.FIELD_TYPE_SINGLE_SELECT
+ inline = 1
+ %]
+ [% UNLESS cf_hidden_in_product('cf_rank', bug.product, bug.component) %]
+ [% rendered_custom_fields.push('cf_rank') %]
+ [% INCLUDE bug_modal/field.html.tmpl
+ field = bug_fields.cf_rank
+ field_type = constants.FIELD_TYPE_INTEGER
+ inline = 1
+ label = "Rank"
+ hide_on_view = bug.cf_rank == ""
+ %]
+ [% END %]
+ [% END %]
+
+ [%# status, resolution %]
+ [% IF bug.assigned_to.id != user.id %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ name = "status-view"
+ container = 1
+ label = "Status"
+ hide_on_edit = 1
+ %]
+ [% bug.bug_status FILTER html %]
+ [%+ bug.resolution FILTER html IF bug.resolution %]
+ [% IF bug.dup_id %]
+ of [% terms.bug _ " $bug.dup_id" FILTER bug_link(bug.dup_id) FILTER none %]
+ [% END %]
+ [% END %]
+ [% END %]
+
+ [% END %]
+ [% WRAPPER fields_rhs %]
+
+ [%# creation time %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ field = bug_fields.creation_ts
+ label = "Reported"
+ view_only = 1
+ %]
+ [% INCLUDE bug_modal/rel_time.html.tmpl ts=bug.creation_ts %]
+ [% END %]
+
+ [%# last modified %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ field = bug_fields.delta_ts
+ label = "Modified"
+ view_only = 1
+ %]
+ [% INCLUDE bug_modal/rel_time.html.tmpl ts=bug.delta_ts %]
+ [% END %]
+
+ [% END %]
+
+ [%# status/resolution knob %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ name = "status-edit"
+ container = 1
+ label = "Status"
+ hide_on_view = bug.assigned_to.id != user.id
+ %]
+ [% INCLUDE bug_modal/field.html.tmpl
+ field = bug_fields.bug_status
+ field_type = constants.FIELD_TYPE_SINGLE_SELECT
+ values = bug.choices.bug_status
+ inline = 1
+ no_indent = 1
+ edit_only = 1
+ %]
+ [% INCLUDE bug_modal/field.html.tmpl
+ field = bug_fields.resolution
+ field_type = constants.FIELD_TYPE_SINGLE_SELECT
+ values = bug.choices.resolution
+ inline = 1
+ edit_only = 1
+ %]
+ [% IF bug.choices.resolution.only("name", "DUPLICATE").size %]
+ <div id="duplicate-container">
+ of
+ <input id="dup_id" name="dup_id" size="6" value="[% bug.dup_id FILTER html %]">
+ </div>
+ <div id="duplicate-actions">
+ <button type="button" class="in-page" id="mark-as-dup-btn">
+ Mark as Duplicate
+ </button>
+ </div>
+ [% END %]
+ [% END %]
+[% END %]
+
+[%# === people === %]
+
+[%
+ unassigned = (bug.assigned_to.login == "nobody@mozilla.org")
+ || (bug.assigned_to.login.search("\.bugs$"));
+ sub =
+ "Reporter: " _ bug.reporter.moz_nick
+ _ (unassigned ? ", Unassigned" : ", Assigned: " _ bug.assigned_to.moz_nick)
+ _ (bug.mentors.size ? ", Mentored" : "")
+ _ (needinfo.size ? ", NeedInfo" : "")
+%]
+[% WRAPPER bug_modal/module.html.tmpl
+ title = "People"
+ subtitle = sub
+ collapsed = 1
+%]
+ [% WRAPPER fields_lhs %]
+
+ [%# assignee %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ field = bug_fields.assigned_to
+ field_type = constants.FIELD_TYPE_USER
+ %]
+ [% IF unassigned %]
+ <i>Unassigned</i>
+ [% IF bug.check_can_change_field("assigned_to", 0, 1) %]
+ <button type="button" id="take-btn" class="in-page">Take</button>
+ [% END %]
+ [% ELSE %]
+ [% INCLUDE bug_modal/user.html.tmpl u=bug.assigned_to %]
+ [% END %]
+ [% END %]
+
+ [%# mentors %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ field = bug_fields.bug_mentor
+ field_type = constants.FIELD_TYPE_USERS
+ label = "Mentors"
+ value = bug.mentors.pluck("login")
+ hide_on_view = bug.mentors.size == 0
+ %]
+ [%
+ FOREACH mentor IN bug.mentors;
+ INCLUDE bug_modal/user.html.tmpl u=mentor;
+ END;
+ %]
+ [% END %]
+
+ [%# qa contact %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ field = bug_fields.qa_contact
+ field_type = constants.FIELD_TYPE_USER
+ hide_on_view = bug.qa_contact == ""
+ %]
+ [% INCLUDE bug_modal/user.html.tmpl u=bug.qa_contact %]
+ [% END %]
+
+ [% END %]
+ [% WRAPPER fields_rhs %]
+
+ [%# reporter %]
+ [% INCLUDE bug_modal/field.html.tmpl
+ field = bug_fields.reporter
+ field_type = constants.FIELD_TYPE_USER
+ view_only = 1
+ %]
+
+ [%# needinfo %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ container = 1
+ label = "NeedInfo"
+ hide_on_view = needinfo.size == 0
+ hide_on_edit = 1
+ %]
+ [% INCLUDE bug_modal/flags.html.tmpl
+ types = bug.flag_types.only("name", "needinfo")
+ no_label = 1
+ view_only = 1
+ %]
+ [% END %]
+ [% IF needinfo.size %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ container = 1
+ label = "NeedInfo"
+ hide_on_view = 1
+ %]
+ <button type="button" id="needinfo-scroll" class="in-page">Update</button>
+ [% END %]
+ [% END %]
+
+ [%# cc %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ container = 1
+ label = "CC"
+ hide_on_view = bug.cc.size == 0
+ %]
+ [% IF bug.cc && bug.cc.size %]
+ <span id="cc-latch">&#9656;</span>
+ <span id="cc-summary">
+ [%
+ IF bug.cc.size == 1;
+ is_cced ? "Just you" : "1 person";
+ ELSE;
+ bug.cc.size _ " people";
+ END;
+ %]
+ </span>
+ <div id="cc-list" style="display:none"></div>
+ [% ELSE %]
+ <i>Nobody</i>
+ [% END %]
+ [% END %]
+
+ [% END %]
+[% END %]
+
+[%# === tracking === %]
+
+[%
+ col =
+ (bug.version.lower == "unspecified" || bug.version.lower == "other")
+ && bug.target_milestone == "---"
+ && !has_bug_flags
+ && !set_project_flags.size
+ && !set_tracking_flags.size;
+ sub = [];
+ IF col;
+ sub.push("Not tracked");
+ END;
+ open_deps = bug.depends_on_obj.only("resolution", "").size;
+ IF open_deps;
+ sub.push("Depends on: " _ open_deps _ " bug" _ (open_deps == 1 ? "" : "s"));
+ END;
+ open_deps = bug.blocks_obj.only("resolution", "").size;
+ IF open_deps;
+ sub.push("Blocks: " _ open_deps _ " bug" _ (open_deps == 1 ? "" : "s"));
+ END;
+%]
+[% WRAPPER bug_modal/module.html.tmpl
+ title = "Tracking"
+ subtitle = sub.join(", ")
+ collapsed = col
+%]
+ [% WRAPPER fields_lhs %]
+
+ [%# version %]
+ [% INCLUDE bug_modal/field.html.tmpl
+ field = bug_fields.version
+ field_type = constants.FIELD_TYPE_SINGLE_SELECT
+ %]
+
+ [%# milestone %]
+ [% INCLUDE bug_modal/field.html.tmpl
+ field = bug_fields.target_milestone
+ field_type = constants.FIELD_TYPE_SINGLE_SELECT
+ label = "Target"
+ %]
+
+ [%# platform, op-sys %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ container = 1
+ label = "Platform"
+ hide_on_view = bug.rep_platform == 'All' && bug.op_sys == 'All'
+ %]
+ [% INCLUDE bug_modal/field.html.tmpl
+ field = bug_fields.rep_platform
+ field_type = constants.FIELD_TYPE_SINGLE_SELECT
+ inline = 1
+ no_indent = 1
+ %]
+ [% INCLUDE bug_modal/field.html.tmpl
+ field = bug_fields.op_sys
+ field_type = constants.FIELD_TYPE_SINGLE_SELECT
+ inline = 1
+ %]
+ [% END %]
+
+ [%# keywords %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ field = bug_fields.keywords
+ field_type = constants.FIELD_TYPE_KEYWORDS
+ hide_on_view = bug.keyword_objects.size == 0
+ %]
+ [% bug.keyword_objects.pluck("name").join(", ") FILTER html %]
+ [% END %]
+
+ [% UNLESS cf_hidden_in_product('cf_fx_iteration', bug.product, bug.component) %]
+ [% rendered_custom_fields.push('cf_fx_iteration') %]
+ [% INCLUDE bug_modal/field.html.tmpl
+ field = bug_fields.cf_fx_iteration
+ field_type = bug_fields.cf_fx_iteration.type
+ hide_on_view = bug.cf_iteration == ""
+ %]
+ [% END %]
+
+ [% UNLESS cf_hidden_in_product('cf_fx_points', bug.product, bug.component) %]
+ [% rendered_custom_fields.push('cf_fx_points') %]
+ [% INCLUDE bug_modal/field.html.tmpl
+ field = bug_fields.cf_fx_points
+ field_type = bug_fields.cf_fx_points.type
+ hide_on_view = bug.cf_points == ""
+ %]
+ [% END %]
+
+ [% END %]
+ [% WRAPPER fields_rhs %]
+
+ [%# depends on %]
+ [% INCLUDE bug_modal/field.html.tmpl
+ field = bug_fields.dependson
+ field_type = constants.FIELD_TYPE_BUG_LIST
+ values = bug.depends_on_obj
+ hide_on_view = bug.dependson.size == 0
+ %]
+
+ [%# blocks %]
+ [% INCLUDE bug_modal/field.html.tmpl
+ field = bug_fields.blocked
+ field_type = constants.FIELD_TYPE_BUG_LIST
+ values = bug.blocks_obj
+ hide_on_view = bug.blocked.size == 0
+ %]
+
+ [% IF bug.dependson.size + bug.blocked.size > 1 %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ container = 1
+ label = ""
+ hide_on_edit = 1
+ %]
+ Dependency <a href="showdependencytree.cgi?id=[% bug.bug_id FILTER none %]&amp;hide_resolved=1">tree</a>
+ / <a href="showdependencygraph.cgi?id=[% bug.bug_id FILTER none %]">graph</a>
+ [% END %]
+ [% END %]
+
+ [%# duplicates %]
+ [% IF bug.duplicates.size %]
+ [% INCLUDE bug_modal/field.html.tmpl
+ label = "Duplicates"
+ values = bug.duplicates
+ field_type = constants.FIELD_TYPE_BUG_LIST
+ view_only = 1
+ %]
+ [% END %]
+
+ [%# flags %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ name = "bug_flags"
+ container = 1
+ label = terms.Bug _ " Flags"
+ hide_on_view = !has_bug_flags
+ %]
+ [% INCLUDE bug_modal/flags.html.tmpl
+ types = bug.flag_types.skip("name", "needinfo")
+ %]
+ [% END %]
+
+ [% END %]
+[% END %]
+
+[% IF tracking_flags.size %]
+
+ [%# === tracking flags === %]
+
+ [% WRAPPER bug_modal/module.html.tmpl
+ title = tracking_flags_title
+ collapsed = 1
+ subtitle = firefox_flags_subtitle
+ %]
+ [% WRAPPER fields_lhs %]
+
+ [% UNLESS set_tracking_flags.size || set_project_flags.size %]
+ <p class="edit-hide">
+ This [% terms.bug %] is not currently tracked.
+ </p>
+ [% END %]
+
+ [%# tracking flags %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ container = 1
+ label = "Tracking Flags"
+ hide_on_view = set_tracking_flags.size == 0
+ %]
+ [% INCLUDE bug_modal/tracking_flags.html.tmpl
+ type = "tracking"
+ %]
+ [% END %]
+
+ [% END %]
+ [% WRAPPER fields_rhs %]
+
+ [%# project flags %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ container = 1
+ label = "Project Flags"
+ hide_on_view = set_project_flags.size == 0
+ %]
+ [% INCLUDE bug_modal/tracking_flags.html.tmpl
+ type = "project"
+ %]
+ [% END %]
+
+ [% END %]
+ [% END %]
+
+[% END %]
+
+[%# === details === %]
+
+[%
+ sub = [];
+ IF bug.status_whiteboard != "";
+ sub.push(bug.status_whiteboard.truncate(256, '&hellip;'));
+ END;
+ IF bug.cf_crash_signature != "";
+ sub.push("crash signature");
+ END;
+%]
+[% WRAPPER bug_modal/module.html.tmpl
+ title = "Details"
+ collapsed = 1
+ subtitle = sub.join(", ")
+%]
+ [% WRAPPER fields_lhs %]
+
+ [%# whiteboard %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ field = bug_fields.status_whiteboard
+ field_type = constants.FIELD_TYPE_FREETEXT
+ %]
+ [% bug.status_whiteboard == "" ? "---" : bug.status_whiteboard FILTER html %]
+ [% END %]
+
+ [%# votes %]
+ [% IF bug.product_obj.votesperuser %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ container = 1
+ label = "Votes"
+ %]
+ [% bug.votes FILTER html %]
+ vote[% "s" IF bug.votes != 1 %]
+ [% IF user.id %]
+ <button type="button" class="minor" id="vote-btn">
+ [% bug.user_votes ? "Remove vote" : "Vote" %]
+ </button>
+ [% END %]
+ [% END %]
+ [% END %]
+
+ [%# custom fields (except textarea) %]
+ [%
+ FOREACH field = custom_fields;
+ NEXT IF field.type == constants.FIELD_TYPE_EXTENSION || field.type == constants.FIELD_TYPE_TEXTAREA;
+ NEXT IF rendered_custom_fields.exists(field.name);
+ NEXT IF cf_hidden_in_product(field.name, bug.product, bug.component);
+ cf_value = bug.${field.name};
+ IF field.type == constants.FIELD_TYPE_SINGLE_SELECT;
+ has_value = cf_value != "---";
+ ELSIF field.type == constants.FIELD_TYPE_MULTI_SELECT;
+ has_value = cf_value.size != 0;
+ ELSE;
+ has_value = cf_value != "";
+ END;
+ INCLUDE bug_modal/field.html.tmpl
+ field = field
+ field_type = field.type
+ hide_on_view = !has_value;
+ END;
+ %]
+
+ [% END %]
+ [% WRAPPER fields_rhs %]
+
+ [%# url %]
+ [% WRAPPER bug_modal/field.html.tmpl
+ field = bug_fields.bug_file_loc
+ field_type = constants.FIELD_TYPE_FREETEXT
+ hide_on_view = bug.bug_file_loc == ""
+ %]
+ <a href="[% bug.bug_file_loc FILTER html %]" target="_blank"
+ rel="noreferrer" title="[% bug.bug_file_loc FILTER html %]"
+ [% UNLESS is_safe_url(bug.bug_file_loc) +%] class="unsafe-url"[% END %]
+ >[% bug.bug_file_loc FILTER truncate(40) FILTER html %]</a>
+ [% END %]
+
+ [%# see also %]
+ [% INCLUDE bug_modal/field.html.tmpl
+ field = bug_fields.see_also
+ field_type = constants.FIELD_TYPE_BUG_URLS
+ values = bug.see_also
+ hide_on_view = bug.see_also.size == 0
+ %]
+
+ [% END %]
+
+ [%# custom fields (textarea) %]
+ [%
+ FOREACH field IN custom_fields;
+ NEXT IF field.type != constants.FIELD_TYPE_TEXTAREA;
+ NEXT IF rendered_custom_fields.exists(field.name);
+ INCLUDE bug_modal/field.html.tmpl
+ field = field
+ field_type = field.type
+ hide_on_view = bug.${field.name} == "" || bug.${field.name} == "---";
+ END;
+ %]
+[% END %]
+
+[%# === groups === %]
+
+[% WRAPPER bug_modal/module.html.tmpl
+ title = "Security"
+ collapsed = 1
+ subtitle = bug.groups_in.size ? bug.groups_in.pluck("description").join(", ") : "public"
+ hide_on_view = bug.groups_in.size == 0
+ hide_on_edit = bug.groups.size == 0
+%]
+ [% INCLUDE bug_modal/groups.html.tmpl %]
+[% END %]
+
+[%# === user story === %]
+
+[% IF bug.user_story_visible.0 %]
+ [% WRAPPER bug_modal/module.html.tmpl
+ title = "User Story"
+ hide_on_view = bug.cf_user_story == ""
+ collapsed = bug.cf_user_story == ""
+ %]
+ [% IF user.id %]
+ <div id="user-story-actions">
+ [% IF bug.check_can_change_field('cf_user_story', 0, 1) %]
+ <button type="button" class="in-page" id="user-story-edit-btn">Edit</button>
+ [% END %]
+ [% IF bug.cf_user_story != "" && bug.check_can_change_field('longdesc', 0, 1) %]
+ <button type="button" class="in-page" id="user-story-reply-btn">Reply</button>
+ [% END %]
+ </div>
+ [% END %]
+ <pre id="user-story">[% bug.cf_user_story FILTER html %]</pre>
+ [% IF user.id %]
+ <textarea id="cf_user_story" name="cf_user_story" style="display:none" rows="10" cols="80">
+ [%~ bug.cf_user_story FILTER html ~%]
+ </textarea>
+ [% END %]
+ [% END %]
+[% END %]
+
+[%# === attachments === %]
+
+[% IF active_attachments || obsolete_attachments %]
+ [%
+ sub = [];
+ IF active_attachments;
+ sub.push(active_attachments _ " attachment" _ (active_attachments == 1 ? "" : "s"));
+ END;
+ IF obsolete_attachments;
+ sub.push(obsolete_attachments _ " obsolete attachment" _ (obsolete_attachments == 1 ? "" : "s"));
+ END;
+ %]
+ [% WRAPPER bug_modal/module.html.tmpl
+ title = "Attachments"
+ subtitle = sub.join(", ")
+ collapsed = active_attachments == 0
+ %]
+ [% INCLUDE bug_modal/attachments.html.tmpl %]
+ [% IF obsolete_attachments %]
+ <div id="attachments-actions">
+ <button type="button" id="attachments-obsolete-btn" class="in-page">Show Obsolete Attachments</button>
+ </div>
+ [% END %]
+ [% END %]
+[% END %]
+
+[%# === top (between modules and comments) actions === %]
+
+[% IF user.id %]
+ <div id="top-actions">
+ <button type="button" id="attachments-add-btn" class="minor">Attach File</button>
+ <button type="button" class="comment-btn in-page">Add Comment</button>
+ <button type="submit" class="save-btn" id="top-save-btn" style="display:none">Save Changes</button>
+ </div>
+[% END %]
+
+[%# === comments === %]
+
+[%
+ INCLUDE bug_modal/activity_stream.html.tmpl stream=bug.activity_stream;
+ IF user.id;
+ INCLUDE bug_modal/new_comment.html.tmpl;
+ END;
+%]
+
+[%# === bottom actions === %]
+
+[% IF user.id %]
+ <div id="bottom-actions">
+ <button type="submit" class="save-btn" id="bottom-save-btn">Save Changes</button>
+ [%
+ IF bug.resolution == "";
+ seen_header = 0;
+ FOREACH resolution IN ["FIXED", "INVALID", "DUPLICATE"];
+ NEXT UNLESS bug.choices.resolution.only("name", resolution).size;
+ IF NOT seen_header;
+ seen_header = 1;
+ " Resolve as ";
+ END;
+ %] <button type="button" class="in-page resolution-btn">[% resolution FILTER html %]</button> [%
+ END;
+ ELSIF bug.choices.bug_status.only("name", "REOPENED").size;
+ %] <button type="button" class="in-page status-btn" data-status="REOPENED">REOPEN</button> [%
+ END;
+ %]
+ <div id="bottom-right-actions">
+ <button type="button" id="top-btn" class="in-page">Top &uarr;</button>
+ <button type="button" id="new-bug-btn" class="minor">New [% terms.Bug %] &#9662;</button>
+ </div>
+ </div>
+ </form>
+[% ELSE %]
+ <div id="login-required">
+ You need to <a href="show_bug.cgi?id=[% bug.bug_id FILTER none %]&amp;GoAheadAndLogIn=1">log in</a>
+ before you can comment on or make changes to this [% terms.bug %].
+ </div>
+[% END %]
+
+[%# === blocks === %]
+
+[% BLOCK fields_lhs %]
+ <div class="fields-lhs">[% content FILTER none %]</div>
+[% END %]
+
+[% BLOCK fields_rhs %]
+ <div class="fields-rhs">[% content FILTER none %]</div>
+[% END %]
diff --git a/extensions/BugModal/template/en/default/bug_modal/field.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/field.html.tmpl
new file mode 100644
index 000000000..d9b5873d9
--- /dev/null
+++ b/extensions/BugModal/template/en/default/bug_modal/field.html.tmpl
@@ -0,0 +1,275 @@
+[%# 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.
+ #%]
+
+[%#
+ # field: (field object) bug_fields.$field_name object
+ # field_type: (const) constants.FIELD_TYPE_*
+ # no_label: (boolean) don't output label
+ # label: (string) field label text (default: field_descs.${$field.name}
+ # view_only: (boolean) don't allow editing (default: determined from bug.check_can_change_field)
+ # edit_only: (boolean) always render the edit ui
+ # container: (boolean) output just a label and the content (eg. for multiple fields next to one label)
+ # value: (string) visible value (default: bug.$name)
+ # values: (array of string) list of value objects (FIELD_TYPE_SINGLE_SELECT and _BUG_URLS only) (default: lazy-load on edit)
+ # inline: (boolean) output field as a table-cell instead of as a stand-alone div (default: false)
+ # no_indent: (boolean) don't indent the field (left-padding) (default: false)
+ # full_width: (boolean) the field takes up the complete page width (default: false)
+ # short_width: (boolean) the field shouldn't take up much space at all (default: false)
+ # hide_on_view: (boolean) hide field from read-only view (default: false)
+ # hide_on_edit: (boolean) hide content when in edit mode (default: false)
+ #%]
+
+[%
+IF field_type.defined && !field;
+ RETURN;
+END;
+IF !name.defined;
+ name = field.name;
+END;
+IF !value.defined;
+ value = bug.$name;
+END;
+IF hide_on_edit;
+ view_only = 1;
+END;
+IF view_only || container;
+ editable = 0;
+END;
+IF !editable.defined;
+ editable = bug.check_can_change_field(name, 0, 1);
+END;
+IF inline && !label.defined;
+ no_label = 1;
+END;
+IF !no_label && !label.defined;
+ label = field_descs.${field.name};
+END;
+IF field_type == "";
+ field_type = -1;
+END;
+IF field_type == constants.FIELD_TYPE_DATE
+ || field_type == constants.FIELD_TYPE_DATETIME;
+ short_width = 1;
+END;
+%]
+
+<div class="field
+ [%~ " indent" IF no_label && !no_indent %]
+ [%~ " inline" IF inline %]
+ [%~ " edit-hide" IF hide_on_edit %]
+ [%~ " edit-show" IF hide_on_view && !hide_on_edit %]"
+ [% IF name %] id="field-[% name FILTER html %]"[% END %]
+ [% IF hide_on_view %] style="display:none"[% END %]
+>
+ [% IF label.defined && !no_label %]
+ <div class="name">[% label _ ":" FILTER html IF label %]</div>
+ [% END %]
+
+ [%# read-only html %]
+ [% UNLESS edit_only %]
+ <div class="[% "value" IF !container %][% " edit-hide" IF editable %][% " container" IF container %]">
+ [% IF name %]
+ <span id="field-value-[% name FILTER html %]">
+ [% END %]
+ [% IF content.defined %]
+ [% content FILTER none %]
+ [% ELSE %]
+ [% SWITCH field_type %]
+
+ [% CASE constants.FIELD_TYPE_USER %]
+ [%# users %]
+ [% INCLUDE bug_modal/user.html.tmpl u=value %]
+
+ [% CASE constants.FIELD_TYPE_BUG_URLS %]
+ [%# see also %]
+ [% INCLUDE bug_urls values=values edit=0 %]
+
+ [% CASE constants.FIELD_TYPE_BUG_LIST %]
+ [%# bug lists (generally dependancies) %]
+ [% INCLUDE bug_list values=values edit=0 %]
+
+ [% CASE constants.FIELD_TYPE_TEXTAREA %]
+ [%# text areas %]
+ <span class="multiline-value">[% value FILTER html FILTER html_line_break %]</span>
+
+ [% CASE constants.FIELD_TYPE_MULTI_SELECT %]
+ [%# multi-select %]
+ [% value.join(", ") FILTER html %]
+
+ [% CASE constants.FIELD_TYPE_DATETIME %]
+ [%# datetime %]
+ [% value FILTER time %]
+
+ [% CASE constants.FIELD_TYPE_DATE %]
+ [%# date %]
+ [% value FILTER time("%Y-%m-%d") %]
+
+ [% CASE constants.FIELD_TYPE_BUG_ID %]
+ [%# bug id %]
+ [% value FILTER bug_link(value, use_alias => 1) FILTER none %]
+
+ [% CASE %]
+ [%# every else %]
+ [% value FILTER html %]
+
+ [% END %]
+ [% END %]
+ [% IF name %]
+ </span>
+ [% END %]
+ </div>
+ [% END %]
+
+ [%# edit html %]
+ [% IF editable %]
+ [% "MISSING NAME" UNLESS name %]
+ <div class="value edit edit-show [% " wide" IF full_width %][% " short" IF short_width %]"
+ [% UNLESS edit_only +%] style="display:none"[% END %]>
+ [% SWITCH field_type %]
+
+ [% CASE constants.FIELD_TYPE_SINGLE_SELECT %]
+ [%# single value select %]
+ <select name="[% name FILTER html %]" id="[% name FILTER html %]">
+ [% IF values.defined %]
+ [% FOREACH v IN values %]
+ [% NEXT IF NOT v.is_active AND NOT value.contains(v.name).size %]
+ <option value="[% v.name FILTER html %]"
+ id="v[% v.id FILTER html %]_[% name FILTER html %]"
+ [% " selected" IF value.contains(v.name).size %]
+ >[% v.name FILTER html %]</option>
+ [% END %]
+ [% ELSE %]
+ <option value="[% value FILTER html %]" selected>[% value FILTER html %]</option>
+ [% END %]
+ </select>
+
+ [% CASE constants.FIELD_TYPE_MULTI_SELECT %]
+ [%# multi value select %]
+ <select name="[% name FILTER html %]" id="[% name FILTER html %]" multiple size="5">
+ [% IF values.defined %]
+ [%# not implemented %]
+ [% ELSE %]
+ [% FOREACH v IN value %]
+ <option value="[% v FILTER html %]" selected>[% v FILTER html %]</option>
+ [% END %]
+ [% END %]
+ </select>
+
+ [% CASE constants.FIELD_TYPE_FREETEXT %]
+ [%# normal input field %]
+ <input name="[% name FILTER html %]" id="[% name FILTER html %]" value="[% value FILTER html %]">
+
+ [% CASE constants.FIELD_TYPE_USER %]
+ [%# single user %]
+ [% INCLUDE global/userselect.html.tmpl
+ id = name
+ name = name
+ value = value.login
+ classes = [ "bz_userfield" ]
+ %]
+
+ [% CASE constants.FIELD_TYPE_USERS %]
+ [%# multiple users %]
+ [% INCLUDE global/userselect.html.tmpl
+ id = name
+ name = name
+ value = value.join(", ")
+ classes = [ "bz_userfield" ]
+ multiple = 5
+ %]
+
+ [% CASE constants.FIELD_TYPE_KEYWORDS %]
+ [%# keywords %]
+ <input type="text" id="[% name FILTER html %]" name="[% name FILTER html %]"
+ value="[% value FILTER html %]">
+
+ [% CASE constants.FIELD_TYPE_BUG_URLS %]
+ [%# see also %]
+ [% INCLUDE bug_urls values=values edit=1 %]
+
+ [% CASE constants.FIELD_TYPE_BUG_LIST %]
+ [%# bug lists %]
+ [% INCLUDE bug_list values=values edit=1 %]
+
+ [% CASE constants.FIELD_TYPE_TEXTAREA %]
+ [%# text area %]
+ <button type="button" class="in-page edit-textarea-btn [%= "edit-textarea-set-btn" IF value != "" %]"
+ id="[% name FILTER html %]-edit">Edit</button>
+ <span class="multiline-value" id="[% name FILTER html %]-view">[% value FILTER html FILTER html_line_break %]</span>
+ <textarea id="[% name FILTER html %]" name="[% name FILTER html %]"
+ rows="10" cols="10" style="display:none">[% value FILTER html %]</textarea>
+
+ [% CASE constants.FIELD_TYPE_DATETIME %]
+ [%# datetime %]
+ <input class="cf_datetime" name="[% name FILTER html %]" id="[% name FILTER html %]"
+ value="[% value FILTER html %]">
+ <img class="cf_datetime-img" id="[% name FILTER html %]-img"
+ src="extensions/BugModal/web/calendar.png" width="16" height="16">
+
+ [% CASE constants.FIELD_TYPE_DATE %]
+ [%# date %]
+ <input class="cf_date" name="[% name FILTER html %]" id="[% name FILTER html %]"
+ value="[% value FILTER html %]">
+ <img class="cf_date-img" id="[% name FILTER html %]-img"
+ src="extensions/BugModal/web/calendar.png" width="16" height="16">
+
+ [% CASE constants.FIELD_TYPE_INTEGER %]
+ [%# integer %]
+ <input type="number" name="[% name FILTER html %]" id="[% name FILTER html %]"
+ value="[% value FILTER html %]">
+
+ [% CASE constants.FIELD_TYPE_BUG_ID %]
+ [%# bug id %]
+ <input type="text" name="[% name FILTER html %]" id="[% name FILTER html %]"
+ value="[% value FILTER html %]">
+
+ [% CASE %]
+ [%# error %]
+ ('[% name FILTER html %]' [[% field_type FILTER html %])] not supported)
+
+ [% END %]
+ </div>
+ [% END %]
+</div>
+
+[%# bug-urls, currently just see-also %]
+[% BLOCK bug_urls %]
+ [% FOREACH url IN values %]
+ <div>
+ [% IF url.isa('Bugzilla::BugUrl::Bugzilla::Local') %]
+ [% url.target_bug_id FILTER bug_link(url.target_bug_id, use_alias => 1) FILTER none %]
+ [% ELSE %]
+ <a href="[% url.name FILTER html %]">[% url.name FILTER html %]</a>
+ [% END %]
+ [% IF edit %]
+ <label>
+ <input type="checkbox" name="remove_see_also" value="[% url.name FILTER html %]">
+ Remove
+ </label>
+ [% END %]
+ </div>
+ [% END %]
+
+ [% IF edit %]
+ <button id="[% name FILTER html %]-btn" class="bug-urls-btn in-page">Add</button>
+ <input id="[% name FILTER html %]" name="[% name FILTER html %]" style="display:none">
+ [% END %]
+[% END %]
+
+[%# bug lists, depencancies, blockers %]
+[% BLOCK bug_list %]
+ [% IF NOT edit %]
+ [% FOREACH b IN values %]
+ [% INCLUDE bug/link.html.tmpl bug=b link_text=b.id use_alias=1 %]
+ [% ", " UNLESS loop.last %]
+ [% END %]
+ [% ELSE %]
+ <input type="text" id="[% name FILTER html %]" name="[% name FILTER html %]"
+ value="[% values.pluck('id').join(", ") FILTER html %]">
+ [% END %]
+[% END %]
diff --git a/extensions/BugModal/template/en/default/bug_modal/flags.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/flags.html.tmpl
new file mode 100644
index 000000000..4f2381913
--- /dev/null
+++ b/extensions/BugModal/template/en/default/bug_modal/flags.html.tmpl
@@ -0,0 +1,162 @@
+[%# 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.
+ #%]
+
+[%#
+ # types: array of flag_type objects
+ # no_label: if set to a true value, flag name and status will not be outputted (default: false)
+ # read_only: if true, don't output edit ui (default: false)
+ #%]
+
+[% IF read_only %]
+ <div class="flags edit-hide">
+ [% FOREACH type IN types %]
+ [% FOREACH flag IN type.flags %]
+ <div class="flag">
+ [% UNLESS no_label %]
+ [% INCLUDE bug_modal/user.html.tmpl u=flag.setter nick_only=1 %]
+ [%+ flag.type.name FILTER html %][% flag.status FILTER none %]
+ [% END %]
+ [% IF flag.requestee %]
+ [%+ INCLUDE bug_modal/user.html.tmpl u=flag.requestee nick_only=1 %]
+ [% END %]
+ </div>
+ [% END %]
+ [% END %]
+ </div>
+ [% RETURN %]
+[% END %]
+
+<div id="bug-flags" class="flags">
+ <table class="layout-table">
+ [%
+ FOREACH type IN types;
+ FOREACH flag IN type.flags;
+ IF flag.requestee && flag.requestee.id == user.id;
+ INCLUDE edit_flag t=type f=flag;
+ ELSE;
+ %]
+ <tbody class="edit-hide">
+ [% INCLUDE view_flag t=type f=flag %]
+ </tbody>
+ <tbody class="edit-show" style="display:none">
+ [% INCLUDE edit_flag t=type f=flag %]
+ </tbody>
+ [%
+ END;
+ END;
+ END;
+ %]
+ <tbody class="edit-show" style="display:none">
+ [%
+ FOREACH type IN types;
+ NEXT IF !type.is_active || type.flags.size;
+ INCLUDE edit_flag t=type;
+ END;
+
+ FOREACH type IN types;
+ NEXT IF !type.is_active || !type.is_multiplicable;
+ INCLUDE edit_flag t=type;
+ END;
+ %]
+ </tbody>
+ </table>
+</div>
+
+[% BLOCK view_flag %]
+ <tr>
+ <td class="flag-setter">
+ [% INCLUDE bug_modal/user.html.tmpl u=f.setter nick_only=1 %]
+ </td>
+
+ <td class="flag-name">
+ <span class="rel-time" title="[% f.creation_date FILTER time_duration FILTER html %]">
+ [% f.type.name FILTER html %]
+ </span>
+ </td>
+
+ <td class="flag-value">
+ [% f.status FILTER html %]
+ </td>
+
+ [% IF f.requestee %]
+ <td class="flag-requestee">
+ [% INCLUDE bug_modal/user.html.tmpl u=f.requestee nick_only=1 %]
+ </td>
+ [% END %]
+ </tr>
+[% END %]
+
+[% BLOCK edit_flag %]
+[%
+ can_edit = !f || (f.setter_id == user.id || (f.requestee_id && f.requestee_id == user.id))
+ flag_id = f ? "flag-$f.id" : "flag_type-$t.id";
+%]
+ <tr>
+ <td class="flag-setter">
+ [% IF f %]
+ [% INCLUDE bug_modal/user.html.tmpl u=flag.setter nick_only=1 %]
+ [% ELSIF t.flags.size %]
+ addl.
+ [% END %]
+ </td>
+
+ <td class="flag-name">
+ <label title="[% t.description FILTER html %]" for="[% flag_id FILTER html %]">
+ [%~ t.name FILTER html FILTER no_break ~%]
+ </label>
+ </td>
+
+ <td class="flag-value">
+ <select id="[% flag_id FILTER html %]" name="[% flag_id FILTER html %]"
+ title="[% t.description FILTER html %]"
+ [% UNLESS (t.is_requestable && user.can_request_flag(t)) || user.can_set_flag(t) %]
+ disabled
+ [% END %]
+ class="bug-flag">
+ [% IF !f || (can_edit && user.can_request_flag(t)) || f.setter_id == user.id %]
+ <option value="X"></option>
+ [% END %]
+ [% IF t.is_active && can_edit %]
+ [% IF (t.is_requestable && user.can_request_flag(t)) || (f && f.status == "?") %]
+ <option value="?" [% "selected" IF f && f.status == "?" %]>?</option>
+ [% END %]
+ [% IF user.can_set_flag(t) || (f && f.status == "+") %]
+ <option value="+" [% "selected" IF f && f.status == "+" %]>+</option>
+ [% END %]
+ [% IF user.can_set_flag(t) || (f && f.status == "-") %]
+ <option value="-" [% "selected" IF f && f.status == "-" %]>-</option>
+ [% END %]
+ [% ELSE %]
+ <option value="[% f.status FILTER html %]" selected>[% f.status FILTER html %]</option>
+ [% END %]
+ </select>
+ </td>
+
+ [% IF (t.is_requestable && t.is_requesteeble) || (f && f.requestee) %]
+ <td class="flag-requestee">
+ [% flag_name = f ? "requestee-$f.id" : "requestee_type-$t.id" %]
+ <div id="[% flag_name FILTER none %]-container"
+ [% UNLESS f && f.requestee +%] style="display:none"[% END %]>
+ [%
+ flag_requestee = (f && f.requestee) ? f.requestee.login : '';
+ flag_multiple = f ? 0 : t.is_multiplicable * 3;
+ flag_empty_ok = f ? 1 : !t.is_multiplicable;
+ INCLUDE global/userselect.html.tmpl
+ name = flag_name
+ id = flag_name
+ value = flag_requestee
+ emptyok = flag_empty_ok
+ classes = [ "requestee" ]
+ disabled = !can_edit
+ %]
+ </div>
+ <td>
+ [% END %]
+
+ </tr>
+[% END %]
diff --git a/extensions/BugModal/template/en/default/bug_modal/groups.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/groups.html.tmpl
new file mode 100644
index 000000000..b03db1e49
--- /dev/null
+++ b/extensions/BugModal/template/en/default/bug_modal/groups.html.tmpl
@@ -0,0 +1,68 @@
+[%# 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.
+ #%]
+
+[%#
+ # bug: bug object
+ #%]
+
+[%
+ PROCESS global/variables.none.tmpl;
+
+ in_all_groups = 1;
+ in_a_group = 0;
+ FOREACH group IN bug.groups;
+ IF NOT group.ingroup;
+ in_all_groups = 0;
+ END;
+ IF group.ison;
+ in_a_group = 1;
+ END;
+ END
+%]
+
+<div class="groups edit-hide">
+ [% IF in_a_group %]
+ <div id="groups-description">
+ Only users in all of the following groups can view this [% terms.bug %]:
+ </div>
+ <ul>
+ [% FOREACH group IN bug.groups %]
+ [% NEXT UNLESS group.ison || group.mandatory %]
+ <li>[% group.description FILTER html %]</li>
+ [% END %]
+ </ul>
+ [% ELSE %]
+ This [% terms.bug %] is publicaly visible.
+ [% END %]
+</div>
+
+<div class="groups edit-show" style="display:none">
+ [% emitted_description = 0 %]
+ [% FOREACH group IN bug.groups %]
+ [% IF NOT emitted_description %]
+ [% emitted_description = 1 %]
+ <div id="groups-description">
+ Only users in all of the selected groups can view this [% terms.bug %]:
+ </div>
+ [% END %]
+
+ [% IF group.ingroup %]
+ <input type="hidden" name="defined_groups" value="[% group.name FILTER html %]">
+ [% END %]
+
+ <div class="group">
+ <input type="checkbox" value="[% group.name FILTER html %]"
+ name="groups" id="group_[% group.bit FILTER html %]"
+ [% " checked" IF group.ison %]
+ [% " disabled" IF NOT group.ingroup || group.mandatory %]>
+ <label for="group_[% group.bit FILTER html %]">
+ [%~ group.description FILTER html_light ~%]
+ </label>
+ </div>
+ [% END %]
+</div>
diff --git a/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl
new file mode 100644
index 000000000..c6dd8b74c
--- /dev/null
+++ b/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl
@@ -0,0 +1,100 @@
+[%# 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;
+
+ # <title>
+ IF bugs.defined;
+ bug = bugs.0;
+ END;
+ title = "$bug.bug_id - ";
+ IF bug.alias;
+ title = title _ "($bug.alias) ";
+ END;
+ unfiltered_title = title _ bug.short_desc;
+ filtered_desc = bug.short_desc FILTER html;
+ title = title _ filtered_desc;
+
+ generate_api_token = 1;
+
+ # these aren't always defined
+ UNLESS bodyclasses.defined;
+ bodyclasses = [];
+ END;
+ UNLESS javascript_urls.defined;
+ javascript_urls = [];
+ END;
+ UNLESS style_urls.defined;
+ style_urls = [];
+ END;
+ UNLESS jquery.defined;
+ jquery = [];
+ END;
+
+ # right now we need yui for the user fields
+ no_yui = 0;
+ yui = ['autocomplete'];
+
+ # add body classes for sec-groups, etc
+ FOREACH group IN bug.groups_in;
+ bodyclasses.push("bz_group_$group.name");
+ END;
+ bodyclasses.push("bug_modal");
+
+ # assets
+ javascript_urls.push(
+ "extensions/BugModal/web/bug_modal.js",
+ "extensions/BugModal/web/ZeroClipboard/ZeroClipboard.min.js",
+ "js/field.js",
+ "js/comments.js",
+ );
+ jquery.push(
+ "datetimepicker",
+ );
+ style_urls.push(
+ "extensions/BugModal/web/bug_modal.css",
+ "skins/custom/bug_groups.css",
+ "js/jquery/plugins/datetimepicker/datetimepicker.css",
+ );
+
+ IF user.in_group('canconfirm');
+ style_urls.push('extensions/TagNewUsers/web/style.css');
+ END;
+%]
+
+[% javascript = BLOCK %]
+ [%# add tracking flags json if available %]
+ [% IF tracking_flags %]
+ [% javascript_urls.push("extensions/TrackingFlags/web/js/tracking_flags.js") %]
+ TrackingFlags = [% tracking_flags_json FILTER none %];
+ [% END %]
+
+ [%# update last-visited %]
+ [% IF user.id && user.is_involved_in_bug(bug) %]
+ $(function() {
+ bugzilla_ajax({
+ url: 'rest/bug_user_last_visit/[% bug.id FILTER none %]',
+ type: 'POST'
+ });
+ });
+ [% END %]
+
+ [%# expose useful data to js %]
+ BUGZILLA.bug_id = [% bug.id FILTER none %];
+ BUGZILLA.bug_title = '[% unfiltered_title FILTER js %]';
+ BUGZILLA.user = {
+ id: [% user.id FILTER none %],
+ login: '[% user.login FILTER js %]',
+ is_insider: [% user.is_insider ? "true" : "false" %],
+ settings: {
+ quote_replies: '[% user.settings.quote_replies.value FILTER js %]',
+ zoom_textareas: [% user.settings.zoom_textareas.value == "on" ? "true" : "false" %]
+ }
+ };
+[% END %]
diff --git a/extensions/BugModal/template/en/default/bug_modal/module.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/module.html.tmpl
new file mode 100644
index 000000000..838069a58
--- /dev/null
+++ b/extensions/BugModal/template/en/default/bug_modal/module.html.tmpl
@@ -0,0 +1,38 @@
+[%# 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.
+ #%]
+
+[%#
+ # title: (string, optional) main title of module
+ # collapse: (boolean) if true, show as collapsed by default (default false)
+ # subtitle: (string, optional) sub-title
+ # content: (string, required) module's content (use WRAPPER module..)
+ # hide_on_view: (boolean) if true, the module won't be visible in view mode
+ # hide_on_edit: (boolean) if true, the module won't be visible in edit mode
+ #%]
+
+<div class="module
+ [%~ " edit-hide" IF hide_on_edit %]
+ [%~ " edit-show" IF hide_on_view && !hide_on_edit %]"
+ [% IF hide_on_view +%] style="display:none"[% END %]
+ [% IF title %] id="module-[% title.replace FILTER id %]"[% END %]
+>
+ [% IF title %]
+ <div class="module-header">
+ <div class="module-latch">
+ <div class="module-spinner">[% collapsed ? "&#9656;" : "&#9662;" %]</div>
+ <div class="module-title">[% title FILTER html %]</div>
+ [% IF subtitle %]
+ <div class="module-subtitle">([% subtitle FILTER html %])</div>
+ [% END %]
+ </div>
+ </div>
+ [% END %]
+ <div class="module-content" [% ' style="display:none"' IF collapsed %] >
+ [% content FILTER none %]
+ </div>
+</div>
diff --git a/extensions/BugModal/template/en/default/bug_modal/new_comment.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/new_comment.html.tmpl
new file mode 100644
index 000000000..ff2562bf4
--- /dev/null
+++ b/extensions/BugModal/template/en/default/bug_modal/new_comment.html.tmpl
@@ -0,0 +1,29 @@
+[%# 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.
+ #%]
+
+[%#
+ # comment: comment object
+ # bug: bug object
+ #%]
+
+<div id="add-comment">
+ <div id="add-comment-label">Add Comment:</div>
+ [% IF user.is_insider && bug.check_can_change_field('longdesc', 0, 1) %]
+ <div id="add-comment-private"
+ title="Make comment visible only to members of the '[% Param('insidergroup') FILTER html %]' group"
+ >
+ <input type="checkbox" name="comment_is_private" id="add-comment-private-cb"
+ value="1" comment_id="[% comment.count FILTER none %]">
+ <label for="add-comment-private-cb">Private</label>
+ </div>
+ [% END %]
+ <textarea rows="5" cols="80" name="comment" id="comment"></textarea>
+ <div id="after-comment-commit-button">
+ [% Hook.process("after_comment_commit_button", 'bug/edit.html.tmpl') %]
+ </div>
+</div>
diff --git a/extensions/BugModal/template/en/default/bug_modal/rel_time.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/rel_time.html.tmpl
new file mode 100644
index 000000000..3b31dedeb
--- /dev/null
+++ b/extensions/BugModal/template/en/default/bug_modal/rel_time.html.tmpl
@@ -0,0 +1,21 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%#
+ # ts: timestamp
+ #%]
+
+<span class="rel-time" title="[% ts FILTER time("%Y-%m-%d %H:%M %Z") %]">
+ [%~
+ IF content.defined;
+ content;
+ ELSE;
+ ts FILTER time_duration FILTER html;
+ END;
+ ~%]
+</span>
diff --git a/extensions/BugModal/template/en/default/bug_modal/tracking_flags.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/tracking_flags.html.tmpl
new file mode 100644
index 000000000..5f22338cd
--- /dev/null
+++ b/extensions/BugModal/template/en/default/bug_modal/tracking_flags.html.tmpl
@@ -0,0 +1,96 @@
+[%# 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.
+ #%]
+
+[%#
+ # type: tracking flag type (eg. "project", "tracking")
+ #%]
+
+[%
+ flags = [];
+ set_flags = [];
+ FOREACH flag IN tracking_flags;
+ NEXT UNLESS flag.flag_type == type;
+ flags.push(flag);
+ NEXT IF flag.bug_flag(bug.id).value == "---";
+ set_flags.push(flag);
+ END;
+ RETURN UNLESS flags.size;
+%]
+
+[%# view %]
+[% IF set_flags.size %]
+ <div class="flags edit-hide">
+ <table class="layout-table tracking-flags">
+ [% IF type == "tracking" %]
+ <tr>
+ <th></th>
+ <th>Tracking</th>
+ <th>Status</th>
+ </tr>
+ [% END %]
+ [% FOREACH row IN tracking_flags_table %]
+ [%
+ NEXT UNLESS row.type == type;
+ tracking_value = row.tracking.bug_flag(bug_id).value || "---";
+ status_value = row.status.bug_flag(bug_id).value || "---";
+ NEXT IF tracking_value == "---" && status_value == "---";
+ %]
+ <tr>
+ <td class="tracking-flag-name">[% row.name FILTER html %]</td>
+ [% IF type == "tracking" %]
+ <td class="tracking-flag-tracking">[% tracking_value FILTER html %]</td>
+ [% END %]
+ <td class="tracking-flag-status">[% status_value FILTER html %]</td>
+ </tr>
+ [% END %]
+ </table>
+ </div>
+[% END %]
+
+[%# edit %]
+<div class="flags edit-show" style="display:none">
+ <table class="layout-table tracking-flags">
+ [% IF type == "tracking" %]
+ <tr>
+ <th></th>
+ <th>Tracking</th>
+ <th>Status</th>
+ </tr>
+ [% END %]
+ [% FOREACH row IN tracking_flags_table %]
+ [% NEXT UNLESS row.type == type %]
+ <tr>
+ <td class="tracking-flag-name">[% row.name FILTER html %]</td>
+ [% IF type == "tracking" %]
+ <td class="tracking-flag-tracking">[% INCLUDE tf_select flag=row.tracking %]</td>
+ [% END %]
+ <td class="tracking-flag-status">[% INCLUDE tf_select flag=row.status %]</td>
+ </tr>
+ [% END %]
+ </table>
+</div>
+
+[% BLOCK tf_select %]
+ [% RETURN UNLESS flag %]
+ <select id="[% flag.name FILTER html %]" name="[% flag.name FILTER html %]">
+ [%
+ flag_bug_value = flag.bug_flag(bug_id).value;
+ FOREACH value IN flag.values;
+ IF value.name != flag_bug_value;
+ NEXT IF !value.is_active || !flag.can_set_value(value.name);
+ END;
+ %]
+ <option value="[% value.name FILTER html %]"
+ id="v[% value.id FILTER html %]_[% flag.name FILTER html %]"
+ [% " selected" IF flag_bug_value == value.name %]
+ >
+ [% value.name FILTER html %]
+ </option>
+ [% END %]
+ </select>
+[% END %]
diff --git a/extensions/BugModal/template/en/default/bug_modal/user.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/user.html.tmpl
new file mode 100644
index 000000000..016017084
--- /dev/null
+++ b/extensions/BugModal/template/en/default/bug_modal/user.html.tmpl
@@ -0,0 +1,53 @@
+[%# 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.
+ #%]
+
+[%#
+ # u : user object
+ # simple : boolean, if true an unadorned name will be displayed (no gravatar, no menu) (default: false)
+ # gravatar_size : size of the gravator icon (default 0, which disables the gravatar)
+ # gravatar_only : boolean, if true output just the gravatar (not-simple only)
+ # nick_only : boolean, if true, the nickname will be used instead of the full name
+ #%]
+
+[%
+RETURN UNLESS u.id;
+DEFAULT gravatar_size = 0;
+%]
+<div class="vcard vcard_[% u.id FILTER none %]">
+ [% FILTER collapse %]
+
+ [% IF simple %]
+ [% IF user.id %]
+ <span class="fn" title="[% u.identity FILTER html %]">[% u.moz_nick FILTER html %]</span>
+ [% ELSE %]
+ <span class="fn">[% u.moz_nick FILTER html %]</span>
+ [% END %]
+
+ [% ELSE %]
+ [% IF gravatar_size %]
+ <img src="[% u.gravatar(gravatar_size * 2) FILTER none %]" class="gravatar"
+ width="[% gravatar_size FILTER none %]" height="[% gravatar_size FILTER none %]">
+ [% END %]
+ [% UNLESS gravatar_only %]
+ <a class="email [%= "disabled" UNLESS u.is_enabled %]"
+ [% IF user.id %]
+ href="mailto:[% u.email FILTER html %]"
+ onclick="return show_usermenu([% u.id FILTER none %], '[% u.email FILTER js %]', true,
+ [% user.in_group('editusers') || user.bless_groups.size > 0 ? "true" : "false" %])"
+ title="[% u.identity FILTER html %]"
+ [% ELSE %]
+ href="user_profile?user_id=[% u.id FILTER none %]"
+ [% END %]
+ >
+ <span class="[% user.id ? 'fn' : 'fna' %]">[% nick_only ? u.moz_nick : (u.name || u.nick) FILTER html %]</span>
+ [%~~%]
+ </a>
+ [% END %]
+ [% END %]
+ [% END %]
+</div>
diff --git a/extensions/BugModal/template/en/default/hook/global/setting-descs-settings.none.tmpl b/extensions/BugModal/template/en/default/hook/global/setting-descs-settings.none.tmpl
new file mode 100644
index 000000000..7214977bb
--- /dev/null
+++ b/extensions/BugModal/template/en/default/hook/global/setting-descs-settings.none.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.
+ #%]
+
+[%
+ setting_descs.ui_experiments = "Use experimental user interface"
+%]
diff --git a/extensions/BugModal/template/en/default/hook/global/variables-end.none.tmpl b/extensions/BugModal/template/en/default/hook/global/variables-end.none.tmpl
new file mode 100644
index 000000000..acb6ab870
--- /dev/null
+++ b/extensions/BugModal/template/en/default/hook/global/variables-end.none.tmpl
@@ -0,0 +1,13 @@
+[%# 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.
+ #%]
+
+[%
+ constants.FIELD_TYPE_USER = 20;
+ constants.FIELD_TYPE_USERS = 21;
+ constants.FIELD_TYPE_BUG_LIST = 22;
+%]
diff --git a/extensions/BugModal/web/ZeroClipboard/ZeroClipboard.min.js b/extensions/BugModal/web/ZeroClipboard/ZeroClipboard.min.js
new file mode 100644
index 000000000..b9c734d01
--- /dev/null
+++ b/extensions/BugModal/web/ZeroClipboard/ZeroClipboard.min.js
@@ -0,0 +1,9 @@
+/*!
+ * ZeroClipboard
+ * The ZeroClipboard library provides an easy way to copy text to the clipboard using an invisible Adobe Flash movie and a JavaScript interface.
+ * Copyright (c) 2009-2014 Jon Rohan, James M. Greene
+ * Licensed MIT
+ * http://zeroclipboard.org/
+ * v2.2.0
+ */
+!function(a,b){"use strict";var c,d,e,f=a,g=f.document,h=f.navigator,i=f.setTimeout,j=f.clearTimeout,k=f.setInterval,l=f.clearInterval,m=f.getComputedStyle,n=f.encodeURIComponent,o=f.ActiveXObject,p=f.Error,q=f.Number.parseInt||f.parseInt,r=f.Number.parseFloat||f.parseFloat,s=f.Number.isNaN||f.isNaN,t=f.Date.now,u=f.Object.keys,v=f.Object.defineProperty,w=f.Object.prototype.hasOwnProperty,x=f.Array.prototype.slice,y=function(){var a=function(a){return a};if("function"==typeof f.wrap&&"function"==typeof f.unwrap)try{var b=g.createElement("div"),c=f.unwrap(b);1===b.nodeType&&c&&1===c.nodeType&&(a=f.unwrap)}catch(d){}return a}(),z=function(a){return x.call(a,0)},A=function(){var a,c,d,e,f,g,h=z(arguments),i=h[0]||{};for(a=1,c=h.length;c>a;a++)if(null!=(d=h[a]))for(e in d)w.call(d,e)&&(f=i[e],g=d[e],i!==g&&g!==b&&(i[e]=g));return i},B=function(a){var b,c,d,e;if("object"!=typeof a||null==a||"number"==typeof a.nodeType)b=a;else if("number"==typeof a.length)for(b=[],c=0,d=a.length;d>c;c++)w.call(a,c)&&(b[c]=B(a[c]));else{b={};for(e in a)w.call(a,e)&&(b[e]=B(a[e]))}return b},C=function(a,b){for(var c={},d=0,e=b.length;e>d;d++)b[d]in a&&(c[b[d]]=a[b[d]]);return c},D=function(a,b){var c={};for(var d in a)-1===b.indexOf(d)&&(c[d]=a[d]);return c},E=function(a){if(a)for(var b in a)w.call(a,b)&&delete a[b];return a},F=function(a,b){if(a&&1===a.nodeType&&a.ownerDocument&&b&&(1===b.nodeType&&b.ownerDocument&&b.ownerDocument===a.ownerDocument||9===b.nodeType&&!b.ownerDocument&&b===a.ownerDocument))do{if(a===b)return!0;a=a.parentNode}while(a);return!1},G=function(a){var b;return"string"==typeof a&&a&&(b=a.split("#")[0].split("?")[0],b=a.slice(0,a.lastIndexOf("/")+1)),b},H=function(a){var b,c;return"string"==typeof a&&a&&(c=a.match(/^(?:|[^:@]*@|.+\)@(?=http[s]?|file)|.+?\s+(?: at |@)(?:[^:\(]+ )*[\(]?)((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/),c&&c[1]?b=c[1]:(c=a.match(/\)@((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/),c&&c[1]&&(b=c[1]))),b},I=function(){var a,b;try{throw new p}catch(c){b=c}return b&&(a=b.sourceURL||b.fileName||H(b.stack)),a},J=function(){var a,c,d;if(g.currentScript&&(a=g.currentScript.src))return a;if(c=g.getElementsByTagName("script"),1===c.length)return c[0].src||b;if("readyState"in c[0])for(d=c.length;d--;)if("interactive"===c[d].readyState&&(a=c[d].src))return a;return"loading"===g.readyState&&(a=c[c.length-1].src)?a:(a=I())?a:b},K=function(){var a,c,d,e=g.getElementsByTagName("script");for(a=e.length;a--;){if(!(d=e[a].src)){c=null;break}if(d=G(d),null==c)c=d;else if(c!==d){c=null;break}}return c||b},L=function(){var a=G(J())||K()||"";return a+"ZeroClipboard.swf"},M=function(){return null==a.opener&&(!!a.top&&a!=a.top||!!a.parent&&a!=a.parent)}(),N={bridge:null,version:"0.0.0",pluginType:"unknown",disabled:null,outdated:null,sandboxed:null,unavailable:null,degraded:null,deactivated:null,overdue:null,ready:null},O="11.0.0",P={},Q={},R=null,S=0,T=0,U={ready:"Flash communication is established",error:{"flash-disabled":"Flash is disabled or not installed. May also be attempting to run Flash in a sandboxed iframe, which is impossible.","flash-outdated":"Flash is too outdated to support ZeroClipboard","flash-sandboxed":"Attempting to run Flash in a sandboxed iframe, which is impossible","flash-unavailable":"Flash is unable to communicate bidirectionally with JavaScript","flash-degraded":"Flash is unable to preserve data fidelity when communicating with JavaScript","flash-deactivated":"Flash is too outdated for your browser and/or is configured as click-to-activate.\nThis may also mean that the ZeroClipboard SWF object could not be loaded, so please check your `swfPath` configuration and/or network connectivity.\nMay also be attempting to run Flash in a sandboxed iframe, which is impossible.","flash-overdue":"Flash communication was established but NOT within the acceptable time limit","version-mismatch":"ZeroClipboard JS version number does not match ZeroClipboard SWF version number","clipboard-error":"At least one error was thrown while ZeroClipboard was attempting to inject your data into the clipboard","config-mismatch":"ZeroClipboard configuration does not match Flash's reality","swf-not-found":"The ZeroClipboard SWF object could not be loaded, so please check your `swfPath` configuration and/or network connectivity"}},V=["flash-unavailable","flash-degraded","flash-overdue","version-mismatch","config-mismatch","clipboard-error"],W=["flash-disabled","flash-outdated","flash-sandboxed","flash-unavailable","flash-degraded","flash-deactivated","flash-overdue"],X=new RegExp("^flash-("+W.map(function(a){return a.replace(/^flash-/,"")}).join("|")+")$"),Y=new RegExp("^flash-("+W.slice(1).map(function(a){return a.replace(/^flash-/,"")}).join("|")+")$"),Z={swfPath:L(),trustedDomains:a.location.host?[a.location.host]:[],cacheBust:!0,forceEnhancedClipboard:!1,flashLoadTimeout:3e4,autoActivate:!0,bubbleEvents:!0,containerId:"global-zeroclipboard-html-bridge",containerClass:"global-zeroclipboard-container",swfObjectId:"global-zeroclipboard-flash-bridge",hoverClass:"zeroclipboard-is-hover",activeClass:"zeroclipboard-is-active",forceHandCursor:!1,title:null,zIndex:999999999},$=function(a){if("object"==typeof a&&null!==a)for(var b in a)if(w.call(a,b))if(/^(?:forceHandCursor|title|zIndex|bubbleEvents)$/.test(b))Z[b]=a[b];else if(null==N.bridge)if("containerId"===b||"swfObjectId"===b){if(!nb(a[b]))throw new Error("The specified `"+b+"` value is not valid as an HTML4 Element ID");Z[b]=a[b]}else Z[b]=a[b];{if("string"!=typeof a||!a)return B(Z);if(w.call(Z,a))return Z[a]}},_=function(){return Tb(),{browser:C(h,["userAgent","platform","appName"]),flash:D(N,["bridge"]),zeroclipboard:{version:Vb.version,config:Vb.config()}}},ab=function(){return!!(N.disabled||N.outdated||N.sandboxed||N.unavailable||N.degraded||N.deactivated)},bb=function(a,d){var e,f,g,h={};if("string"==typeof a&&a)g=a.toLowerCase().split(/\s+/);else if("object"==typeof a&&a&&"undefined"==typeof d)for(e in a)w.call(a,e)&&"string"==typeof e&&e&&"function"==typeof a[e]&&Vb.on(e,a[e]);if(g&&g.length){for(e=0,f=g.length;f>e;e++)a=g[e].replace(/^on/,""),h[a]=!0,P[a]||(P[a]=[]),P[a].push(d);if(h.ready&&N.ready&&Vb.emit({type:"ready"}),h.error){for(e=0,f=W.length;f>e;e++)if(N[W[e].replace(/^flash-/,"")]===!0){Vb.emit({type:"error",name:W[e]});break}c!==b&&Vb.version!==c&&Vb.emit({type:"error",name:"version-mismatch",jsVersion:Vb.version,swfVersion:c})}}return Vb},cb=function(a,b){var c,d,e,f,g;if(0===arguments.length)f=u(P);else if("string"==typeof a&&a)f=a.split(/\s+/);else if("object"==typeof a&&a&&"undefined"==typeof b)for(c in a)w.call(a,c)&&"string"==typeof c&&c&&"function"==typeof a[c]&&Vb.off(c,a[c]);if(f&&f.length)for(c=0,d=f.length;d>c;c++)if(a=f[c].toLowerCase().replace(/^on/,""),g=P[a],g&&g.length)if(b)for(e=g.indexOf(b);-1!==e;)g.splice(e,1),e=g.indexOf(b,e);else g.length=0;return Vb},db=function(a){var b;return b="string"==typeof a&&a?B(P[a])||null:B(P)},eb=function(a){var b,c,d;return a=ob(a),a&&!vb(a)?"ready"===a.type&&N.overdue===!0?Vb.emit({type:"error",name:"flash-overdue"}):(b=A({},a),tb.call(this,b),"copy"===a.type&&(d=Db(Q),c=d.data,R=d.formatMap),c):void 0},fb=function(){var a=N.sandboxed;if(Tb(),"boolean"!=typeof N.ready&&(N.ready=!1),N.sandboxed!==a&&N.sandboxed===!0)N.ready=!1,Vb.emit({type:"error",name:"flash-sandboxed"});else if(!Vb.isFlashUnusable()&&null===N.bridge){var b=Z.flashLoadTimeout;"number"==typeof b&&b>=0&&(S=i(function(){"boolean"!=typeof N.deactivated&&(N.deactivated=!0),N.deactivated===!0&&Vb.emit({type:"error",name:"flash-deactivated"})},b)),N.overdue=!1,Bb()}},gb=function(){Vb.clearData(),Vb.blur(),Vb.emit("destroy"),Cb(),Vb.off()},hb=function(a,b){var c;if("object"==typeof a&&a&&"undefined"==typeof b)c=a,Vb.clearData();else{if("string"!=typeof a||!a)return;c={},c[a]=b}for(var d in c)"string"==typeof d&&d&&w.call(c,d)&&"string"==typeof c[d]&&c[d]&&(Q[d]=c[d])},ib=function(a){"undefined"==typeof a?(E(Q),R=null):"string"==typeof a&&w.call(Q,a)&&delete Q[a]},jb=function(a){return"undefined"==typeof a?B(Q):"string"==typeof a&&w.call(Q,a)?Q[a]:void 0},kb=function(a){if(a&&1===a.nodeType){d&&(Lb(d,Z.activeClass),d!==a&&Lb(d,Z.hoverClass)),d=a,Kb(a,Z.hoverClass);var b=a.getAttribute("title")||Z.title;if("string"==typeof b&&b){var c=Ab(N.bridge);c&&c.setAttribute("title",b)}var e=Z.forceHandCursor===!0||"pointer"===Mb(a,"cursor");Rb(e),Qb()}},lb=function(){var a=Ab(N.bridge);a&&(a.removeAttribute("title"),a.style.left="0px",a.style.top="-9999px",a.style.width="1px",a.style.height="1px"),d&&(Lb(d,Z.hoverClass),Lb(d,Z.activeClass),d=null)},mb=function(){return d||null},nb=function(a){return"string"==typeof a&&a&&/^[A-Za-z][A-Za-z0-9_:\-\.]*$/.test(a)},ob=function(a){var b;if("string"==typeof a&&a?(b=a,a={}):"object"==typeof a&&a&&"string"==typeof a.type&&a.type&&(b=a.type),b){b=b.toLowerCase(),!a.target&&(/^(copy|aftercopy|_click)$/.test(b)||"error"===b&&"clipboard-error"===a.name)&&(a.target=e),A(a,{type:b,target:a.target||d||null,relatedTarget:a.relatedTarget||null,currentTarget:N&&N.bridge||null,timeStamp:a.timeStamp||t()||null});var c=U[a.type];return"error"===a.type&&a.name&&c&&(c=c[a.name]),c&&(a.message=c),"ready"===a.type&&A(a,{target:null,version:N.version}),"error"===a.type&&(X.test(a.name)&&A(a,{target:null,minimumVersion:O}),Y.test(a.name)&&A(a,{version:N.version})),"copy"===a.type&&(a.clipboardData={setData:Vb.setData,clearData:Vb.clearData}),"aftercopy"===a.type&&(a=Eb(a,R)),a.target&&!a.relatedTarget&&(a.relatedTarget=pb(a.target)),qb(a)}},pb=function(a){var b=a&&a.getAttribute&&a.getAttribute("data-clipboard-target");return b?g.getElementById(b):null},qb=function(a){if(a&&/^_(?:click|mouse(?:over|out|down|up|move))$/.test(a.type)){var c=a.target,d="_mouseover"===a.type&&a.relatedTarget?a.relatedTarget:b,e="_mouseout"===a.type&&a.relatedTarget?a.relatedTarget:b,h=Nb(c),i=f.screenLeft||f.screenX||0,j=f.screenTop||f.screenY||0,k=g.body.scrollLeft+g.documentElement.scrollLeft,l=g.body.scrollTop+g.documentElement.scrollTop,m=h.left+("number"==typeof a._stageX?a._stageX:0),n=h.top+("number"==typeof a._stageY?a._stageY:0),o=m-k,p=n-l,q=i+o,r=j+p,s="number"==typeof a.movementX?a.movementX:0,t="number"==typeof a.movementY?a.movementY:0;delete a._stageX,delete a._stageY,A(a,{srcElement:c,fromElement:d,toElement:e,screenX:q,screenY:r,pageX:m,pageY:n,clientX:o,clientY:p,x:o,y:p,movementX:s,movementY:t,offsetX:0,offsetY:0,layerX:0,layerY:0})}return a},rb=function(a){var b=a&&"string"==typeof a.type&&a.type||"";return!/^(?:(?:before)?copy|destroy)$/.test(b)},sb=function(a,b,c,d){d?i(function(){a.apply(b,c)},0):a.apply(b,c)},tb=function(a){if("object"==typeof a&&a&&a.type){var b=rb(a),c=P["*"]||[],d=P[a.type]||[],e=c.concat(d);if(e&&e.length){var g,h,i,j,k,l=this;for(g=0,h=e.length;h>g;g++)i=e[g],j=l,"string"==typeof i&&"function"==typeof f[i]&&(i=f[i]),"object"==typeof i&&i&&"function"==typeof i.handleEvent&&(j=i,i=i.handleEvent),"function"==typeof i&&(k=A({},a),sb(i,j,[k],b))}return this}},ub=function(a){var b=null;return(M===!1||a&&"error"===a.type&&a.name&&-1!==V.indexOf(a.name))&&(b=!1),b},vb=function(a){var b=a.target||d||null,f="swf"===a._source;switch(delete a._source,a.type){case"error":var g="flash-sandboxed"===a.name||ub(a);"boolean"==typeof g&&(N.sandboxed=g),-1!==W.indexOf(a.name)?A(N,{disabled:"flash-disabled"===a.name,outdated:"flash-outdated"===a.name,unavailable:"flash-unavailable"===a.name,degraded:"flash-degraded"===a.name,deactivated:"flash-deactivated"===a.name,overdue:"flash-overdue"===a.name,ready:!1}):"version-mismatch"===a.name&&(c=a.swfVersion,A(N,{disabled:!1,outdated:!1,unavailable:!1,degraded:!1,deactivated:!1,overdue:!1,ready:!1})),Pb();break;case"ready":c=a.swfVersion;var h=N.deactivated===!0;A(N,{disabled:!1,outdated:!1,sandboxed:!1,unavailable:!1,degraded:!1,deactivated:!1,overdue:h,ready:!h}),Pb();break;case"beforecopy":e=b;break;case"copy":var i,j,k=a.relatedTarget;!Q["text/html"]&&!Q["text/plain"]&&k&&(j=k.value||k.outerHTML||k.innerHTML)&&(i=k.value||k.textContent||k.innerText)?(a.clipboardData.clearData(),a.clipboardData.setData("text/plain",i),j!==i&&a.clipboardData.setData("text/html",j)):!Q["text/plain"]&&a.target&&(i=a.target.getAttribute("data-clipboard-text"))&&(a.clipboardData.clearData(),a.clipboardData.setData("text/plain",i));break;case"aftercopy":wb(a),Vb.clearData(),b&&b!==Jb()&&b.focus&&b.focus();break;case"_mouseover":Vb.focus(b),Z.bubbleEvents===!0&&f&&(b&&b!==a.relatedTarget&&!F(a.relatedTarget,b)&&xb(A({},a,{type:"mouseenter",bubbles:!1,cancelable:!1})),xb(A({},a,{type:"mouseover"})));break;case"_mouseout":Vb.blur(),Z.bubbleEvents===!0&&f&&(b&&b!==a.relatedTarget&&!F(a.relatedTarget,b)&&xb(A({},a,{type:"mouseleave",bubbles:!1,cancelable:!1})),xb(A({},a,{type:"mouseout"})));break;case"_mousedown":Kb(b,Z.activeClass),Z.bubbleEvents===!0&&f&&xb(A({},a,{type:a.type.slice(1)}));break;case"_mouseup":Lb(b,Z.activeClass),Z.bubbleEvents===!0&&f&&xb(A({},a,{type:a.type.slice(1)}));break;case"_click":e=null,Z.bubbleEvents===!0&&f&&xb(A({},a,{type:a.type.slice(1)}));break;case"_mousemove":Z.bubbleEvents===!0&&f&&xb(A({},a,{type:a.type.slice(1)}))}return/^_(?:click|mouse(?:over|out|down|up|move))$/.test(a.type)?!0:void 0},wb=function(a){if(a.errors&&a.errors.length>0){var b=B(a);A(b,{type:"error",name:"clipboard-error"}),delete b.success,i(function(){Vb.emit(b)},0)}},xb=function(a){if(a&&"string"==typeof a.type&&a){var b,c=a.target||null,d=c&&c.ownerDocument||g,e={view:d.defaultView||f,canBubble:!0,cancelable:!0,detail:"click"===a.type?1:0,button:"number"==typeof a.which?a.which-1:"number"==typeof a.button?a.button:d.createEvent?0:1},h=A(e,a);c&&d.createEvent&&c.dispatchEvent&&(h=[h.type,h.canBubble,h.cancelable,h.view,h.detail,h.screenX,h.screenY,h.clientX,h.clientY,h.ctrlKey,h.altKey,h.shiftKey,h.metaKey,h.button,h.relatedTarget],b=d.createEvent("MouseEvents"),b.initMouseEvent&&(b.initMouseEvent.apply(b,h),b._source="js",c.dispatchEvent(b)))}},yb=function(){var a=Z.flashLoadTimeout;if("number"==typeof a&&a>=0){var b=Math.min(1e3,a/10),c=Z.swfObjectId+"_fallbackContent";T=k(function(){var a=g.getElementById(c);Ob(a)&&(Pb(),N.deactivated=null,Vb.emit({type:"error",name:"swf-not-found"}))},b)}},zb=function(){var a=g.createElement("div");return a.id=Z.containerId,a.className=Z.containerClass,a.style.position="absolute",a.style.left="0px",a.style.top="-9999px",a.style.width="1px",a.style.height="1px",a.style.zIndex=""+Sb(Z.zIndex),a},Ab=function(a){for(var b=a&&a.parentNode;b&&"OBJECT"===b.nodeName&&b.parentNode;)b=b.parentNode;return b||null},Bb=function(){var a,b=N.bridge,c=Ab(b);if(!b){var d=Ib(f.location.host,Z),e="never"===d?"none":"all",h=Gb(A({jsVersion:Vb.version},Z)),i=Z.swfPath+Fb(Z.swfPath,Z);c=zb();var j=g.createElement("div");c.appendChild(j),g.body.appendChild(c);var k=g.createElement("div"),l="activex"===N.pluginType;k.innerHTML='<object id="'+Z.swfObjectId+'" name="'+Z.swfObjectId+'" width="100%" height="100%" '+(l?'classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000"':'type="application/x-shockwave-flash" data="'+i+'"')+">"+(l?'<param name="movie" value="'+i+'"/>':"")+'<param name="allowScriptAccess" value="'+d+'"/><param name="allowNetworking" value="'+e+'"/><param name="menu" value="false"/><param name="wmode" value="transparent"/><param name="flashvars" value="'+h+'"/><div id="'+Z.swfObjectId+'_fallbackContent">&nbsp;</div></object>',b=k.firstChild,k=null,y(b).ZeroClipboard=Vb,c.replaceChild(b,j),yb()}return b||(b=g[Z.swfObjectId],b&&(a=b.length)&&(b=b[a-1]),!b&&c&&(b=c.firstChild)),N.bridge=b||null,b},Cb=function(){var a=N.bridge;if(a){var d=Ab(a);d&&("activex"===N.pluginType&&"readyState"in a?(a.style.display="none",function e(){if(4===a.readyState){for(var b in a)"function"==typeof a[b]&&(a[b]=null);a.parentNode&&a.parentNode.removeChild(a),d.parentNode&&d.parentNode.removeChild(d)}else i(e,10)}()):(a.parentNode&&a.parentNode.removeChild(a),d.parentNode&&d.parentNode.removeChild(d))),Pb(),N.ready=null,N.bridge=null,N.deactivated=null,c=b}},Db=function(a){var b={},c={};if("object"==typeof a&&a){for(var d in a)if(d&&w.call(a,d)&&"string"==typeof a[d]&&a[d])switch(d.toLowerCase()){case"text/plain":case"text":case"air:text":case"flash:text":b.text=a[d],c.text=d;break;case"text/html":case"html":case"air:html":case"flash:html":b.html=a[d],c.html=d;break;case"application/rtf":case"text/rtf":case"rtf":case"richtext":case"air:rtf":case"flash:rtf":b.rtf=a[d],c.rtf=d}return{data:b,formatMap:c}}},Eb=function(a,b){if("object"!=typeof a||!a||"object"!=typeof b||!b)return a;var c={};for(var d in a)if(w.call(a,d))if("errors"===d){c[d]=a[d]?a[d].slice():[];for(var e=0,f=c[d].length;f>e;e++)c[d][e].format=b[c[d][e].format]}else if("success"!==d&&"data"!==d)c[d]=a[d];else{c[d]={};var g=a[d];for(var h in g)h&&w.call(g,h)&&w.call(b,h)&&(c[d][b[h]]=g[h])}return c},Fb=function(a,b){var c=null==b||b&&b.cacheBust===!0;return c?(-1===a.indexOf("?")?"?":"&")+"noCache="+t():""},Gb=function(a){var b,c,d,e,g="",h=[];if(a.trustedDomains&&("string"==typeof a.trustedDomains?e=[a.trustedDomains]:"object"==typeof a.trustedDomains&&"length"in a.trustedDomains&&(e=a.trustedDomains)),e&&e.length)for(b=0,c=e.length;c>b;b++)if(w.call(e,b)&&e[b]&&"string"==typeof e[b]){if(d=Hb(e[b]),!d)continue;if("*"===d){h.length=0,h.push(d);break}h.push.apply(h,[d,"//"+d,f.location.protocol+"//"+d])}return h.length&&(g+="trustedOrigins="+n(h.join(","))),a.forceEnhancedClipboard===!0&&(g+=(g?"&":"")+"forceEnhancedClipboard=true"),"string"==typeof a.swfObjectId&&a.swfObjectId&&(g+=(g?"&":"")+"swfObjectId="+n(a.swfObjectId)),"string"==typeof a.jsVersion&&a.jsVersion&&(g+=(g?"&":"")+"jsVersion="+n(a.jsVersion)),g},Hb=function(a){if(null==a||""===a)return null;if(a=a.replace(/^\s+|\s+$/g,""),""===a)return null;var b=a.indexOf("//");a=-1===b?a:a.slice(b+2);var c=a.indexOf("/");return a=-1===c?a:-1===b||0===c?null:a.slice(0,c),a&&".swf"===a.slice(-4).toLowerCase()?null:a||null},Ib=function(){var a=function(a){var b,c,d,e=[];if("string"==typeof a&&(a=[a]),"object"!=typeof a||!a||"number"!=typeof a.length)return e;for(b=0,c=a.length;c>b;b++)if(w.call(a,b)&&(d=Hb(a[b]))){if("*"===d){e.length=0,e.push("*");break}-1===e.indexOf(d)&&e.push(d)}return e};return function(b,c){var d=Hb(c.swfPath);null===d&&(d=b);var e=a(c.trustedDomains),f=e.length;if(f>0){if(1===f&&"*"===e[0])return"always";if(-1!==e.indexOf(b))return 1===f&&b===d?"sameDomain":"always"}return"never"}}(),Jb=function(){try{return g.activeElement}catch(a){return null}},Kb=function(a,b){var c,d,e,f=[];if("string"==typeof b&&b&&(f=b.split(/\s+/)),a&&1===a.nodeType&&f.length>0)if(a.classList)for(c=0,d=f.length;d>c;c++)a.classList.add(f[c]);else if(a.hasOwnProperty("className")){for(e=" "+a.className+" ",c=0,d=f.length;d>c;c++)-1===e.indexOf(" "+f[c]+" ")&&(e+=f[c]+" ");a.className=e.replace(/^\s+|\s+$/g,"")}return a},Lb=function(a,b){var c,d,e,f=[];if("string"==typeof b&&b&&(f=b.split(/\s+/)),a&&1===a.nodeType&&f.length>0)if(a.classList&&a.classList.length>0)for(c=0,d=f.length;d>c;c++)a.classList.remove(f[c]);else if(a.className){for(e=(" "+a.className+" ").replace(/[\r\n\t]/g," "),c=0,d=f.length;d>c;c++)e=e.replace(" "+f[c]+" "," ");a.className=e.replace(/^\s+|\s+$/g,"")}return a},Mb=function(a,b){var c=m(a,null).getPropertyValue(b);return"cursor"!==b||c&&"auto"!==c||"A"!==a.nodeName?c:"pointer"},Nb=function(a){var b={left:0,top:0,width:0,height:0};if(a.getBoundingClientRect){var c=a.getBoundingClientRect(),d=f.pageXOffset,e=f.pageYOffset,h=g.documentElement.clientLeft||0,i=g.documentElement.clientTop||0,j=0,k=0;if("relative"===Mb(g.body,"position")){var l=g.body.getBoundingClientRect(),m=g.documentElement.getBoundingClientRect();j=l.left-m.left||0,k=l.top-m.top||0}b.left=c.left+d-h-j,b.top=c.top+e-i-k,b.width="width"in c?c.width:c.right-c.left,b.height="height"in c?c.height:c.bottom-c.top}return b},Ob=function(a){if(!a)return!1;var b=m(a,null),c=r(b.height)>0,d=r(b.width)>0,e=r(b.top)>=0,f=r(b.left)>=0,g=c&&d&&e&&f,h=g?null:Nb(a),i="none"!==b.display&&"collapse"!==b.visibility&&(g||!!h&&(c||h.height>0)&&(d||h.width>0)&&(e||h.top>=0)&&(f||h.left>=0));return i},Pb=function(){j(S),S=0,l(T),T=0},Qb=function(){var a;if(d&&(a=Ab(N.bridge))){var b=Nb(d);A(a.style,{width:b.width+"px",height:b.height+"px",top:b.top+"px",left:b.left+"px",zIndex:""+Sb(Z.zIndex)})}},Rb=function(a){N.ready===!0&&(N.bridge&&"function"==typeof N.bridge.setHandCursor?N.bridge.setHandCursor(a):N.ready=!1)},Sb=function(a){if(/^(?:auto|inherit)$/.test(a))return a;var b;return"number"!=typeof a||s(a)?"string"==typeof a&&(b=Sb(q(a,10))):b=a,"number"==typeof b?b:"auto"},Tb=function(b){var c,d,e,f=N.sandboxed,g=null;if(b=b===!0,M===!1)g=!1;else{try{d=a.frameElement||null}catch(h){e={name:h.name,message:h.message}}if(d&&1===d.nodeType&&"IFRAME"===d.nodeName)try{g=d.hasAttribute("sandbox")}catch(h){g=null}else{try{c=document.domain||null}catch(h){c=null}(null===c||e&&"SecurityError"===e.name&&/(^|[\s\(\[@])sandbox(es|ed|ing|[\s\.,!\)\]@]|$)/.test(e.message.toLowerCase()))&&(g=!0)}}return N.sandboxed=g,f===g||b||Ub(o),g},Ub=function(a){function b(a){var b=a.match(/[\d]+/g);return b.length=3,b.join(".")}function c(a){return!!a&&(a=a.toLowerCase())&&(/^(pepflashplayer\.dll|libpepflashplayer\.so|pepperflashplayer\.plugin)$/.test(a)||"chrome.plugin"===a.slice(-13))}function d(a){a&&(i=!0,a.version&&(l=b(a.version)),!l&&a.description&&(l=b(a.description)),a.filename&&(k=c(a.filename)))}var e,f,g,i=!1,j=!1,k=!1,l="";if(h.plugins&&h.plugins.length)e=h.plugins["Shockwave Flash"],d(e),h.plugins["Shockwave Flash 2.0"]&&(i=!0,l="2.0.0.11");else if(h.mimeTypes&&h.mimeTypes.length)g=h.mimeTypes["application/x-shockwave-flash"],e=g&&g.enabledPlugin,d(e);else if("undefined"!=typeof a){j=!0;try{f=new a("ShockwaveFlash.ShockwaveFlash.7"),i=!0,l=b(f.GetVariable("$version"))}catch(m){try{f=new a("ShockwaveFlash.ShockwaveFlash.6"),i=!0,l="6.0.21"}catch(n){try{f=new a("ShockwaveFlash.ShockwaveFlash"),i=!0,l=b(f.GetVariable("$version"))}catch(o){j=!1}}}}N.disabled=i!==!0,N.outdated=l&&r(l)<r(O),N.version=l||"0.0.0",N.pluginType=k?"pepper":j?"activex":i?"netscape":"unknown"};Ub(o),Tb(!0);var Vb=function(){return this instanceof Vb?void("function"==typeof Vb._createClient&&Vb._createClient.apply(this,z(arguments))):new Vb};v(Vb,"version",{value:"2.2.0",writable:!1,configurable:!0,enumerable:!0}),Vb.config=function(){return $.apply(this,z(arguments))},Vb.state=function(){return _.apply(this,z(arguments))},Vb.isFlashUnusable=function(){return ab.apply(this,z(arguments))},Vb.on=function(){return bb.apply(this,z(arguments))},Vb.off=function(){return cb.apply(this,z(arguments))},Vb.handlers=function(){return db.apply(this,z(arguments))},Vb.emit=function(){return eb.apply(this,z(arguments))},Vb.create=function(){return fb.apply(this,z(arguments))},Vb.destroy=function(){return gb.apply(this,z(arguments))},Vb.setData=function(){return hb.apply(this,z(arguments))},Vb.clearData=function(){return ib.apply(this,z(arguments))},Vb.getData=function(){return jb.apply(this,z(arguments))},Vb.focus=Vb.activate=function(){return kb.apply(this,z(arguments))},Vb.blur=Vb.deactivate=function(){return lb.apply(this,z(arguments))},Vb.activeElement=function(){return mb.apply(this,z(arguments))};var Wb=0,Xb={},Yb=0,Zb={},$b={};A(Z,{autoActivate:!0});var _b=function(a){var b=this;b.id=""+Wb++,Xb[b.id]={instance:b,elements:[],handlers:{}},a&&b.clip(a),Vb.on("*",function(a){return b.emit(a)}),Vb.on("destroy",function(){b.destroy()}),Vb.create()},ac=function(a,d){var e,f,g,h={},i=Xb[this.id],j=i&&i.handlers;if(!i)throw new Error("Attempted to add new listener(s) to a destroyed ZeroClipboard client instance");if("string"==typeof a&&a)g=a.toLowerCase().split(/\s+/);else if("object"==typeof a&&a&&"undefined"==typeof d)for(e in a)w.call(a,e)&&"string"==typeof e&&e&&"function"==typeof a[e]&&this.on(e,a[e]);if(g&&g.length){for(e=0,f=g.length;f>e;e++)a=g[e].replace(/^on/,""),h[a]=!0,j[a]||(j[a]=[]),j[a].push(d);if(h.ready&&N.ready&&this.emit({type:"ready",client:this}),h.error){for(e=0,f=W.length;f>e;e++)if(N[W[e].replace(/^flash-/,"")]){this.emit({type:"error",name:W[e],client:this});break}c!==b&&Vb.version!==c&&this.emit({type:"error",name:"version-mismatch",jsVersion:Vb.version,swfVersion:c})}}return this},bc=function(a,b){var c,d,e,f,g,h=Xb[this.id],i=h&&h.handlers;if(!i)return this;if(0===arguments.length)f=u(i);else if("string"==typeof a&&a)f=a.split(/\s+/);else if("object"==typeof a&&a&&"undefined"==typeof b)for(c in a)w.call(a,c)&&"string"==typeof c&&c&&"function"==typeof a[c]&&this.off(c,a[c]);if(f&&f.length)for(c=0,d=f.length;d>c;c++)if(a=f[c].toLowerCase().replace(/^on/,""),g=i[a],g&&g.length)if(b)for(e=g.indexOf(b);-1!==e;)g.splice(e,1),e=g.indexOf(b,e);else g.length=0;return this},cc=function(a){var b=null,c=Xb[this.id]&&Xb[this.id].handlers;return c&&(b="string"==typeof a&&a?c[a]?c[a].slice(0):[]:B(c)),b},dc=function(a){if(ic.call(this,a)){"object"==typeof a&&a&&"string"==typeof a.type&&a.type&&(a=A({},a));var b=A({},ob(a),{client:this});jc.call(this,b)}return this},ec=function(a){if(!Xb[this.id])throw new Error("Attempted to clip element(s) to a destroyed ZeroClipboard client instance");a=kc(a);for(var b=0;b<a.length;b++)if(w.call(a,b)&&a[b]&&1===a[b].nodeType){a[b].zcClippingId?-1===Zb[a[b].zcClippingId].indexOf(this.id)&&Zb[a[b].zcClippingId].push(this.id):(a[b].zcClippingId="zcClippingId_"+Yb++,Zb[a[b].zcClippingId]=[this.id],Z.autoActivate===!0&&lc(a[b]));var c=Xb[this.id]&&Xb[this.id].elements;-1===c.indexOf(a[b])&&c.push(a[b])}return this},fc=function(a){var b=Xb[this.id];if(!b)return this;var c,d=b.elements;a="undefined"==typeof a?d.slice(0):kc(a);for(var e=a.length;e--;)if(w.call(a,e)&&a[e]&&1===a[e].nodeType){for(c=0;-1!==(c=d.indexOf(a[e],c));)d.splice(c,1);var f=Zb[a[e].zcClippingId];if(f){for(c=0;-1!==(c=f.indexOf(this.id,c));)f.splice(c,1);0===f.length&&(Z.autoActivate===!0&&mc(a[e]),delete a[e].zcClippingId)}}return this},gc=function(){var a=Xb[this.id];return a&&a.elements?a.elements.slice(0):[]},hc=function(){Xb[this.id]&&(this.unclip(),this.off(),delete Xb[this.id])},ic=function(a){if(!a||!a.type)return!1;if(a.client&&a.client!==this)return!1;var b=Xb[this.id],c=b&&b.elements,d=!!c&&c.length>0,e=!a.target||d&&-1!==c.indexOf(a.target),f=a.relatedTarget&&d&&-1!==c.indexOf(a.relatedTarget),g=a.client&&a.client===this;return b&&(e||f||g)?!0:!1},jc=function(a){var b=Xb[this.id];if("object"==typeof a&&a&&a.type&&b){var c=rb(a),d=b&&b.handlers["*"]||[],e=b&&b.handlers[a.type]||[],g=d.concat(e);if(g&&g.length){var h,i,j,k,l,m=this;for(h=0,i=g.length;i>h;h++)j=g[h],k=m,"string"==typeof j&&"function"==typeof f[j]&&(j=f[j]),"object"==typeof j&&j&&"function"==typeof j.handleEvent&&(k=j,j=j.handleEvent),"function"==typeof j&&(l=A({},a),sb(j,k,[l],c))}}},kc=function(a){return"string"==typeof a&&(a=[]),"number"!=typeof a.length?[a]:a},lc=function(a){if(a&&1===a.nodeType){var b=function(a){(a||(a=f.event))&&("js"!==a._source&&(a.stopImmediatePropagation(),a.preventDefault()),delete a._source)},c=function(c){(c||(c=f.event))&&(b(c),Vb.focus(a))};a.addEventListener("mouseover",c,!1),a.addEventListener("mouseout",b,!1),a.addEventListener("mouseenter",b,!1),a.addEventListener("mouseleave",b,!1),a.addEventListener("mousemove",b,!1),$b[a.zcClippingId]={mouseover:c,mouseout:b,mouseenter:b,mouseleave:b,mousemove:b}}},mc=function(a){if(a&&1===a.nodeType){var b=$b[a.zcClippingId];if("object"==typeof b&&b){for(var c,d,e=["move","leave","enter","out","over"],f=0,g=e.length;g>f;f++)c="mouse"+e[f],d=b[c],"function"==typeof d&&a.removeEventListener(c,d,!1);delete $b[a.zcClippingId]}}};Vb._createClient=function(){_b.apply(this,z(arguments))},Vb.prototype.on=function(){return ac.apply(this,z(arguments))},Vb.prototype.off=function(){return bc.apply(this,z(arguments))},Vb.prototype.handlers=function(){return cc.apply(this,z(arguments))},Vb.prototype.emit=function(){return dc.apply(this,z(arguments))},Vb.prototype.clip=function(){return ec.apply(this,z(arguments))},Vb.prototype.unclip=function(){return fc.apply(this,z(arguments))},Vb.prototype.elements=function(){return gc.apply(this,z(arguments))},Vb.prototype.destroy=function(){return hc.apply(this,z(arguments))},Vb.prototype.setText=function(a){if(!Xb[this.id])throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance");return Vb.setData("text/plain",a),this},Vb.prototype.setHtml=function(a){if(!Xb[this.id])throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance");return Vb.setData("text/html",a),this},Vb.prototype.setRichText=function(a){if(!Xb[this.id])throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance");return Vb.setData("application/rtf",a),this},Vb.prototype.setData=function(){if(!Xb[this.id])throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance");return Vb.setData.apply(this,z(arguments)),this},Vb.prototype.clearData=function(){if(!Xb[this.id])throw new Error("Attempted to clear pending clipboard data from a destroyed ZeroClipboard client instance");return Vb.clearData.apply(this,z(arguments)),this},Vb.prototype.getData=function(){if(!Xb[this.id])throw new Error("Attempted to get pending clipboard data from a destroyed ZeroClipboard client instance");return Vb.getData.apply(this,z(arguments))},"function"==typeof define&&define.amd?define(function(){return Vb}):"object"==typeof module&&module&&"object"==typeof module.exports&&module.exports?module.exports=Vb:a.ZeroClipboard=Vb}(function(){return this||window}());
diff --git a/extensions/BugModal/web/ZeroClipboard/ZeroClipboard.swf b/extensions/BugModal/web/ZeroClipboard/ZeroClipboard.swf
new file mode 100644
index 000000000..8bad6a3e3
--- /dev/null
+++ b/extensions/BugModal/web/ZeroClipboard/ZeroClipboard.swf
Binary files differ
diff --git a/extensions/BugModal/web/bug_modal.css b/extensions/BugModal/web/bug_modal.css
new file mode 100644
index 000000000..27a9e3f73
--- /dev/null
+++ b/extensions/BugModal/web/bug_modal.css
@@ -0,0 +1,666 @@
+/* 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. */
+
+/* generic */
+
+.container {
+ display: table-cell;
+ width: 100%;
+}
+
+.layout-table {
+ border-spacing: 0;
+}
+
+.layout-table td {
+ padding: 0;
+}
+
+.inline {
+ display: table-cell;
+}
+
+.gravatar {
+ vertical-align: middle;
+ margin-right: 5px;
+}
+
+.flag .vcard {
+ display: inline;
+}
+
+.group-padlock {
+ vertical-align: middle;
+ margin-right: 5px;
+}
+
+button.minor {
+ background-color: #888;
+ background-image: linear-gradient(#aaa, #888);
+ font-size: 11px;
+ padding: 0.25em 0.5em;
+}
+
+button.minor:hover {
+ -webkit-box-shadow: 0 1px 0 0 rgba(0,0,0,0.2), inset 0 -1px 0 0 rgba(0,0,0,0.3), inset 0 12px 24px 2px #bbb;
+ -moz-box-shadow: 0 1px 0 0 rgba(0,0,0,0.2), inset 0 -1px 0 0 rgba(0,0,0,0.3), inset 0 12px 24px 2px #bbb;
+ box-shadow: 0 1px 0 0 rgba(0,0,0,0.2), inset 0 -1px 0 0 rgba(0,0,0,0.3), inset 0 12px 24px 2px #bbb;
+}
+
+button.in-page {
+ background-color: #fff;
+ background-image: linear-gradient(#fff, #fff);
+ color: #000;
+ border: 1px solid #ccc;
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ box-shadow: none;
+ font-size: 11px;
+ padding: 0.25em 0.5em;
+}
+
+button.in-page:hover {
+ -webkit-box-shadow: inset 0 12px 24px 2px #eee;
+ -moz-box-shadow: inset 0 12px 24px 2px #eee;
+ box-shadow: inset 0 12px 24px 2px #eee;
+ -moz-transition: all linear 100ms;
+ -webkit-transition: all linear 100ms;
+ transition: all linear 100ms;
+}
+
+select[multiple], .text_input, .yui-ac-input, input {
+ font-size: 12px !important;
+}
+
+.spin-toggle {
+ cursor: pointer;
+}
+
+.spin-toggle:hover {
+ text-decoration: underline;
+}
+
+.spin-latch {
+ color: #999;
+ padding-right: 5px;
+}
+
+/* modules */
+
+.module {
+ color: #000;
+ border-radius: 2px;
+ margin-top: 5px;
+ font-size: 13px;
+}
+
+.module.module-collapsed .module-content {
+ border: 1px solid red;
+}
+
+.module-header {
+ background: #eee;
+ padding: 2px 5px;
+ cursor: pointer;
+}
+
+.module-header:hover {
+ outline: 1px solid #ccc;
+}
+
+.module-latch {
+ display: table-cell;
+ padding-left: 5px;
+ padding-right: 5px;
+}
+
+.module-spinner {
+ color: #999;
+ display: table-cell;
+ width: 10px;
+}
+
+.module-title, .module-subtitle {
+ display: table-cell;
+ padding-left: 5px;
+}
+
+.module-subtitle {
+ padding-right: 5px;
+ color: #666;
+ font-size: 12px;
+}
+
+.module .fields-lhs {
+ min-width: 450px;
+ display: table-cell;
+ vertical-align: top;
+}
+
+.module .fields-rhs {
+ min-width: 450px;
+ display: table-cell;
+ vertical-align: top;
+ width: 100%;
+}
+
+.module-content {
+ padding: 2px 5px;
+ background: #fff;
+}
+
+.module .field {
+ margin-top: 4px;
+ vertical-align: top;
+}
+
+.module .field.right {
+}
+
+.module .field .name {
+ display: table-cell;
+ width: 100px;
+ min-width: 100px;
+ text-align: right;
+ vertical-align: top;
+ padding-right: 10px;
+ color: #666;
+}
+
+.module .field.inline .name {
+ min-width: 0px;
+ width: auto;
+ padding-left: 10px;
+}
+
+.module .indent {
+ padding-left: 10px;
+}
+
+.module .field .value {
+ display: table-cell;
+}
+
+.module .field .value.wide {
+ display: block;
+}
+
+.module .field .value.edit {
+ width: 100%;
+}
+
+.module .field .value input {
+ width: 100%;
+}
+
+.module .field .value input[type="checkbox"] {
+ width: auto;
+}
+
+.module .field .value.short input {
+ width: 170px;
+}
+
+/* field types */
+
+input[type="number"] {
+ text-align: right;
+ width: 5em !important;
+}
+
+.cf_date-img, .cf_datetime-img {
+ vertical-align: middle;
+}
+
+/* specific fields */
+
+#field-value-bug_id {
+ font-weight: bold;
+}
+
+#field-value-short_desc {
+ font-weight: bold;
+ font-size: 120%;
+}
+
+#status-needinfo, #status-needinfo .vcard {
+ display: inline;
+}
+
+#duplicate-container, #duplicate-actions {
+ display: table-cell;
+ vertical-align: top;
+ padding-left: 8px;
+}
+
+#dup_id {
+ margin-left: 4px;
+}
+
+#duplicate-container #dup_up {
+ padding-left: 5px;
+}
+
+#after-comment-commit-button {
+ margin-left: -8px;
+}
+
+#needinfo_from_autocomplete {
+ width: auto;
+}
+
+#needinfo_role_identity {
+ margin-left: 5px;
+}
+
+#user-story {
+ margin: 0;
+}
+
+#user-story-actions {
+ float: right;
+}
+
+#login-required {
+ padding: 20px 8px;
+ margin-bottom: 50px;
+}
+
+#product-info, #component-info {
+ color: #484;
+}
+
+#cc-latch {
+ color: #999;
+}
+
+#cc-latch, #cc-summary {
+ cursor: pointer;
+}
+
+#cc-summary:hover {
+ text-decoration: underline;
+}
+
+#cc-list {
+ max-height: 150px;
+ overflow-y: scroll;
+}
+
+/* actions */
+
+#copy-summary {
+ margin-left: 20px;
+}
+
+#top-actions {
+ margin-top: 5px;
+ padding-bottom: 20px;
+}
+
+#top-actions .save-btn {
+ float: right;
+}
+
+#bottom-actions {
+ margin-bottom: 50px;
+}
+
+#bottom-right-actions {
+ float: right;
+}
+
+.edit-textarea-set-btn {
+ float: right;
+}
+
+/* attachments */
+
+#attachments {
+ width: 100%;
+}
+
+#attachments tr:hover {
+ background-image: linear-gradient(to right, #666, rgba(0, 0, 0, 0) 1px);
+}
+
+#attachments td {
+ padding: 4px 8px;
+ vertical-align: top;
+ font-size: 13px;
+ border-bottom: 1px dotted silver;
+}
+
+#attachments .attach-desc-td {
+ width: 100%;
+}
+
+#attachments .attach-desc {
+ font-weight: bold;
+}
+
+#attachments .attach-desc a {
+ color: #000;
+}
+
+#attachments .attach-info {
+ font-size: 11px;
+}
+
+#attachments .attach-time {
+ font-size: 11px;
+}
+
+#attachments .attach-actions {
+ white-space: nowrap;
+}
+
+#attachments .attach-flag {
+ white-space: nowrap;
+}
+
+#attachments .flag-name-status {
+ font-weight: bold;
+}
+
+#attachments .attach-obsolete .attach-desc {
+ text-decoration: line-through;
+}
+
+#attachments .attach-patch {
+ background: #ffc;
+ background-image: linear-gradient(to right, #ffc, #fff);
+}
+
+#attachments .vcard {
+ display: inline;
+}
+
+#attachments-actions {
+ padding: 2px;
+}
+
+#attachments .attach-flag .vcard {
+ white-space: nowrap;
+}
+
+/* flags */
+
+.flags td {
+ font-size: 13px !important;
+}
+
+.flag-name {
+ text-align: right;
+}
+
+td.flag-name, td.flag-requestee {
+ padding-left: 5px;
+}
+
+td.flag-value select {
+ margin-left: 5px;
+}
+
+td.flag-requestee {
+ width: 100%;
+}
+
+.flags .vcard {
+ white-space: nowrap;
+}
+
+.tracking-flags td, .tracking-flags th {
+ padding: 0 5px;
+}
+
+.tracking-flags th {
+ font-weight: normal;
+ text-align: left;
+ color: #666;
+}
+
+.tracking-flag-name, .tracking-flag-tracking {
+ text-align: right;
+ white-space: nowrap;
+}
+
+/* comments and activity */
+
+.change-set {
+ clear: both;
+ -webkit-box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
+ -moz-box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
+ box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
+ border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+ margin-top: 20px;
+ border: 1px solid #ddd;
+}
+
+.change-set:target {
+ outline: 2px solid #0095dd;
+}
+
+.change-head {
+ width: 100%;
+ background: #eee;
+}
+
+.change-gravatar {
+ padding-left: 8px !important;
+}
+
+.change-author {
+ width: 100%;
+ vertical-align: top;
+ padding-top: 4px !important;
+}
+
+.change-author .vcard {
+ display: inline;
+ font-weight: bold;
+}
+
+.change-author .user-role {
+ margin-left: 1em;
+ color: #448844;
+}
+
+
+.change-name, .change-time, .comment-private {
+ display: inline;
+}
+
+.comment-actions {
+ white-space: nowrap;
+ padding: 2px 2px 0 0 !important;
+}
+
+.comment-spinner {
+ font-family: monospace;
+ white-space: nowrap;
+ vertical-align: bottom;
+}
+
+.comment-text {
+ white-space: pre-wrap;
+ font: 13px/1.2 "Droid Sans Mono",Menlo,Monaco,"Courier New",Courier,monospace;
+ background: #fff;
+ margin: 1px 0 0 0;
+ overflow: auto;
+ padding: 8px;
+ border-top: 1px solid #ddd;
+}
+
+.comment-text span.quote, .comment-text span.quote_wrapped {
+ background: #eee !important;
+ color: #444 !important;
+ display: block !important;
+ padding: 5px !important;
+ display: inline-block !important;
+ width: 99% !important;
+}
+
+.comment-tags {
+ padding: 0 8px 2px 8px !important;
+}
+
+.comment-tag {
+ border: 1px solid #ccc;
+ padding: 2px 4px;
+ margin-right: 2px;
+ border-radius: 0.5em;
+ background-color: #fff;
+ color: #000;
+ font-size: 12px;
+}
+
+.comment-collapse-reason {
+ padding: 5px 7px !important;
+ width: 100%;
+}
+
+.default-collapsed {
+ background: inherit;
+ color: #888;
+}
+
+.default-collapsed .comment-actions {
+ padding: 2px;
+}
+
+.private-comment {
+ color: #8b0000;
+ background: #f3eeee;
+}
+
+.activity {
+ padding: 5px 8px;
+ background: #eee;
+ border-top: 1px solid #ddd;
+}
+
+.activity-deleted {
+ text-decoration: line-through;
+}
+
+.change-set .reporter {
+ background-image: linear-gradient(to right, #feb, rgba(0, 0, 0, 0) 150px);
+}
+
+.change-set .assignee {
+ background-image: linear-gradient(to right, #ffc, rgba(0, 0, 0, 0) 150px);
+}
+
+/* add comment */
+
+#add-comment {
+ margin-top: 20px;
+}
+
+#add-comment-label {
+ display: inline;
+ font-weight: bold;
+}
+
+#add-comment-private {
+ float: right;
+}
+
+#comment {
+ width: 100%;
+ box-sizing: border-box !important;
+}
+
+/* controls */
+
+#summary-container {
+ display: table-cell;
+ width: 100%;
+ vertical-align: top;
+}
+
+#xhr-error {
+ background: #fff;
+ color: #000;
+ border-radius: 2px;
+ border: 1px solid maroon;
+ margin: 5px 0px;
+ padding: 5px;
+}
+
+#mode-container {
+ display: table-cell;
+ white-space: nowrap;
+ text-align: right;
+ padding: 5px;
+ margin: 5px;
+}
+
+#mode-btn, #commit-btn {
+ margin: 0 0 5px 0;
+}
+
+#mode-btn-loading, #mode-btn-editing {
+ display: none;
+}
+
+#edit-throbber {
+ margin-right: 5px;
+}
+
+#commit {
+ margin: 5px;
+}
+
+#mode-container .button-row {
+ margin-top: 1px;
+}
+
+/* theme */
+
+#bugzilla-body {
+ margin: 5px !important;
+ max-width: 1024px !important;
+ min-width: 800px !important;
+ width: 100% !important;
+}
+
+#footer {
+ max-width: 1024px !important;
+ min-width: 800px !important;
+}
+
+.vcard {
+ white-space: normal;
+}
+
+.vcard a.disabled {
+ color: #888;
+}
+
+.xdsoft_datetimepicker button, .xdsoft_datetimepicker button:hover {
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ box-shadow: none;
+}
+
+div.ui-widget-content {
+ background: #fff;
+}
+
+div.ui-tooltip {
+ padding: 4px;
+ font-size: 13px;
+ font-family: inherit;
+ max-width: 500px;
+}
+
+.yui-ac {
+ width: 100%;
+}
+
diff --git a/extensions/BugModal/web/bug_modal.js b/extensions/BugModal/web/bug_modal.js
new file mode 100644
index 000000000..c3fd84e97
--- /dev/null
+++ b/extensions/BugModal/web/bug_modal.js
@@ -0,0 +1,730 @@
+/* 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. */
+
+$(function() {
+ 'use strict';
+
+ // all keywords for autocompletion (lazy-loaded on edit)
+ var keywords = [];
+
+ // products with descriptions (also lazy-loaded)
+ var products = [];
+
+ // scroll to an element
+ function scroll_to(el, complete) {
+ var offset = el.offset();
+ $('html, body')
+ .animate({
+ scrollTop: offset.top - 20,
+ scrollLeft: offset.left = 20
+ },
+ 200,
+ complete
+ );
+ }
+
+ // expand all modules
+ $('#expand-all-btn')
+ .click(function(event) {
+ event.preventDefault();
+ var btn = $(event.target);
+ if (btn.data('expanded-modules')) {
+ btn.data('expanded-modules').slideToggle(200, 'swing', function() {
+ btn.data('expanded-spinners').html('&#9656;');
+ });
+ btn.data('expanded-modules', false);
+ btn.text('Expand All');
+ }
+ else {
+ var modules = $('.module-content:hidden');
+ var spinners = $([]);
+ modules.each(function() {
+ spinners.push($(this).parent('.module').find('.module-spinner')[0]);
+ });
+ btn.data('expanded-modules', modules);
+ btn.data('expanded-spinners', spinners);
+ modules.slideToggle(200, 'swing', function() {
+ spinners.html('&#9662;');
+ });
+ btn.text('Collapse');
+ }
+ });
+
+ // expand/colapse module
+ $('.module-header')
+ .click(function(event) {
+ event.preventDefault();
+ var target = $(event.target);
+ var latch = target.hasClass('module-header') ? target.children('.module-latch') : target.parent('.module-latch');
+ var spinner = $(latch.children('.module-spinner')[0]);
+ var module = $(latch.parents('.module')[0]);
+ var content = $(module.children('.module-content')[0]);
+ content.slideToggle(200, 'swing', function() {
+ spinner.html(content.is(':visible') ? '&#9662;' : '&#9656;');
+ });
+ });
+
+ // toggle obsolete attachments
+ $('#attachments-obsolete-btn')
+ .click(function(event) {
+ event.preventDefault();
+ $(event.target).text(($('#attachments tr:hidden').length ? 'Hide' : 'Show') + ' Obsolete Attachments');
+ $('#attachments tr.attach-obsolete').toggle();
+ });
+
+ // comment collapse/expand
+ $('.comment-spinner')
+ .click(function(event) {
+ event.preventDefault();
+ var spinner = $(event.target);
+ var id = spinner.attr('id').match(/\d+$/)[0];
+ // switch to full header for initially collapsed comments
+ if (spinner.attr('id').match(/^ccs-/)) {
+ $('#cc-' + id).hide();
+ $('#ch-' + id).show();
+ }
+ $('#ct-' + id).slideToggle('fast', function() {
+ $('#c' + id).find('.activity').toggle();
+ spinner.text($('#ct-' + id + ':visible').length ? '-' : '+');
+ });
+ });
+
+ // url --> unsafe warning
+ $('.unsafe-url')
+ .click(function(event) {
+ event.preventDefault();
+ if (confirm('This is considered an unsafe URL and could possibly be harmful. ' +
+ 'The full URL is:\n\n' + $(event.target).attr('title') + '\n\nContinue?'))
+ {
+ try {
+ window.open($(event.target).attr('title'));
+ } catch(ex) {
+ alert('Malformed URL');
+ }
+ }
+ });
+
+ // last comment btn
+ $('#last-comment-btn')
+ .click(function(event) {
+ event.preventDefault();
+ var id = $('.comment:last')[0].parentNode.id;
+ scroll_to($('#' + id));
+ window.location.hash = id;
+ });
+
+ // top btn
+ $('#top-btn')
+ .click(function(event) {
+ event.preventDefault();
+ scroll_to($('body'));
+ });
+
+ // use non-native tooltips for relative times and bug summaries
+ $('.rel-time, .bz_bug_link').tooltip({
+ position: { my: "left top+8", at: "left bottom", collision: "flipfit" },
+ show: { effect: 'none' },
+ hide: { effect: 'none' }
+ });
+
+ // tooltips create a new ui-helper-hidden-accessible div each time a
+ // tooltip is shown. this is never removed leading to memory leak and
+ // bloated dom. http://bugs.jqueryui.com/ticket/10689
+ $('.ui-helper-hidden-accessible').remove();
+
+ // product/component info
+ $('.spin-toggle')
+ .click(function(event) {
+ event.preventDefault();
+ var latch = $($(event.target).data('latch'));
+ var el_for = $($(event.target).data('for'));
+
+ if (latch.data('expanded')) {
+ latch.data('expanded', false).html('&#9656;');
+ el_for.hide();
+ }
+ else {
+ latch.data('expanded', true).html('&#9662;');
+ el_for.show();
+ }
+ });
+
+ // cc list
+ $('#cc-latch, #cc-summary')
+ .click(function(event) {
+ event.preventDefault();
+ var latch = $('#cc-latch');
+
+ if (latch.data('expanded')) {
+ latch.data('expanded', false).html('&#9656;');
+ $('#cc-list').hide();
+ }
+ else {
+ latch.data('expanded', true).html('&#9662;');
+ $('#cc-list').show();
+ if (!latch.data('fetched')) {
+ $('#cc-list').html(
+ '<img src="extensions/BugModal/web/throbber.gif" width="16" height="11"> Loading...'
+ );
+ bugzilla_ajax(
+ {
+ url: 'rest/bug_modal/cc/' + BUGZILLA.bug_id
+ },
+ function(data) {
+ $('#cc-list').html(data.html);
+ latch.data('fetched', true);
+ }
+ );
+ }
+ }
+ });
+
+ // copy summary to clipboard
+ if ($('#copy-summary').length) {
+ var zero = new ZeroClipboard($('#copy-summary'));
+ zero.on({
+ 'error': function(event) {
+ console.log(event.message);
+ zero.destroy();
+ $('#copy-summary').hide();
+
+ },
+ 'copy': function(event) {
+ var clipboard = event.clipboardData;
+ clipboard.setData('text/plain', 'Bug ' + BUGZILLA.bug_id + ' - ' + $('#field-value-short_desc').text());
+ }
+ });
+ }
+
+ //
+ // anything after this point is only executed for logged in users
+ //
+
+ if (BUGZILLA.user.id === 0) return;
+
+ // edit/save mode button
+ $('#mode-btn')
+ .click(function(event) {
+ event.preventDefault();
+
+ // hide buttons, old error messages
+ $('#mode-btn-readonly').hide();
+
+ // toggle visibility
+ $('.edit-hide').hide();
+ $('.edit-show').show();
+
+ // expand specific modules
+ $('#module-details .module-header').each(function() {
+ if ($(this.parentNode).find('.module-content:visible').length === 0) {
+ $(this).click();
+ }
+ });
+
+ // if there's no current user-story, it's a better experience if it's editable by default
+ if ($('#cf_user_story').val() === '') {
+ $('#user-story-edit-btn').click();
+ }
+
+ // "loading.." ui
+ $('#mode-btn-loading').show();
+ $('#cancel-btn').prop('disabled', true);
+ $('#mode-btn').prop('disabled', true);
+
+ // load the missing select data
+ bugzilla_ajax(
+ {
+ url: 'rest/bug_modal/edit/' + BUGZILLA.bug_id
+ },
+ function(data) {
+ $('#mode-btn').hide();
+
+ // populate select menus
+ $.each(data.options, function(key, value) {
+ var el = $('#' + key);
+ if (!el) return;
+ var selected = el.val();
+ el.empty();
+ $(value).each(function(i, v) {
+ el.append($('<option>', { value: v.name, text: v.name }));
+ });
+ el.val(selected);
+ if (el.attr('multiple') && value.length < 5) {
+ el.attr('size', value.length);
+ }
+ });
+
+ // build our product description hash
+ $.each(data.options.product, function() {
+ products[this.name] = this.description;
+ });
+
+ // keywords is a multi-value autocomplete
+ // (this should probably be a simple jquery plugin)
+ keywords = data.keywords;
+ $('#keywords')
+ .bind('keydown', function(event) {
+ if (event.keyCode == $.ui.keyCode.TAB && $(this).autocomplete('instance').menu.active)
+ {
+ event.preventDefault();
+ }
+ })
+ .blur(function() {
+ $(this).val($(this).val().replace(/,\s*$/, ''));
+ })
+ .autocomplete({
+ source: function(request, response) {
+ response($.ui.autocomplete.filter(keywords, request.term.split(/,\s*/).pop()));
+ },
+ focus: function() {
+ return false;
+ },
+ select: function(event, ui) {
+ var terms = this.value.split(/,\s*/);
+ terms.pop();
+ terms.push(ui.item.value);
+ terms.push('');
+ this.value = terms.join(', ');
+ return false;
+ }
+ });
+
+ $('#cancel-btn').prop('disabled', false);
+ $('#top-save-btn').show();
+ $('#cancel-btn').show();
+ $('#commit-btn').show();
+ },
+ function() {
+ $('#mode-btn-readonly').show();
+ $('#mode-btn-loading').hide();
+ $('#mode-btn').prop('disabled', false);
+ $('#mode-btn').show();
+ $('#cancel-btn').hide();
+ $('#commit-btn').hide();
+
+ $('.edit-show').hide();
+ $('.edit-hide').show();
+ }
+ );
+ });
+ $('#mode-btn').prop('disabled', false);
+
+ // cc add/remove
+ $('#cc-btn')
+ .click(function(event) {
+ event.preventDefault();
+ var is_cced = $(event.target).data('is-cced') == '1';
+
+ var cc_change;
+ if (is_cced) {
+ cc_change = { remove: [ BUGZILLA.user.login ] };
+ $('#cc-btn')
+ .text('Follow')
+ .data('is-cced', '0')
+ .prop('disabled', true);
+ }
+ else {
+ cc_change = { add: [ BUGZILLA.user.login ] };
+ $('#cc-btn')
+ .text('Stop following')
+ .data('is-cced', '1')
+ .prop('disabled', true);
+ }
+
+ bugzilla_ajax(
+ {
+ url: 'rest/bug/' + BUGZILLA.bug_id,
+ type: 'PUT',
+ data: JSON.stringify({ cc: cc_change })
+ },
+ function(data) {
+ $('#cc-btn').prop('disabled', false);
+ if (!(data.bugs[0].changes && data.bugs[0].changes.cc))
+ return;
+ if (data.bugs[0].changes.cc.added == BUGZILLA.user.login) {
+ $('#cc-btn')
+ .text('Stop following')
+ .data('is-cced', '1');
+ }
+ else if (data.bugs[0].changes.cc.removed == BUGZILLA.user.login) {
+ $('#cc-btn')
+ .text('Follow')
+ .data('is-cced', '0');
+ }
+ },
+ function(message) {
+ $('#cc-btn').prop('disabled', false);
+ }
+ );
+
+ });
+
+ // cancel button, reset the ui back to read-only state
+ // for now, do this with a redirect to self
+ // ideally this should revert all field back to their initially loaded
+ // values and switch the ui back to read-only mode without the redirect
+ $('#cancel-btn')
+ .click(function(event) {
+ event.preventDefault();
+ window.location.replace($('#this-bug').val());
+ });
+
+ // top comment button, scroll the textarea into view
+ $('.comment-btn')
+ .click(function(event) {
+ event.preventDefault();
+ // focus first to grow the textarea, so we scroll to the correct location
+ $('#comment').focus();
+ scroll_to($('#bottom-save-btn'));
+ });
+
+ // needinfo in people section -> scroll to near-comment ui
+ $('#needinfo-scroll')
+ .click(function(event) {
+ event.preventDefault();
+ scroll_to($('#needinfo_role'), function() { $('#needinfo_role').focus(); });
+ });
+
+ // knob
+ $('#bug_status')
+ .change(function(event) {
+ if (event.target.value == "RESOLVED" || event.target.value == "VERIFIED") {
+ $('#resolution').change().show();
+ }
+ else {
+ $('#resolution').hide();
+ $('#duplicate-container').hide();
+ $('#mark-as-dup-btn').show();
+ }
+ })
+ .change();
+ $('#resolution')
+ .change(function(event) {
+ if (event.target.value == "DUPLICATE") {
+ $('#duplicate-container').show();
+ $('#mark-as-dup-btn').hide();
+ $('#dup_id').focus();
+ }
+ else {
+ $('#duplicate-container').hide();
+ $('#mark-as-dup-btn').show();
+ }
+ })
+ .change();
+ $('#mark-as-dup-btn')
+ .click(function(event) {
+ event.preventDefault();
+ $('#bug_status').val('RESOLVED').change();
+ $('#resolution').val('DUPLICATE').change();
+ $('#dup_id').focus();
+ });
+
+ // add see-also button
+ $('.bug-urls-btn')
+ .click(function(event) {
+ event.preventDefault();
+ var name = event.target.id.replace(/-btn$/, '');
+ $(event.target).hide();
+ $('#' + name).show().focus();
+ });
+
+ // bug flag value <select>
+ $('.bug-flag')
+ .change(function(event) {
+ var target = $(event.target);
+ var id = target.prop('id').replace(/^flag(_type)?-(\d+)/, "#requestee$1-$2");
+ if (target.val() == '?') {
+ $(id + '-container').show();
+ $(id).focus().select();
+ }
+ else {
+ $(id + '-container').hide();
+ }
+ });
+
+ // tracking flags
+ $('.tracking-flags select')
+ .change(function(event) {
+ tracking_flag_change(event.target);
+ });
+
+ // add attachments
+ $('#attachments-add-btn')
+ .click(function(event) {
+ event.preventDefault();
+ window.location.replace('attachment.cgi?bugid=' + BUGZILLA.bug_id + '&action=enter');
+ });
+
+ // take button
+ $('#take-btn')
+ .click(function(event) {
+ event.preventDefault();
+ $('#field-assigned_to .edit-hide').hide();
+ $('#field-assigned_to .edit-show').show();
+ $('#assigned_to').val(BUGZILLA.user.login).focus().select();
+ $('#top-save-btn').show();
+ });
+
+ // reply button
+ $('.reply-btn')
+ .click(function(event) {
+ event.preventDefault();
+ var comment_id = $(event.target).data('reply-id');
+ var comment_author = $(event.target).data('reply-name');
+
+ var prefix = "(In reply to " + comment_author + " from comment #" + comment_id + ")\n";
+ var reply_text = "";
+ if (BUGZILLA.user.settings.quote_replies == 'quoted_reply') {
+ var text = $('#ct-' + comment_id).text();
+ reply_text = prefix + wrapReplyText(text);
+ }
+ else if (BUGZILLA.user.settings.quote_replies == 'simply_reply') {
+ reply_text = prefix;
+ }
+
+ // quoting a private comment, check the 'private' cb
+ $('#add-comment-private-cb').prop('checked',
+ $('#add-comment-private-cb:checked').length || $('#is-private-' + comment_id + ':checked').length);
+
+ // remove embedded links to attachment details
+ reply_text = reply_text.replace(/(attachment\s+\d+)(\s+\[[^\[\n]+\])+/gi, '$1');
+
+ if ($('#comment').val() != reply_text) {
+ $('#comment').val($('#comment').val() + reply_text);
+ }
+ scroll_to($('#comment'), function() { $('#comment').focus(); });
+ });
+
+ // add comment --> enlarge on focus
+ if (BUGZILLA.user.settings.zoom_textareas) {
+ $('#comment')
+ .focus(function(event) {
+ $(event.target).attr('rows', 25);
+ });
+ }
+
+ // add comment --> private
+ $('#add-comment-private-cb')
+ .click(function(event) {
+ if ($(event.target).prop('checked')) {
+ $('#comment').addClass('private-comment');
+ }
+ else {
+ $('#comment').removeClass('private-comment');
+ }
+ });
+
+ // show "save changes" button if there are any immediately editable elements
+ if ($('.module select:visible').length || $('.module input:visible').length) {
+ $('#top-save-btn').show();
+ }
+
+ // status/resolve as buttons
+ $('.resolution-btn')
+ .click(function(event) {
+ event.preventDefault();
+ $('#field-status-view').hide();
+ $('#field-status-edit .edit-hide').hide();
+ $('#field-status-edit .edit-show').show();
+ $('#field-status-edit').show();
+ $('#bug_status').val('RESOLVED').change();
+ $('#resolution').val($(event.target).text()).change();
+ $('#top-save-btn').show();
+ if ($(event.target).text() == "DUPLICATE") {
+ scroll_to($('body'));
+ }
+ else {
+ scroll_to($('body'), function() { $('#resolution').focus(); });
+ }
+ });
+ $('.status-btn')
+ .click(function(event) {
+ event.preventDefault();
+ $('#field-status-view').hide();
+ $('#field-status-edit .edit-hide').hide();
+ $('#field-status-edit .edit-show').show();
+ $('#field-status-edit').show();
+ $('#bug_status').val($(event.target).data('status')).change();
+ $('#top-save-btn').show();
+ scroll_to($('body'), function() { $('#bug_status').focus(); });
+ });
+
+ // vote button
+ // ideally this should function like CC and xhr it, but that would require
+ // a rewrite of the voting extension
+ $('#vote-btn')
+ .click(function(event) {
+ event.preventDefault();
+ window.location.replace('page.cgi?id=voting/user.html&bug_id=' + BUGZILLA.bug_id + '#vote_' + BUGZILLA.bug_id);
+ });
+
+ // user-story
+ $('#user-story-edit-btn')
+ .click(function(event) {
+ event.preventDefault();
+ $('#user-story').hide();
+ $('#user-story-edit-btn').hide();
+ $('#cf_user_story').show().focus().select();
+ $('#top-save-btn').show();
+ });
+ $('#user-story-reply-btn')
+ .click(function(event) {
+ event.preventDefault();
+ var text = "(Commenting on User Story)\n" + wrapReplyText($('#cf_user_story').val());
+ var current = $('#comment').val();
+ if (current != text) {
+ $('#comment').val(current + text);
+ $('#comment').focus();
+ scroll_to($('#bottom-save-btn'));
+ }
+ });
+
+ // custom textarea fields
+ $('.edit-textarea-btn')
+ .click(function(event) {
+ event.preventDefault();
+ var id = $(event.target).attr('id').replace(/-edit$/, '');
+ $(event.target).hide();
+ $('#' + id + '-view').hide();
+ $('#' + id).show().focus().select();
+ });
+
+ // date/datetime pickers
+ $('.cf_datetime').datetimepicker({
+ format: 'Y-m-d G:i:s',
+ datepicker: true,
+ timepicker: true,
+ scrollInput: false,
+ lazyInit: false, // there's a bug which prevents img->show from working with lazy:true
+ closeOnDateSelect: true
+ });
+ $('.cf_date').datetimepicker({
+ format: 'Y-m-d',
+ datepicker: true,
+ timepicker: false,
+ scrollInput: false,
+ lazyInit: false,
+ closeOnDateSelect: true
+ });
+ $('.cf_datetime-img, .cf_date-img')
+ .click(function(event) {
+ var id = $(event.target).attr('id').replace(/-img$/, '');
+ $('#' + id).datetimepicker('show');
+ });
+
+ // new bug button
+ $.contextMenu({
+ selector: '#new-bug-btn',
+ trigger: 'left',
+ items: [
+ {
+ name: 'Create a new Bug',
+ callback: function() {
+ window.open('enter_bug.cgi', '_blank');
+ }
+ },
+ {
+ name: '&hellip; in this product',
+ callback: function() {
+ window.open('enter_bug.cgi?product=' + encodeURIComponent($('#product').val()), '_blank');
+ }
+ },
+ {
+ name: '&hellip; in this component',
+ callback: function() {
+ window.open('enter_bug.cgi?' +
+ 'product=' + encodeURIComponent($('#product').val()) +
+ '&component=' + encodeURIComponent($('#component').val()), '_blank');
+ }
+ },
+ {
+ name: '&hellip; that blocks this bug',
+ callback: function() {
+ window.open('enter_bug.cgi?format=__default__' +
+ '&product=' + encodeURIComponent($('#product').val()) +
+ '&blocked=' + BUGZILLA.bug_id, '_blank');
+ }
+ },
+ {
+ name: '&hellip; that depends on this bug',
+ callback: function() {
+ window.open('enter_bug.cgi?format=__default__' +
+ '&product=' + encodeURIComponent($('#product').val()) +
+ '&dependson=' + BUGZILLA.bug_id, '_blank');
+ }
+ },
+ {
+ name: '&hellip; as a clone of this bug',
+ callback: function() {
+ window.open('enter_bug.cgi?format=__default__' +
+ '&product=' + encodeURIComponent($('#product').val()) +
+ '&cloned_bug_id=' + BUGZILLA.bug_id, '_blank');
+ }
+ },
+ {
+ name: '&hellip; as a clone, in a different product',
+ callback: function() {
+ window.open('enter_bug.cgi?format=__default__' +
+ '&cloned_bug_id=' + BUGZILLA.bug_id, '_blank');
+ }
+ },
+ ]
+ });
+
+});
+
+function confirmUnsafeURL(url) {
+ return confirm(
+ 'This is considered an unsafe URL and could possibly be harmful.\n' +
+ 'The full URL is:\n\n' + url + '\n\nContinue?');
+}
+
+// fix url after bug creation/update
+if (history && history.replaceState) {
+ var href = document.location.href;
+ if (!href.match(/show_bug\.cgi/)) {
+ history.replaceState(null, BUGZILLA.bug_title, 'show_bug.cgi?id=' + BUGZILLA.bug_id);
+ document.title = BUGZILLA.bug_title;
+ }
+ if (href.match(/show_bug\.cgi\?.*list_id=/)) {
+ href = href.replace(/[\?&]+list_id=(\d+|cookie)/, '');
+ history.replaceState(null, BUGZILLA.bug_title, href);
+ }
+}
+
+// ajax wrapper, to simplify error handling and auth
+function bugzilla_ajax(request, done_fn, error_fn) {
+ $('#xhr-error').hide('');
+ $('#xhr-error').html('');
+ request.url += (request.url.match('\\?') ? '&' : '?') +
+ 'Bugzilla_api_token=' + encodeURIComponent(BUGZILLA.api_token);
+ if (request.type != 'GET') {
+ request.contentType = 'application/json';
+ }
+ $.ajax(request)
+ .done(function(data) {
+ if (data.error) {
+ $('#xhr-error').html(data.message);
+ $('#xhr-error').show('fast');
+ if (error_fn)
+ error_fn(data.message);
+ }
+ else if (done_fn) {
+ done_fn(data);
+ }
+ })
+ .error(function(data) {
+ $('#xhr-error').html(data.responseJSON.message);
+ $('#xhr-error').show('fast');
+ if (error_fn)
+ error_fn(data.responseJSON.message);
+ });
+}
+
+// no-ops
+function initHidingOptionsForIE() {}
+function showFieldWhen() {}
diff --git a/extensions/BugModal/web/calendar.png b/extensions/BugModal/web/calendar.png
new file mode 100644
index 000000000..5eef6597a
--- /dev/null
+++ b/extensions/BugModal/web/calendar.png
Binary files differ
diff --git a/extensions/BugModal/web/throbber.gif b/extensions/BugModal/web/throbber.gif
new file mode 100644
index 000000000..bc4fa6561
--- /dev/null
+++ b/extensions/BugModal/web/throbber.gif
Binary files differ
diff --git a/extensions/TrackingFlags/web/js/tracking_flags.js b/extensions/TrackingFlags/web/js/tracking_flags.js
index b0bdb2ebd..0b899676c 100644
--- a/extensions/TrackingFlags/web/js/tracking_flags.js
+++ b/extensions/TrackingFlags/web/js/tracking_flags.js
@@ -55,16 +55,18 @@ function tracking_flag_change(e) {
// create "comment required"
var span = document.createElement('span');
span.id = 'cr_' + e.id;
- span.appendChild(document.createTextNode('('));
+ span.appendChild(document.createTextNode(' ('));
var a = document.createElement('a');
a.appendChild(document.createTextNode('comment required'));
a.href = '#';
- a.onclick = function() {
+ a.onclick = function(event) {
+ event.preventDefault();
var c = document.getElementById('comment');
c.focus();
c.select();
- document.getElementById('add_comment').scrollIntoView();
- return false;
+ var btn = document.getElementById('add_comment') || document.getElementById('add-comment');
+ if (btn)
+ btn.scrollIntoView();
};
span.appendChild(a);
span.appendChild(document.createTextNode(')'));