diff options
Diffstat (limited to 'extensions/BugModal')
27 files changed, 4288 insertions, 0 deletions
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> + • + <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> + • + <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 %]&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; + ' → '; + 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, '…') 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, '…') 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 %]&action=edit">Details</a> + [% IF attachment.ispatch %] + | <a href="attachment.cgi?id=[% attachment.id FILTER none %]&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 %] + [%~ '&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 ↓</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">▸</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">▸</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 %]& + [%~ %]product=[% bug.product FILTER uri %]& + [%~ %]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">▸</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 %]&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, '…')); + 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 ↑</button> + <button type="button" id="new-bug-btn" class="minor">New [% terms.Bug %] ▾</button> + </div> + </div> + </form> +[% ELSE %] + <div id="login-required"> + You need to <a href="show_bug.cgi?id=[% bug.bug_id FILTER none %]&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 ? "▸" : "▾" %]</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"> </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 Binary files differnew file mode 100644 index 000000000..8bad6a3e3 --- /dev/null +++ b/extensions/BugModal/web/ZeroClipboard/ZeroClipboard.swf 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('▸'); + }); + 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('▾'); + }); + 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') ? '▾' : '▸'); + }); + }); + + // 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('▸'); + el_for.hide(); + } + else { + latch.data('expanded', true).html('▾'); + 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('▸'); + $('#cc-list').hide(); + } + else { + latch.data('expanded', true).html('▾'); + $('#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: '… in this product', + callback: function() { + window.open('enter_bug.cgi?product=' + encodeURIComponent($('#product').val()), '_blank'); + } + }, + { + name: '… in this component', + callback: function() { + window.open('enter_bug.cgi?' + + 'product=' + encodeURIComponent($('#product').val()) + + '&component=' + encodeURIComponent($('#component').val()), '_blank'); + } + }, + { + name: '… that blocks this bug', + callback: function() { + window.open('enter_bug.cgi?format=__default__' + + '&product=' + encodeURIComponent($('#product').val()) + + '&blocked=' + BUGZILLA.bug_id, '_blank'); + } + }, + { + name: '… that depends on this bug', + callback: function() { + window.open('enter_bug.cgi?format=__default__' + + '&product=' + encodeURIComponent($('#product').val()) + + '&dependson=' + BUGZILLA.bug_id, '_blank'); + } + }, + { + name: '… 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: '… 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 Binary files differnew file mode 100644 index 000000000..5eef6597a --- /dev/null +++ b/extensions/BugModal/web/calendar.png diff --git a/extensions/BugModal/web/throbber.gif b/extensions/BugModal/web/throbber.gif Binary files differnew file mode 100644 index 000000000..bc4fa6561 --- /dev/null +++ b/extensions/BugModal/web/throbber.gif |