summaryrefslogtreecommitdiffstats
path: root/extensions/TrackingFlags
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/TrackingFlags')
-rw-r--r--extensions/TrackingFlags/lib/Admin.pm421
-rw-r--r--extensions/TrackingFlags/lib/Constants.pm45
-rw-r--r--extensions/TrackingFlags/lib/Flag.pm399
-rw-r--r--extensions/TrackingFlags/lib/Flag/Bug.pm171
-rw-r--r--extensions/TrackingFlags/lib/Flag/Value.pm130
-rw-r--r--extensions/TrackingFlags/lib/Flag/Visibility.pm172
-rw-r--r--extensions/TrackingFlags/template/en/default/bug/tracking_flags.html.tmpl57
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/admin/admin-end_links_right.html.tmpl18
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/create/create-bug_flags_end.html.tmpl33
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/create/create-form.html.tmpl63
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/create/create-project_flags_end.html.tmpl18
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/create/create-tracking_flags_end.html.tmpl18
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-bug_flags_end.html.tmpl33
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-project_flags_end.html.tmpl18
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-tracking_flags_end.html.tmpl18
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl191
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/field-editable.html.tmpl38
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/field-non_editable.html.tmpl9
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/show-header-end.html.tmpl10
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/global/code-error-errors.html.tmpl26
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/global/messages-messages.html.tmpl15
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/global/user-error-errors.html.tmpl57
-rw-r--r--extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_edit.html.tmpl194
-rw-r--r--extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_list.html.tmpl56
-rw-r--r--extensions/TrackingFlags/web/js/admin.js384
-rw-r--r--extensions/TrackingFlags/web/js/tracking_flags.js56
-rw-r--r--extensions/TrackingFlags/web/styles/admin.css107
-rw-r--r--extensions/TrackingFlags/web/styles/edit_bug.css18
28 files changed, 2775 insertions, 0 deletions
diff --git a/extensions/TrackingFlags/lib/Admin.pm b/extensions/TrackingFlags/lib/Admin.pm
new file mode 100644
index 000000000..9e8b33b43
--- /dev/null
+++ b/extensions/TrackingFlags/lib/Admin.pm
@@ -0,0 +1,421 @@
+# 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::TrackingFlags::Admin;
+
+use strict;
+use warnings;
+
+use Bugzilla;
+use Bugzilla::Component;
+use Bugzilla::Error;
+use Bugzilla::Group;
+use Bugzilla::Product;
+use Bugzilla::Util qw(trim detaint_natural);
+
+use Bugzilla::Extension::TrackingFlags::Constants;
+use Bugzilla::Extension::TrackingFlags::Flag;
+use Bugzilla::Extension::TrackingFlags::Flag::Value;
+use Bugzilla::Extension::TrackingFlags::Flag::Visibility;
+
+use JSON;
+use Scalar::Util qw(blessed);
+
+use base qw(Exporter);
+our @EXPORT = qw(
+ admin_list
+ admin_edit
+);
+
+#
+# page loading
+#
+
+sub admin_list {
+ my ($vars) = @_;
+
+ $vars->{flags} = [ Bugzilla::Extension::TrackingFlags::Flag->get_all() ];
+}
+
+sub admin_edit {
+ my ($vars, $page) = @_;
+ my $input = Bugzilla->input_params;
+
+ $vars->{groups} = _groups_to_json();
+ $vars->{mode} = $input->{mode} || 'new';
+ $vars->{flag_id} = $input->{flag_id} || 0;
+ $vars->{tracking_flag_types} = FLAG_TYPES;
+
+ if ($input->{delete}) {
+ my $flag = Bugzilla::Extension::TrackingFlags::Flag->new($vars->{flag_id})
+ || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $vars->{flag_id} });
+ $flag->remove_from_db();
+
+ $vars->{mode} = 'deleted';
+ $vars->{flag_name} = $flag->name;
+
+ } elsif ($input->{save}) {
+ # save
+
+ my ($flag, $values, $visibilities) = _load_from_input($input, $vars);
+ _validate($flag, $values, $visibilities);
+ my $flag_obj = _update_db($flag, $values, $visibilities);
+
+ $vars->{flag} = $flag_obj;
+ $vars->{values} = _flag_values_to_json($values);
+ $vars->{visibility} = _flag_visibility_to_json($visibilities);
+ $vars->{can_delete} = !$flag_obj->has_values;
+
+ if ($vars->{mode} eq 'new') {
+ $vars->{message} = 'tracking_flags_created';
+ } else {
+ $vars->{message} = 'tracking_flags_updated';
+ }
+
+ } else {
+ # initial load
+
+ if ($vars->{mode} eq 'edit') {
+ # edit - straight load
+ my $flag = Bugzilla::Extension::TrackingFlags::Flag->new($vars->{flag_id})
+ || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $vars->{flag_id} });
+ $vars->{flag} = $flag;
+ $vars->{values} = _flag_values_to_json($flag->values);
+ $vars->{visibility} = _flag_visibility_to_json($flag->visibility);
+ $vars->{can_delete} = !$flag->has_values;
+
+ } elsif ($vars->{mode} eq 'copy') {
+ # copy - load the source flag
+ $vars->{mode} = 'new';
+ my $flag = Bugzilla::Extension::TrackingFlags::Flag->new($input->{copy_from})
+ || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $vars->{copy_from} });
+
+ # increment the number at the end of the name and description
+ if ($flag->name =~ /^(\D+)(\d+)$/) {
+ $flag->set_name("$1" . ($2 + 1));
+ }
+ if ($flag->description =~ /^(\D+)(\d+)$/) {
+ $flag->set_description("$1" . ($2 + 1));
+ }
+ $flag->set_sortkey(_next_unique_sortkey($flag->sortkey));
+ $flag->set_type($flag->flag_type);
+ # always default new flags as active, even when copying an inactive one
+ $flag->set_is_active(1);
+
+ $vars->{flag} = $flag;
+ $vars->{values} = _flag_values_to_json($flag->values, 1);
+ $vars->{visibility} = _flag_visibility_to_json($flag->visibility, 1);
+ $vars->{can_delete} = 0;
+
+ } else {
+ $vars->{mode} = 'new';
+ $vars->{flag} = {
+ sortkey => 0,
+ is_active => 1,
+ };
+ $vars->{values} = _flag_values_to_json([
+ {
+ id => 0,
+ value => '---',
+ setter_group_id => '',
+ is_active => 1,
+ },
+ ]);
+ $vars->{visibility} = '';
+ $vars->{can_delete} = 0;
+ }
+ }
+}
+
+sub _load_from_input {
+ my ($input, $vars) = @_;
+
+ # flag
+
+ my $flag = {
+ id => $input->{flag_id} || 0,
+ name => trim($input->{flag_name} || ''),
+ description => trim($input->{flag_desc} || ''),
+ sortkey => $input->{flag_sort} || 0,
+ type => trim($input->{flag_type} || ''),
+ is_active => $input->{flag_active} ? 1 : 0,
+ };
+ detaint_natural($flag->{id});
+ detaint_natural($flag->{sortkey});
+ detaint_natural($flag->{is_active});
+
+ # values
+
+ my $values = decode_json($input->{values} || '[]');
+ foreach my $value (@$values) {
+ $value->{value} = '' unless exists $value->{value} && defined $value->{value};
+ $value->{setter_group_id} = '' unless $value->{setter_group_id};
+ $value->{is_active} = $value->{is_active} ? 1 : 0;
+ }
+
+ # vibility
+
+ my $visibilities = decode_json($input->{visibility} || '[]');
+ foreach my $visibility (@$visibilities) {
+ $visibility->{product} = '' unless exists $visibility->{product} && defined $visibility->{product};
+ $visibility->{component} = '' unless exists $visibility->{component} && defined $visibility->{component};
+ }
+
+ return ($flag, $values, $visibilities);
+}
+
+sub _next_unique_sortkey {
+ my ($sortkey) = @_;
+
+ my %current;
+ foreach my $flag (Bugzilla::Extension::TrackingFlags::Flag->get_all()) {
+ $current{$flag->sortkey} = 1;
+ }
+
+ $sortkey += 5;
+ $sortkey += 5 while exists $current{$sortkey};
+ return $sortkey;
+}
+
+#
+# validation
+#
+
+sub _validate {
+ my ($flag, $values, $visibilities) = @_;
+
+ # flag
+
+ my @missing;
+ push @missing, 'Field Name' if $flag->{name} eq '';
+ push @missing, 'Field Description' if $flag->{description} eq '';
+ push @missing, 'Field Sort Key' if $flag->{sortkey} eq '';
+ scalar(@missing)
+ && ThrowUserError('tracking_flags_missing_mandatory', { fields => \@missing });
+
+ $flag->{name} =~ /^cf_/
+ || ThrowUserError('tracking_flags_cf_prefix');
+
+ if ($flag->{id}) {
+ my $old_flag = Bugzilla::Extension::TrackingFlags::Flag->new($flag->{id})
+ || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $flag->{id} });
+ if ($flag->{name} ne $old_flag->name) {
+ Bugzilla::Field->new({ name => $flag->{name} })
+ && ThrowUserError('field_already_exists', { field => { name => $flag->{name} }});
+ }
+ } else {
+ Bugzilla::Field->new({ name => $flag->{name} })
+ && ThrowUserError('field_already_exists', { field => { name => $flag->{name} }});
+ }
+
+ # values
+
+ scalar(@$values)
+ || ThrowUserError('tracking_flags_missing_values');
+
+ my %seen;
+ foreach my $value (@$values) {
+ my $v = $value->{value};
+
+ $v eq ''
+ && ThrowUserError('tracking_flags_missing_value');
+
+ exists $seen{$v}
+ && ThrowUserError('tracking_flags_duplicate_value', { value => $v });
+ $seen{$v} = 1;
+
+ push @missing, "Setter for $v" if !$value->{setter_group_id};
+ }
+ scalar(@missing)
+ && ThrowUserError('tracking_flags_missing_mandatory', { fields => \@missing });
+
+ # visibility
+
+ scalar(@$visibilities)
+ || ThrowUserError('tracking_flags_missing_visibility');
+
+ %seen = ();
+ foreach my $visibility (@$visibilities) {
+ my $name = $visibility->{product} . ':' . $visibility->{component};
+
+ exists $seen{$name}
+ && ThrowUserError('tracking_flags_duplicate_visibility', { name => $name });
+
+ $visibility->{product_obj} = Bugzilla::Product->new({ name => $visibility->{product} })
+ || ThrowCodeError('tracking_flags_invalid_product', { product => $visibility->{product} });
+
+ if ($visibility->{component} ne '') {
+ $visibility->{component_obj} = Bugzilla::Component->new({ product => $visibility->{product_obj},
+ name => $visibility->{component} })
+ || ThrowCodeError('tracking_flags_invalid_component', { component => $visibility->{component} });
+ }
+ }
+
+}
+
+#
+# database updating
+#
+
+sub _update_db {
+ my ($flag, $values, $visibilities) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ $dbh->bz_start_transaction();
+ my $flag_obj = _update_db_flag($flag);
+ _update_db_values($flag_obj, $flag, $values);
+ _update_db_visibility($flag_obj, $flag, $visibilities);
+ $dbh->bz_commit_transaction();
+
+ return $flag_obj;
+}
+
+sub _update_db_flag {
+ my ($flag) = @_;
+
+ my $object_set = {
+ name => $flag->{name},
+ description => $flag->{description},
+ sortkey => $flag->{sortkey},
+ type => $flag->{type},
+ is_active => $flag->{is_active},
+ };
+
+ my $flag_obj;
+ if ($flag->{id}) {
+ # update existing flag
+ $flag_obj = Bugzilla::Extension::TrackingFlags::Flag->new($flag->{id})
+ || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $flag->{id} });
+ $flag_obj->set_all($object_set);
+ $flag_obj->update();
+
+ } else {
+ # create new flag
+ $flag_obj = Bugzilla::Extension::TrackingFlags::Flag->create($object_set);
+ }
+
+ return $flag_obj;
+}
+
+sub _update_db_values {
+ my ($flag_obj, $flag, $values) = @_;
+
+ # delete
+ foreach my $current_value (@{ $flag_obj->values }) {
+ if (!grep { $_->{id} == $current_value->id } @$values) {
+ $current_value->remove_from_db();
+ }
+ }
+
+ # add/update
+ my $sortkey = 0;
+ foreach my $value (@{ $values }) {
+ $sortkey += 10;
+
+ my $object_set = {
+ value => $value->{value},
+ setter_group_id => $value->{setter_group_id},
+ is_active => $value->{is_active},
+ sortkey => $sortkey,
+ };
+
+ if ($value->{id}) {
+ my $value_obj = Bugzilla::Extension::TrackingFlags::Flag::Value->new($value->{id})
+ || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag value', id => $flag->{id} });
+ $value_obj->set_all($object_set);
+ $value_obj->update();
+ } else {
+ $object_set->{tracking_flag_id} = $flag_obj->flag_id;
+ Bugzilla::Extension::TrackingFlags::Flag::Value->create($object_set);
+ }
+ }
+}
+
+sub _update_db_visibility {
+ my ($flag_obj, $flag, $visibilities) = @_;
+
+ # delete
+ foreach my $current_visibility (@{ $flag_obj->visibility }) {
+ if (!grep { $_->{id} == $current_visibility->id } @$visibilities) {
+ $current_visibility->remove_from_db();
+ }
+ }
+
+ # add
+ foreach my $visibility (@{ $visibilities }) {
+ next if $visibility->{id};
+ Bugzilla::Extension::TrackingFlags::Flag::Visibility->create({
+ tracking_flag_id => $flag_obj->flag_id,
+ product_id => $visibility->{product_obj}->id,
+ component_id => $visibility->{component} ? $visibility->{component_obj}->id : undef,
+ });
+ }
+}
+
+#
+# serialisation
+#
+
+sub _groups_to_json {
+ my @data;
+ foreach my $group (sort { $a->name cmp $b->name } Bugzilla::Group->get_all()) {
+ push @data, {
+ id => $group->id,
+ name => $group->name,
+ };
+ }
+ return encode_json(\@data);
+}
+
+sub _flag_values_to_json {
+ my ($values, $is_copy) = @_;
+ # setting is_copy will set the id's to zero, to force new values rather
+ # than editing existing ones
+ my @data;
+ foreach my $value (@$values) {
+ push @data, {
+ id => $is_copy ? 0 : $value->{id},
+ value => $value->{value},
+ setter_group_id => $value->{setter_group_id},
+ is_active => $value->{is_active} ? JSON::true : JSON::false,
+ };
+ }
+ return encode_json(\@data);
+}
+
+sub _flag_visibility_to_json {
+ my ($visibilities, $is_copy) = @_;
+ # setting is_copy will set the id's to zero, to force new visibilites
+ # rather than editing existing ones
+ my @data;
+
+ foreach my $visibility (@$visibilities) {
+ my $product = exists $visibility->{product_id}
+ ? $visibility->product->name
+ : $visibility->{product};
+ my $component;
+ if (exists $visibility->{component_id} && $visibility->{component_id}) {
+ $component = $visibility->component->name;
+ } elsif (exists $visibility->{component}) {
+ $component = $visibility->{component};
+ } else {
+ $component = undef;
+ }
+ push @data, {
+ id => $is_copy ? 0 : $visibility->{id},
+ product => $product,
+ component => $component,
+ };
+ }
+ @data = sort {
+ lc($a->{product}) cmp lc($b->{product})
+ || lc($a->{component}) cmp lc($b->{component})
+ } @data;
+ return encode_json(\@data);
+}
+
+1;
diff --git a/extensions/TrackingFlags/lib/Constants.pm b/extensions/TrackingFlags/lib/Constants.pm
new file mode 100644
index 000000000..edeb1a62b
--- /dev/null
+++ b/extensions/TrackingFlags/lib/Constants.pm
@@ -0,0 +1,45 @@
+# 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::TrackingFlags::Constants;
+
+use strict;
+use base qw(Exporter);
+
+our @EXPORT = qw(
+ FLAG_TYPES
+);
+
+use constant FLAG_TYPES => [
+ {
+ name => 'tracking',
+ description => 'Tracking Flags',
+ collapsed => 1,
+ },
+ {
+ name => 'project',
+ description => 'Project Flags',
+ collapsed => 0,
+ },
+ {
+ name => 'status',
+ description => 'Status Flags',
+ collapsed => 1,
+ },
+ {
+ name => 'blocking',
+ description => 'Blocking Flags',
+ collapsed => 1,
+ },
+ {
+ name => 'b2g',
+ description => 'B2G Flags',
+ collapsed => 1,
+ }
+];
+
+1;
diff --git a/extensions/TrackingFlags/lib/Flag.pm b/extensions/TrackingFlags/lib/Flag.pm
new file mode 100644
index 000000000..2151c3763
--- /dev/null
+++ b/extensions/TrackingFlags/lib/Flag.pm
@@ -0,0 +1,399 @@
+# 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::TrackingFlags::Flag;
+
+use base qw(Bugzilla::Object);
+
+use strict;
+use warnings;
+
+use Bugzilla::Error;
+use Bugzilla::Constants;
+use Bugzilla::Util qw(detaint_natural trim);
+
+use Bugzilla::Extension::TrackingFlags::Constants;
+use Bugzilla::Extension::TrackingFlags::Flag::Bug;
+use Bugzilla::Extension::TrackingFlags::Flag::Value;
+use Bugzilla::Extension::TrackingFlags::Flag::Visibility;
+
+###############################
+#### Initialization ####
+###############################
+
+use constant DB_TABLE => 'tracking_flags';
+
+use constant DB_COLUMNS => qw(
+ id
+ field_id
+ name
+ description
+ type
+ sortkey
+ is_active
+);
+
+use constant LIST_ORDER => 'sortkey';
+
+use constant UPDATE_COLUMNS => qw(
+ name
+ description
+ type
+ sortkey
+ is_active
+);
+
+use constant VALIDATORS => {
+ name => \&_check_name,
+ description => \&_check_description,
+ type => \&_check_type,
+ sortkey => \&_check_sortkey,
+ is_active => \&Bugzilla::Object::check_boolean,
+
+};
+
+use constant UPDATE_VALIDATORS => {
+ name => \&_check_name,
+ description => \&_check_description,
+ type => \&_check_type,
+ sortkey => \&_check_sortkey,
+ is_active => \&Bugzilla::Object::check_boolean,
+};
+
+###############################
+#### Methods ####
+###############################
+
+sub new {
+ my $class = shift;
+ my $param = shift;
+ my $cache = Bugzilla->request_cache;
+
+ if (!ref $param
+ && exists $cache->{'tracking_flags'}
+ && exists $cache->{'tracking_flags'}->{$param})
+ {
+ return $cache->{'tracking_flags'}->{$param};
+ }
+
+ return $class->SUPER::new($param);
+}
+
+sub create {
+ my $class = shift;
+ my $params = shift;
+ my $dbh = Bugzilla->dbh;
+
+ $dbh->bz_start_transaction();
+
+ $params = $class->run_create_validators($params);
+
+ # We have to create an entry for this new flag
+ # in the fielddefs table for use elsewhere. We cannot
+ # use Bugzilla::Field->create as it will create the
+ # additional tables needed by custom fields which we
+ # do not need. Also we do this so as not to add a
+ # another column to the bugs table.
+ # We will create the entry as a custom field with a
+ # type of FIELD_TYPE_EXTENSION so Bugzilla will skip
+ # these field types in certain parts of the core code.
+ $dbh->do("INSERT INTO fielddefs
+ (name, description, sortkey, type, custom, obsolete, buglist)
+ VALUES
+ (?, ?, ?, ?, ?, ?, ?)",
+ undef,
+ $params->{'name'},
+ $params->{'description'},
+ $params->{'sortkey'},
+ FIELD_TYPE_EXTENSION,
+ 1, 0, 1);
+ $params->{'field_id'} = $dbh->bz_last_key;
+
+ my $flag = $class->SUPER::create($params);
+
+ $dbh->bz_commit_transaction();
+
+ return $flag;
+}
+
+sub update {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ my $old_self = $self->new($self->flag_id);
+
+ # HACK! Bugzilla::Object::update uses hardcoded $self->id
+ # instead of $self->{ID_FIELD} so we need to reverse field_id
+ # and the real id temporarily
+ my $field_id = $self->id;
+ $self->{'field_id'} = $self->{'id'};
+
+ my $changes = $self->SUPER::update(@_);
+
+ $self->{'field_id'} = $field_id;
+
+ # Update the fielddefs entry
+ $dbh->do("UPDATE fielddefs SET name = ?, description = ? WHERE name = ?",
+ undef,
+ $self->name, $self->description, $old_self->name);
+
+ # Update request_cache
+ my $cache = Bugzilla->request_cache;
+ if (exists $cache->{'tracking_flags'}) {
+ $cache->{'tracking_flags'}->{$self->flag_id} = $self;
+ }
+
+ return $changes;
+}
+
+sub match {
+ my $class = shift;
+ my ($params) = @_;
+
+ # Use later for preload
+ my $bug_id = delete $params->{'bug_id'};
+
+ # Retrieve all flags relevant for the given product and component
+ if (!exists $params->{'id'}
+ && ($params->{'component'} || $params->{'component_id'}
+ || $params->{'product'} || $params->{'product_id'}))
+ {
+ my $visible_flags
+ = Bugzilla::Extension::TrackingFlags::Flag::Visibility->match(@_);
+ my @flag_ids = map { $_->tracking_flag_id } @$visible_flags;
+
+ delete $params->{'component'} if exists $params->{'component'};
+ delete $params->{'component_id'} if exists $params->{'component_id'};
+ delete $params->{'product'} if exists $params->{'product'};
+ delete $params->{'product_id'} if exists $params->{'product_id'};
+
+ $params->{'id'} = \@flag_ids;
+ }
+
+ my $flags = $class->SUPER::match(@_);
+
+ preload_all_the_things($flags, { bug_id => $bug_id });
+
+ return [ sort { $a->sortkey <=> $b->sortkey } @$flags ];
+}
+
+sub get_all {
+ my $self = shift;
+ my $cache = Bugzilla->request_cache;
+ if (!exists $cache->{'tracking_flags'}) {
+ my @tracking_flags = $self->SUPER::get_all(@_);
+ preload_all_the_things(\@tracking_flags);
+ my %tracking_flags_hash = map { $_->flag_id => $_ } @tracking_flags;
+ $cache->{'tracking_flags'} = \%tracking_flags_hash;
+ }
+ return values %{ $cache->{'tracking_flags'} };
+}
+
+sub remove_from_db {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_start_transaction();
+ $dbh->do('DELETE FROM fielddefs WHERE name = ?', undef, $self->name);
+ $self->SUPER::remove_from_db(@_);
+ $dbh->bz_commit_transaction();
+
+ # Remove from request cache
+ my $cache = Bugzilla->request_cache;
+ if (exists $cache->{'tracking_flags'}) {
+ delete $cache->{'tracking_flags'}->{$self->flag_id};
+ }
+}
+
+sub preload_all_the_things {
+ my ($flags, $params) = @_;
+
+ my %flag_hash = map { $_->flag_id => $_ } @$flags;
+ my @flag_ids = keys %flag_hash;
+ return unless @flag_ids;
+
+ # Preload values
+ my $value_objects
+ = Bugzilla::Extension::TrackingFlags::Flag::Value->match({ tracking_flag_id => \@flag_ids });
+
+ # Now populate the tracking flags with this set of value objects.
+ foreach my $obj (@$value_objects) {
+ my $flag_id = $obj->tracking_flag_id;
+
+ # Prepopulate the tracking flag object in the value object
+ $obj->{'tracking_flag'} = $flag_hash{$flag_id};
+
+ # Prepopulate the current value objects for this tracking flag
+ $flag_hash{$flag_id}->{'values'} ||= [];
+ push(@{$flag_hash{$flag_id}->{'values'}}, $obj);
+ }
+
+ # Preload bug values if a bug_id is passed
+ if ($params && exists $params->{'bug_id'} && $params->{'bug_id'}) {
+ # We don't want to use @flag_ids here as we want all flags attached to this bug
+ # even if they are inactive.
+ my $bug_objects
+ = Bugzilla::Extension::TrackingFlags::Flag::Bug->match({ bug_id => $params->{'bug_id'} });
+ # Now populate the tracking flags with this set of objects.
+ # Also we add them to the flag hash since we want them to be visible even if
+ # they are not longer applicable to this product/component.
+ foreach my $obj (@$bug_objects) {
+ my $flag_id = $obj->tracking_flag_id;
+
+ # Prepopulate the tracking flag object in the bug flag object
+ $obj->{'tracking_flag'} = $flag_hash{$flag_id};
+
+ # Prepopulate the the current bug flag object for the tracking flag
+ $flag_hash{$flag_id}->{'bug_flag'} = $obj;
+ }
+ }
+
+ @$flags = values %flag_hash;
+}
+
+###############################
+#### Validators ####
+###############################
+
+sub _check_name {
+ my ($invocant, $name) = @_;
+ $name = trim($name);
+ $name || ThrowCodeError('param_required', { param => 'name' });
+ return $name;
+}
+
+sub _check_description {
+ my ($invocant, $description) = @_;
+ $description = trim($description);
+ $description || ThrowCodeError( 'param_required', { param => 'description' } );
+ return $description;
+}
+
+sub _check_type {
+ my ($invocant, $type) = @_;
+ $type = trim($type);
+ $type || ThrowCodeError( 'param_required', { param => 'type' } );
+ grep($_->{name} eq $type, @{FLAG_TYPES()})
+ || ThrowUserError('tracking_flags_invalid_flag_type', { type => $type });
+ return $type;
+}
+
+sub _check_sortkey {
+ my ($invocant, $sortkey) = @_;
+ detaint_natural($sortkey)
+ || ThrowUserError('field_invalid_sortkey', { sortkey => $sortkey });
+ return $sortkey;
+}
+
+###############################
+#### Setters ####
+###############################
+
+sub set_name { $_[0]->set('name', $_[1]); }
+sub set_description { $_[0]->set('description', $_[1]); }
+sub set_type { $_[0]->set('type', $_[1]); }
+sub set_sortkey { $_[0]->set('sortkey', $_[1]); }
+sub set_is_active { $_[0]->set('is_active', $_[1]); }
+
+###############################
+#### Accessors ####
+###############################
+
+sub flag_id { return $_[0]->{'id'}; }
+sub name { return $_[0]->{'name'}; }
+sub description { return $_[0]->{'description'}; }
+sub flag_type { return $_[0]->{'type'}; }
+sub sortkey { return $_[0]->{'sortkey'}; }
+sub is_active { return $_[0]->{'is_active'}; }
+
+sub values {
+ return $_[0]->{'values'} ||= Bugzilla::Extension::TrackingFlags::Flag::Value->match({
+ tracking_flag_id => $_[0]->flag_id
+ });
+}
+
+sub visibility {
+ return $_[0]->{'visibility'} ||= Bugzilla::Extension::TrackingFlags::Flag::Visibility->match({
+ tracking_flag_id => $_[0]->flag_id
+ });
+}
+
+sub can_set_value {
+ my ($self, $new_value, $user) = @_;
+ $user ||= Bugzilla->user;
+ my $new_value_obj;
+ foreach my $value (@{$self->values}) {
+ if ($value->value eq $new_value) {
+ $new_value_obj = $value;
+ last;
+ }
+ }
+ return $new_value_obj && $user->in_group($new_value_obj->setter_group->name)
+ ? 1
+ : 0;
+}
+
+sub bug_flag {
+ my ($self, $bug_id) = @_;
+ # Return the current bug value object if defined unless the passed bug_id does
+ # not equal the current bug value objects id.
+ if (defined $self->{'bug_flag'}
+ && (!$bug_id || $self->{'bug_flag'}->bug->id == $bug_id))
+ {
+ return $self->{'bug_flag'};
+ }
+
+ # Flag::Bug->new will return a default bug value object if $params undefined
+ my $params = !$bug_id
+ ? undef
+ : { condition => "tracking_flag_id = ? AND bug_id = ?",
+ values => [ $self->flag_id, $bug_id ] };
+ return $self->{'bug_flag'} = Bugzilla::Extension::TrackingFlags::Flag::Bug->new($params);
+}
+
+sub has_values {
+ my ($self) = @_;
+ return $self->{'has_values'} if defined $self->{'has_values'};
+ my $dbh = Bugzilla->dbh;
+ $self->{'has_values'} = scalar $dbh->selectrow_array("
+ SELECT 1
+ FROM tracking_flags_bugs
+ WHERE tracking_flag_id = ? " .
+ $dbh->sql_limit(1),
+ undef, $self->flag_id);
+ return $self->{'has_values'};
+}
+
+######################################
+# Compatibility with Bugzilla::Field #
+######################################
+
+# Here we return 'field_id' instead of the real
+# id as we want other Bugzilla code to treat this
+# as a Bugzilla::Field object in certain places.
+sub id { return $_[0]->{'field_id'}; }
+sub type { return FIELD_TYPE_EXTENSION; }
+sub legal_values { return $_[0]->values; }
+sub custom { return 1; }
+sub in_new_bugmail { return 1; }
+sub obsolete { return 0; }
+sub enter_bug { return 1; }
+sub buglist { return 1; }
+sub is_select { return 1; }
+sub is_abnormal { return 1; }
+sub is_timetracking { return 0; }
+sub visibility_field { return undef; }
+sub visibility_values { return undef; }
+sub controls_visibility_of { return undef; }
+sub value_field { return undef; }
+sub controls_values_of { return undef; }
+sub is_visible_on_bug { return 1; }
+sub is_relationship { return 0; }
+sub reverse_desc { return ''; }
+sub is_mandatory { return 0; }
+sub is_numeric { return 0; }
+
+1;
diff --git a/extensions/TrackingFlags/lib/Flag/Bug.pm b/extensions/TrackingFlags/lib/Flag/Bug.pm
new file mode 100644
index 000000000..5e2886e66
--- /dev/null
+++ b/extensions/TrackingFlags/lib/Flag/Bug.pm
@@ -0,0 +1,171 @@
+# 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::TrackingFlags::Flag::Bug;
+
+use base qw(Bugzilla::Object);
+
+use strict;
+use warnings;
+
+use Bugzilla::Extension::TrackingFlags::Flag;
+
+use Bugzilla::Bug;
+use Bugzilla::Error;
+
+use Scalar::Util qw(blessed);
+
+###############################
+#### Initialization ####
+###############################
+
+use constant DEFAULT_FLAG_BUG => {
+ 'id' => 0,
+ 'tracking_flag_id' => 0,
+ 'bug_id' => '',
+ 'value' => '---',
+};
+
+use constant DB_TABLE => 'tracking_flags_bugs';
+
+use constant DB_COLUMNS => qw(
+ id
+ tracking_flag_id
+ bug_id
+ value
+);
+
+use constant LIST_ORDER => 'id';
+
+use constant UPDATE_COLUMNS => qw(
+ value
+);
+
+use constant VALIDATORS => {
+ tracking_flag_id => \&_check_tracking_flag,
+ value => \&_check_value,
+};
+
+use constant AUDIT_CREATES => 0;
+use constant AUDIT_UPDATES => 0;
+use constant AUDIT_REMOVES => 0;
+
+###############################
+#### Object Methods ####
+###############################
+
+sub new {
+ my $invocant = shift;
+ my $class = ref($invocant) || $invocant;
+ my ($param) = @_;
+
+ my $self;
+ if ($param) {
+ $self = $class->SUPER::new(@_);
+ if (!$self) {
+ $self = DEFAULT_FLAG_BUG;
+ bless($self, $class);
+ }
+ }
+ else {
+ $self = DEFAULT_FLAG_BUG;
+ bless($self, $class);
+ }
+
+ return $self
+}
+
+sub match {
+ my $class = shift;
+ my $bug_flags = $class->SUPER::match(@_);
+ preload_all_the_things($bug_flags);
+ return $bug_flags;
+}
+
+sub remove_from_db {
+ my ($self) = @_;
+ $self->SUPER::remove_from_db();
+ $self->{'id'} = $self->{'tracking_flag_id'} = $self->{'bug_id'} = 0;
+ $self->{'value'} = '---';
+}
+
+sub preload_all_the_things {
+ my ($bug_flags) = @_;
+ my $cache = Bugzilla->request_cache;
+
+ # Preload tracking flag objects
+ my @tracking_flag_ids;
+ foreach my $bug_flag (@$bug_flags) {
+ if (exists $cache->{'tracking_flags'}
+ && $cache->{'tracking_flags'}->{$bug_flag->tracking_flag_id})
+ {
+ $bug_flag->{'tracking_flag'}
+ = $cache->{'tracking_flags'}->{$bug_flag->tracking_flag_id};
+ next;
+ }
+ push(@tracking_flag_ids, $bug_flag->tracking_flag_id);
+ }
+
+ return unless @tracking_flag_ids;
+
+ my $tracking_flags
+ = Bugzilla::Extension::TrackingFlags::Flag->match({ id => \@tracking_flag_ids });
+ my %tracking_flag_hash = map { $_->flag_id => $_ } @$tracking_flags;
+
+ foreach my $bug_flag (@$bug_flags) {
+ next if exists $bug_flag->{'tracking_flag'};
+ $bug_flag->{'tracking_flag'} = $tracking_flag_hash{$bug_flag->tracking_flag_id};
+ }
+}
+
+###############################
+#### Validators ####
+###############################
+
+sub _check_value {
+ my ($invocant, $value) = @_;
+ $value || ThrowCodeError('param_required', { param => 'value' });
+ return $value;
+}
+
+sub _check_tracking_flag {
+ my ($invocant, $flag) = @_;
+ if (blessed $flag) {
+ return $flag->flag_id;
+ }
+ $flag = Bugzilla::Extension::TrackingFlags::Flag->new({ id => $flag, cache => 1 })
+ || ThrowCodeError('tracking_flags_invalid_param', { name => 'flag_id', value => $flag });
+ return $flag->flag_id;
+}
+
+###############################
+#### Setters ####
+###############################
+
+sub set_value { $_[0]->set('value', $_[1]); }
+
+###############################
+#### Accessors ####
+###############################
+
+sub tracking_flag_id { return $_[0]->{'tracking_flag_id'}; }
+sub bug_id { return $_[0]->{'bug_id'}; }
+sub value { return $_[0]->{'value'}; }
+
+sub bug {
+ return $_[0]->{'bug'} ||= Bugzilla::Bug->new({
+ id => $_[0]->bug_id, cache => 1
+ });
+}
+
+sub tracking_flag {
+ return $_[0]->{'tracking_flag'} ||= Bugzilla::Extension::TrackingFlags::Flag->new({
+ id => $_[0]->tracking_flag_id, cache => 1
+ });
+}
+
+1;
diff --git a/extensions/TrackingFlags/lib/Flag/Value.pm b/extensions/TrackingFlags/lib/Flag/Value.pm
new file mode 100644
index 000000000..4023e191d
--- /dev/null
+++ b/extensions/TrackingFlags/lib/Flag/Value.pm
@@ -0,0 +1,130 @@
+# 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::TrackingFlags::Flag::Value;
+
+use base qw(Bugzilla::Object);
+
+use strict;
+use warnings;
+
+use Bugzilla::Error;
+use Bugzilla::Group;
+use Bugzilla::Util qw(detaint_natural);
+use Scalar::Util qw(blessed);
+
+###############################
+#### Initialization ####
+###############################
+
+use constant DB_TABLE => 'tracking_flags_values';
+
+use constant DB_COLUMNS => qw(
+ id
+ tracking_flag_id
+ setter_group_id
+ value
+ sortkey
+ is_active
+);
+
+use constant LIST_ORDER => 'sortkey';
+
+use constant UPDATE_COLUMNS => qw(
+ setter_group_id
+ value
+ sortkey
+ is_active
+);
+
+use constant VALIDATORS => {
+ tracking_flag_id => \&_check_tracking_flag,
+ setter_group_id => \&_check_setter_group,
+ value => \&_check_value,
+ sortkey => \&_check_sortkey,
+ is_active => \&Bugzilla::Object::check_boolean,
+};
+
+###############################
+#### Validators ####
+###############################
+
+sub _check_value {
+ my ($invocant, $value) = @_;
+ defined $value || ThrowCodeError('param_required', { param => 'value' });
+ return $value;
+}
+
+sub _check_tracking_flag {
+ my ($invocant, $flag) = @_;
+ if (blessed $flag) {
+ return $flag->flag_id;
+ }
+ $flag = Bugzilla::Extension::TrackingFlags::Flag->new({ id => $flag, cache => 1 })
+ || ThrowCodeError('tracking_flags_invalid_param', { name => 'flag_id', value => $flag });
+ return $flag->flag_id;
+}
+
+sub _check_setter_group {
+ my ($invocant, $group) = @_;
+ if (blessed $group) {
+ return $group->id;
+ }
+ $group = Bugzilla::Group->new({ id => $group, cache => 1 })
+ || ThrowCodeError('tracking_flags_invalid_param', { name => 'setter_group_id', value => $group });
+ return $group->id;
+}
+
+sub _check_sortkey {
+ my ($invocant, $sortkey) = @_;
+ detaint_natural($sortkey)
+ || ThrowUserError('field_invalid_sortkey', { sortkey => $sortkey });
+ return $sortkey;
+}
+
+###############################
+#### Setters ####
+###############################
+
+sub set_setter_group_id { $_[0]->set('setter_group_id', $_[1]); }
+sub set_value { $_[0]->set('value', $_[1]); }
+sub set_sortkey { $_[0]->set('sortkey', $_[1]); }
+sub set_is_active { $_[0]->set('is_active', $_[1]); }
+
+###############################
+#### Accessors ####
+###############################
+
+sub tracking_flag_id { return $_[0]->{'tracking_flag_id'}; }
+sub setter_group_id { return $_[0]->{'setter_group_id'}; }
+sub value { return $_[0]->{'value'}; }
+sub sortkey { return $_[0]->{'sortkey'}; }
+sub is_active { return $_[0]->{'is_active'}; }
+
+sub tracking_flag {
+ return $_[0]->{'tracking_flag'} ||= Bugzilla::Extension::TrackingFlags::Flag->new({
+ id => $_[0]->tracking_flag_id, cache => 1
+ });
+}
+
+sub setter_group {
+ if ($_[0]->setter_group_id) {
+ $_[0]->{'setter_group'} ||= Bugzilla::Group->new({
+ id => $_[0]->setter_group_id, cache => 1
+ });
+ }
+ return $_[0]->{'setter_group'};
+}
+
+########################################
+## Compatibility with Bugzilla::Field ##
+########################################
+
+sub name { return $_[0]->{'value'}; }
+sub is_visible_on_bug { return 1; }
+
+1;
diff --git a/extensions/TrackingFlags/lib/Flag/Visibility.pm b/extensions/TrackingFlags/lib/Flag/Visibility.pm
new file mode 100644
index 000000000..7600d71bd
--- /dev/null
+++ b/extensions/TrackingFlags/lib/Flag/Visibility.pm
@@ -0,0 +1,172 @@
+# 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::TrackingFlags::Flag::Visibility;
+
+use base qw(Bugzilla::Object);
+
+use strict;
+use warnings;
+
+use Bugzilla::Error;
+use Bugzilla::Product;
+use Bugzilla::Component;
+use Scalar::Util qw(blessed);
+
+###############################
+#### Initialization ####
+###############################
+
+use constant DB_TABLE => 'tracking_flags_visibility';
+
+use constant DB_COLUMNS => qw(
+ id
+ tracking_flag_id
+ product_id
+ component_id
+);
+
+use constant LIST_ORDER => 'id';
+
+use constant UPDATE_COLUMNS => (); # imutable
+
+use constant VALIDATORS => {
+ tracking_flag_id => \&_check_tracking_flag,
+ product_id => \&_check_product,
+ component_id => \&_check_component,
+};
+
+###############################
+#### Methods ####
+###############################
+
+sub match {
+ my $class= shift;
+ my ($params) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ # Allow matching component and product by name
+ # (in addition to matching by ID).
+ # Borrowed from Bugzilla::Bug::match
+ my %translate_fields = (
+ product => 'Bugzilla::Product',
+ component => 'Bugzilla::Component',
+ );
+
+ foreach my $field (keys %translate_fields) {
+ my @ids;
+ # Convert names to ids. We use "exists" everywhere since people can
+ # legally specify "undef" to mean IS NULL
+ if (exists $params->{$field}) {
+ my $names = $params->{$field};
+ my $type = $translate_fields{$field};
+ my $objects = Bugzilla::Object::match($type, { name => $names });
+ push(@ids, map { $_->id } @$objects);
+ }
+ # You can also specify ids directly as arguments to this function,
+ # so include them in the list if they have been specified.
+ if (exists $params->{"${field}_id"}) {
+ my $current_ids = $params->{"${field}_id"};
+ my @id_array = ref $current_ids ? @$current_ids : ($current_ids);
+ push(@ids, @id_array);
+ }
+ # We do this "or" instead of a "scalar(@ids)" to handle the case
+ # when people passed only invalid object names. Otherwise we'd
+ # end up with a SUPER::match call with zero criteria (which dies).
+ if (exists $params->{$field} or exists $params->{"${field}_id"}) {
+ delete $params->{$field};
+ $params->{"${field}_id"} = scalar(@ids) == 1 ? [ $ids[0] ] : \@ids;
+ }
+ }
+
+ # If we aren't matching on the product, use the default matching code
+ if (!exists $params->{product_id}) {
+ return $class->SUPER::match(@_);
+ }
+
+ my @criteria = ("1=1");
+
+ if ($params->{product_id}) {
+ push(@criteria, $dbh->sql_in('product_id', $params->{'product_id'}));
+ if ($params->{component_id}) {
+ my $component_id = $params->{component_id};
+ push(@criteria, "(" . $dbh->sql_in('component_id', $params->{'component_id'}) .
+ " OR component_id IS NULL)");
+ }
+ }
+
+ my $where = join(' AND ', @criteria);
+ my $flag_ids = $dbh->selectcol_arrayref("SELECT id
+ FROM tracking_flags_visibility
+ WHERE $where");
+
+ return Bugzilla::Extension::TrackingFlags::Flag::Visibility->new_from_list($flag_ids);
+}
+
+###############################
+#### Validators ####
+###############################
+
+sub _check_tracking_flag {
+ my ($invocant, $flag) = @_;
+ if (blessed $flag) {
+ return $flag->flag_id;
+ }
+ $flag = Bugzilla::Extension::TrackingFlags::Flag->new($flag)
+ || ThrowCodeError('tracking_flags_invalid_param', { name => 'flag_id', value => $flag });
+ return $flag->flag_id;
+}
+
+sub _check_product {
+ my ($invocant, $product) = @_;
+ if (blessed $product) {
+ return $product->id;
+ }
+ $product = Bugzilla::Product->new($product)
+ || ThrowCodeError('tracking_flags_invalid_param', { name => 'product_id', value => $product });
+ return $product->id;
+}
+
+sub _check_component {
+ my ($invocant, $component) = @_;
+ return undef unless defined $component;
+ if (blessed $component) {
+ return $component->id;
+ }
+ $component = Bugzilla::Component->new($component)
+ || ThrowCodeError('tracking_flags_invalid_param', { name => 'component_id', value => $component });
+ return $component->id;
+}
+
+###############################
+#### Accessors ####
+###############################
+
+sub tracking_flag_id { return $_[0]->{'tracking_flag_id'}; }
+sub product_id { return $_[0]->{'product_id'}; }
+sub component_id { return $_[0]->{'component_id'}; }
+
+sub tracking_flag {
+ my ($self) = @_;
+ $self->{'tracking_flag'} ||= Bugzilla::Extension::TrackingFlags::Flag->new($self->tracking_flag_id);
+ return $self->{'tracking_flag'};
+}
+
+sub product {
+ my ($self) = @_;
+ $self->{'product'} ||= Bugzilla::Product->new($self->product_id);
+ return $self->{'product'};
+}
+
+sub component {
+ my ($self) = @_;
+ return undef unless $self->component_id;
+ $self->{'component'} ||= Bugzilla::Component->new($self->component_id);
+ return $self->{'component'};
+}
+
+1;
diff --git a/extensions/TrackingFlags/template/en/default/bug/tracking_flags.html.tmpl b/extensions/TrackingFlags/template/en/default/bug/tracking_flags.html.tmpl
new file mode 100644
index 000000000..b2b6efca7
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/bug/tracking_flags.html.tmpl
@@ -0,0 +1,57 @@
+[%# 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 flag = flag_list %]
+ [% SET bug_id = bug.defined ? bug.id : 0 %]
+ [% SET flag_bug_value = flag.bug_flag(bug_id).value %]
+ [% NEXT IF !new_bug && (!user.id && flag_bug_value == '---') %]
+ <tr id="row_[% flag.name FILTER html %]">
+ <td [% IF new_bug %]class="field_label"[% END %]>
+ <label for="[% flag.name FILTER html %]">
+ [% IF new_bug %]
+ <a
+ [% IF help_html.${flag.name}.defined %]
+ title="[% help_html.${flag.name} FILTER txt FILTER collapse FILTER html %]"
+ class="field_help_link"
+ [% END %]
+ href="page.cgi?id=fields.html#[% flag.name FILTER uri %]">
+ [% END %]
+ [% flag.description FILTER html %]
+ [% IF new_bug %]
+ </a>
+ [% END %]:</label>
+ </td>
+ <td>
+ [% IF user.id %]
+ <input type="hidden" id="[% flag.name FILTER html %]_dirty">
+ <select id="[% flag.name FILTER html %]"
+ name="[% flag.name FILTER html %]">
+ [% FOREACH value = flag.values %]
+ [% IF new_bug || 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 !new_bug && flag_bug_value == value.name %]>
+ [% value.name FILTER html %]</option>
+ [% END %]
+ </select>
+ <script type="text/javascript">
+ initHidingOptionsForIE('[% flag.name FILTER js %]');
+ </script>
+ [% IF !new_bug && user.id %]
+ <span id="ro_[% flag.name FILTER html %]" class="bz_default_hidden">
+ [% flag_bug_value FILTER html %]
+ </span>
+ [% END %]
+ [% ELSE %]
+ [% flag_bug_value FILTER html %]
+ [% END %]
+ </td>
+ </tr>
+[% END %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/admin/admin-end_links_right.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/admin/admin-end_links_right.html.tmpl
new file mode 100644
index 000000000..4808da069
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/admin/admin-end_links_right.html.tmpl
@@ -0,0 +1,18 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF user.in_group('admin') %]
+ <dt id="push">
+ <a href="page.cgi?id=tracking_flags_admin_list.html">Release Tracking Flags</a>
+ </dt>
+ <dd>
+ Tracking flags are special multi-value fields used to aid tracking releases
+ of Firefox, Firefox OS, Thunderbird, and other projects.
+ </dd>
+[% END %]
+
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/create/create-bug_flags_end.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-bug_flags_end.html.tmpl
new file mode 100644
index 000000000..2a90cbfe3
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-bug_flags_end.html.tmpl
@@ -0,0 +1,33 @@
+[%# 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.
+ #%]
+
+[% RETURN IF NOT tracking_flags.size %]
+
+[% FOREACH type = tracking_flag_types %]
+ [% NEXT IF type.name == 'tracking' || type.name == 'project' %]
+ [% flag_list = [] %]
+ [% FOREACH flag = tracking_flags %]
+ [% flag_list.push(flag) IF flag.flag_type == type.name %]
+ [% END %]
+ [% IF flag_list.size %]
+ <tr>
+ <td>
+ <table class="tracking_flags">
+ <tr>
+ <th>
+ [% type.description FILTER html %]:
+ </th>
+ </tr>
+ [% INCLUDE bug/tracking_flags.html.tmpl
+ flag_list = flag_list
+ new_bug = 1 %]
+ </table>
+ </td>
+ </tr>
+ [% END %]
+[% END %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/create/create-form.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-form.html.tmpl
new file mode 100644
index 000000000..59fe1d0ec
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-form.html.tmpl
@@ -0,0 +1,63 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF tracking_flags.size %]
+ [% tracking_flag_names = [] %]
+ [% FOREACH flag = tracking_flags %]
+ [% tracking_flag_names.push(flag.name) %]
+ [% END %]
+
+ <script type="text/javascript">
+ [% js_filtered_names = [] %]
+ [% FOREACH flag = tracking_flag_names %]
+ [% js_filtered = flag FILTER js %]
+ [% js_filtered_names.push(js_filtered) %]
+ [% END %]
+ var tracking_flag_names = ['[% js_filtered_names.join("','") FILTER none %]'];
+ var tracking_flags = new Array([% product.components.size %]);
+
+ [% count = 0 %]
+ [% FOREACH c = product.components %]
+ [% NEXT IF NOT c.is_active %]
+ [% tracking_flag_list = [] %]
+ [% FOREACH flag = tracking_flags %]
+ [% FOREACH v = flag.visibility %]
+ [% IF v.product_id == product.id
+ && (!v.component_id.defined || v.component_id == c.id) %]
+ [% tracking_flag_list.push(flag.name) %]
+ [% END %]
+ [% END %]
+ [% END %]
+ [% js_filtered_flags = [] %]
+ [% FOREACH flag = tracking_flag_list %]
+ [% js_filtered = flag FILTER js %]
+ [% js_filtered_flags.push(js_filtered) %]
+ [% END %]
+ tracking_flags[[% count %]] = ['[% js_filtered_flags.join("','") FILTER none %]'];
+ [% count = count + 1 %]
+ [% END %]
+
+ function update_tracking_flags () {
+ var component = document.getElementById('component');
+ // First, we disable all flags.
+ for (var i = 0; i < tracking_flag_names.length; i++) {
+ var flagField = document.getElementById(tracking_flag_names[i]);
+ flagField.disabled = true;
+ }
+ // Now enable flags available for the selected component.
+ var index = component.selectedIndex;
+ for (var i = 0; i < tracking_flags[index].length; i++) {
+ var flagField = document.getElementById(tracking_flags[index][i]);
+ flagField.disabled = false;
+ }
+ }
+
+ YAHOO.util.Event.onDOMReady(update_tracking_flags);
+ YAHOO.util.Event.addListener("component", "change", update_tracking_flags);
+ </script>
+[% END %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/create/create-project_flags_end.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-project_flags_end.html.tmpl
new file mode 100644
index 000000000..662bc26ee
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-project_flags_end.html.tmpl
@@ -0,0 +1,18 @@
+[%# 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.
+ #%]
+
+[% RETURN IF NOT tracking_flags.size %]
+
+[% flag_list = [] %]
+[% FOREACH flag = tracking_flags %]
+ [% NEXT IF flag.flag_type != 'project' %]
+ [% flag_list.push(flag) %]
+[% END %]
+[% INCLUDE bug/tracking_flags.html.tmpl
+ flag_list = flag_list
+ new_bug = 1 %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/create/create-tracking_flags_end.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-tracking_flags_end.html.tmpl
new file mode 100644
index 000000000..69827a87a
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-tracking_flags_end.html.tmpl
@@ -0,0 +1,18 @@
+[%# 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.
+ #%]
+
+[% RETURN IF NOT tracking_flags.size %]
+
+[% flag_list = [] %]
+[% FOREACH flag = tracking_flags %]
+ [% NEXT IF flag.flag_type != 'tracking' %]
+ [% flag_list.push(flag) %]
+[% END %]
+[% INCLUDE bug/tracking_flags.html.tmpl
+ flag_list = flag_list
+ new_bug = 1 %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-bug_flags_end.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-bug_flags_end.html.tmpl
new file mode 100644
index 000000000..2a90cbfe3
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-bug_flags_end.html.tmpl
@@ -0,0 +1,33 @@
+[%# 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.
+ #%]
+
+[% RETURN IF NOT tracking_flags.size %]
+
+[% FOREACH type = tracking_flag_types %]
+ [% NEXT IF type.name == 'tracking' || type.name == 'project' %]
+ [% flag_list = [] %]
+ [% FOREACH flag = tracking_flags %]
+ [% flag_list.push(flag) IF flag.flag_type == type.name %]
+ [% END %]
+ [% IF flag_list.size %]
+ <tr>
+ <td>
+ <table class="tracking_flags">
+ <tr>
+ <th>
+ [% type.description FILTER html %]:
+ </th>
+ </tr>
+ [% INCLUDE bug/tracking_flags.html.tmpl
+ flag_list = flag_list
+ new_bug = 1 %]
+ </table>
+ </td>
+ </tr>
+ [% END %]
+[% END %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-project_flags_end.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-project_flags_end.html.tmpl
new file mode 100644
index 000000000..662bc26ee
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-project_flags_end.html.tmpl
@@ -0,0 +1,18 @@
+[%# 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.
+ #%]
+
+[% RETURN IF NOT tracking_flags.size %]
+
+[% flag_list = [] %]
+[% FOREACH flag = tracking_flags %]
+ [% NEXT IF flag.flag_type != 'project' %]
+ [% flag_list.push(flag) %]
+[% END %]
+[% INCLUDE bug/tracking_flags.html.tmpl
+ flag_list = flag_list
+ new_bug = 1 %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-tracking_flags_end.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-tracking_flags_end.html.tmpl
new file mode 100644
index 000000000..69827a87a
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-tracking_flags_end.html.tmpl
@@ -0,0 +1,18 @@
+[%# 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.
+ #%]
+
+[% RETURN IF NOT tracking_flags.size %]
+
+[% flag_list = [] %]
+[% FOREACH flag = tracking_flags %]
+ [% NEXT IF flag.flag_type != 'tracking' %]
+ [% flag_list.push(flag) %]
+[% END %]
+[% INCLUDE bug/tracking_flags.html.tmpl
+ flag_list = flag_list
+ new_bug = 1 %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
new file mode 100644
index 000000000..697db75ce
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
@@ -0,0 +1,191 @@
+[%# 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.
+ #%]
+
+[%# Old style custom field based tracking flags %]
+[% old_tracking_flags = [] %]
+[% old_project_flags = [] %]
+[% FOREACH field = Bugzilla.active_custom_fields(product=>bug.product_obj,component=>bug.component_obj,type=>2) %]
+ [% NEXT IF field.type == constants.FIELD_TYPE_EXTENSION %]
+ [% NEXT IF NOT user.id AND bug.${field.name} == "---" %]
+ [% NEXT IF cf_flag_disabled(field.name, bug) %]
+ [% IF cf_is_project_flag(field.name) %]
+ [% old_project_flags.push(field) %]
+ [% ELSE %]
+ [% old_tracking_flags.push(field) %]
+ [% END %]
+[% END %]
+
+[%# Add in the new tracking flags that are type tracking or project %]
+[% new_tracking_flags = [] %]
+[% new_project_flags = [] %]
+[% IF tracking_flags.size %]
+ [% FOREACH flag = tracking_flags %]
+ [% IF flag.flag_type == 'tracking' %]
+ [% new_tracking_flags.push(flag) %]
+ [% END %]
+ [% IF flag.flag_type == 'project' %]
+ [% new_project_flags.push(flag) %]
+ [% END %]
+ [% END %]
+[% END %]
+
+[% IF old_project_flags.size || new_project_flags.size %]
+ <tr>
+ <td class="field_label">
+ <label>Project Flags:</label>
+ </td>
+ <td>
+ [% IF bug.check_can_change_field('flagtypes.name', 0, 1) %]
+ <table class="tracking_flags">
+ [% FOREACH field = old_project_flags %]
+ [% NEXT IF NOT user.id AND field.value == "---" %]
+ <tr id="row_[% field.name FILTER js %]">
+ <td>
+ <label for="[% field.name FILTER html %]">
+ [% field_descs.${field.name} FILTER html %]:
+ </label>
+ </td>
+ <td>
+ [% PROCESS bug/field.html.tmpl value = bug.${field.name}
+ editable = user.id
+ no_tds = 1 %]
+ [% IF user.id %]
+ <span id="ro_[% field.name FILTER html %]" class="bz_default_hidden">
+ [% bug.${field.name} FILTER html %]
+ </span>
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+ [% INCLUDE bug/tracking_flags.html.tmpl
+ flag_list = new_project_flags %]
+ </table>
+ [% ELSE %]
+ [% FOREACH field = old_project_flags %]
+ [% NEXT IF bug.${field.name} == "---" %]
+ [% field_descs.${field.name} FILTER html %]: [% bug.${field.name} FILTER html %]<br>
+ [% END %]
+ [% FOREACH flag = project_flags %]
+ [% NEXT IF flag.bug_flag.value == '---' %]
+ [% flag.description FILTER html %]: [% flag.bug_flag.value FILTER html %]<br>
+ [% END %]
+ [% END %]
+ </td>
+ </tr>
+[% END %]
+
+[% IF old_tracking_flags.size || new_tracking_flags.size %]
+ <tr>
+ <td class="field_label">
+ <label>Tracking Flags:</label>
+ </td>
+ <td>
+ [% IF bug.check_can_change_field('flagtypes.name', 0, 1) %]
+ [% IF user.id %]
+ <span id="edit_tracking_flags_action">
+ (<a href="#" name="tracking" class="edit_tracking_flags_link">edit</a>)
+ </span>
+ [% END %]
+ <table class="tracking_flags">
+ [% FOREACH field = old_tracking_flags %]
+ [% NEXT IF NOT user.id AND field.value == "---" %]
+ <tr id="row_[% field.name FILTER js %]">
+ <td>
+ <label for="[% field.name FILTER html %]">
+ [% field_descs.${field.name} FILTER html %]:
+ </label>
+ </td>
+ <td>
+ [% PROCESS bug/field.html.tmpl
+ value = bug.${field.name}
+ editable = user.id
+ no_tds = 1
+ %]
+ [% IF user.id %]
+ <span id="ro_[% field.name FILTER html %]" class="bz_default_hidden">
+ [% bug.${field.name} FILTER html %]
+ </span>
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+ [% INCLUDE bug/tracking_flags.html.tmpl
+ flag_list = new_tracking_flags %]
+ </table>
+ [% ELSE %]
+ [% FOREACH field = old_tracking_flags %]
+ [% NEXT IF bug.${field.name} == "---" %]
+ [% field_descs.${field.name} FILTER html %]: [% bug.${field.name} FILTER html %]<br>
+ [% END %]
+ [% FOREACH flag = new_tracking_flags %]
+ [% NEXT IF flag.status == '---' %]
+ [% flag.description FILTER html %]: [% flag.bug_flag.value FILTER html %]<br>
+ [% END %]
+ [% END %]
+ </td>
+ </tr>
+ <script type="text/javascript">
+ TrackingFlags.flags['tracking'] = {};
+ [% FOREACH field = old_tracking_flags %]
+ TrackingFlags.flags['tracking']['[% field.name FILTER js %]'] = '[% bug.${field.name} FILTER js %]';
+ [% END %]
+ [% FOREACH flag = new_tracking_flags %]
+ TrackingFlags.flags['tracking']['[% flag.name FILTER js %]'] = '[% flag.bug_flag.value FILTER js %]';
+ [% END %]
+ TrackingFlags.types.push('tracking');
+ </script>
+[% END %]
+
+[%# Last, display any new style flags that are not type tracking or project %]
+[% IF tracking_flags.size %]
+ [% FOREACH type = tracking_flag_types %]
+ [% NEXT IF type.name == 'tracking' || type.name == 'project' %]
+ [% flag_list = [] %]
+ [% FOREACH flag = tracking_flags %]
+ [% flag_list.push(flag) IF flag.flag_type == type.name %]
+ [% END %]
+ [% IF flag_list.size %]
+ <tr>
+ <td class="field_label">
+ <label>[% type.description FILTER html %]:</label>
+ </td>
+ <td>
+ [% IF bug.check_can_change_field('flagtypes.name', 0, 1) %]
+ [% IF user.id && type.collapsed %]
+ <span id="edit_[% type.name FILTER html %]_flags_action">
+ (<a href="#" name="[% type.name FILTER html %]" class="edit_tracking_flags_link">edit</a>)
+ </span>
+ [% END %]
+ <table class="tracking_flags">
+ [% INCLUDE bug/tracking_flags.html.tmpl
+ flag_list = flag_list %]
+ </table>
+ [% IF type.collapsed %]
+ <script type="text/javascript">
+ TrackingFlags.flags['[% type.name FILTER js %]'] = {};
+ [% FOREACH flag = flag_list %]
+ TrackingFlags.flags['[% type.name FILTER js %]']['[% flag.name FILTER js %]'] = '[% flag.bug_flag.value FILTER js %]';
+ [% END %]
+ TrackingFlags.types.push('[% type.name FILTER js %]');
+ </script>
+ [% END %]
+ [% ELSE %]
+ [% FOREACH flag = flag_list %]
+ [% NEXT IF flag.status == '---' %]
+ [% flag.description FILTER html %]: [% flag.bug_flag.value FILTER html %]<br>
+ [% END %]
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+ [% END %]
+[% END %]
+
+<script type="text/javascript">
+ hide_tracking_flags();
+</script>
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/field-editable.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/field-editable.html.tmpl
new file mode 100644
index 000000000..f598609e8
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/field-editable.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.
+ #%]
+
+<input type="hidden" id="[% field.name FILTER html %]_dirty">
+<select id="[% field.name FILTER html %]"
+ name="[% field.name FILTER html %]">
+ [% IF allow_dont_change %]
+ <option value="[% dontchange FILTER html %]"
+ [% ' selected="selected"' IF value == dontchange %]>
+ [% dontchange FILTER html %]
+ </option>
+ [% END %]
+ [% FOREACH legal_value = field.values %]
+ [% IF legal_value.name != value %]
+ [% NEXT IF !field.can_set_value(legal_value.name) %]
+ [% NEXT IF !legal_value.is_active %]
+ [% END %]
+ <option value="[% legal_value.name FILTER html %]"
+ id="v[% legal_value.id FILTER html %] [%- field.name FILTER html %]"
+ [% IF legal_value.name == value %]
+ selected="selected"
+ [% END %]>
+ [%- display_value(field.name, legal_value.name) FILTER html ~%]
+ </option>
+ [% END %]
+</select>
+<script type="text/javascript">
+<!--
+ initHidingOptionsForIE('[% field.name FILTER js %]');
+ [%+ INCLUDE "bug/field-events.js.tmpl"
+ field = field, product = bug.product_obj %]
+//-->
+</script>
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/field-non_editable.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/field-non_editable.html.tmpl
new file mode 100644
index 000000000..8fa1f1623
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/field-non_editable.html.tmpl
@@ -0,0 +1,9 @@
+[%# 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.
+ #%]
+
+[% display_value(field.name, value) FILTER html %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/show-header-end.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/show-header-end.html.tmpl
new file mode 100644
index 000000000..5e4ef2fcb
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/show-header-end.html.tmpl
@@ -0,0 +1,10 @@
+[%# 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.
+ #%]
+
+[% javascript_urls.push('extensions/TrackingFlags/web/js/tracking_flags.js') %]
+[% style_urls.push('extensions/TrackingFlags/web/styles/edit_bug.css') %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/global/code-error-errors.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/global/code-error-errors.html.tmpl
new file mode 100644
index 000000000..2d462ebaf
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/global/code-error-errors.html.tmpl
@@ -0,0 +1,26 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "tracking_flags_invalid_product" %]
+ [% title = "Invalid Product" %]
+ The product named '[% product FILTER html %]' does not exist.
+
+[% ELSIF error == "tracking_flags_invalid_component" %]
+ [% title = "Invalid Component" %]
+ The component named '[% component FILTER html %]' does not exist.
+
+[% ELSIF error == "tracking_flags_invalid_item_id" %]
+ [% title = "Invalid " _ item _ " ID" %]
+ Invalid [% item FILTER html %] ID ([% id FILTER html %]).
+
+[% ELSIF error == "tracking_flags_invalid_param" %]
+ [% title = "Invalid Parameter Provided" %]
+ An invalid parameter '[% value FILTER html %]'
+ for '[% name FILTER html %]' was provided.
+
+[% END %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/global/messages-messages.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/global/messages-messages.html.tmpl
new file mode 100644
index 000000000..45bb7332c
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/global/messages-messages.html.tmpl
@@ -0,0 +1,15 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF message_tag == 'tracking_flags_created' %]
+ The tracking flag has been created.
+
+[% ELSIF message_tag == 'tracking_flags_updated' %]
+ The tracking flag has been updated.
+
+[% END %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..f17df1bdf
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,57 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "tracking_flags_change_denied" %]
+ [% title = "Tracking Flag Modification Denied" %]
+ You tried to update the status of the tracking flag '[% flag.name FILTER html %]'
+ [% IF value %] to '[% value.name FILTER html %]'[% END %].
+ Only a user with the required permissions may make this change.
+
+[% ELSIF error == "tracking_flags_missing_mandatory" %]
+ [% IF fields.size == 1 %]
+ [% title = "Missing mandatory field" %]
+ The field "[% fields.first FILTER html %]" is mandatory, and must be provided.
+ [% ELSE %]
+ [% title = "Missing mandatory fields" %]
+ The following fields are mandatory, and must be provided:
+ [%+ fields.join(', ') FILTER html %]
+ [% END %]
+
+[% ELSIF error == "tracking_flags_cf_prefix" %]
+ [% title = "Invalid flag name" %]
+ The flag name must start with 'cf_'.
+
+[% ELSIF error == "tracking_flags_duplicate_field" %]
+ [% title = "Duplicate field" %]
+ A tracking flag field named "[% name FILTER html %]" already exists.
+
+[% ELSIF error == "tracking_flags_missing_values" %]
+ [% title = "Missing values" %]
+ You must provide at least one value.
+
+[% ELSIF error == "tracking_flags_missing_value" %]
+ [% title = "Missing value" %]
+ You must provied the value for all values.
+
+[% ELSIF error == "tracking_flags_duplicate_value" %]
+ [% title = "Duplicate value" %]
+ The value "[% value FILTER html %]" has been provided more than once.
+
+[% ELSIF error == "tracking_flags_missing_visibility" %]
+ [% title = "Missing visibility" %]
+ You must provide at least one product for visibility.
+
+[% ELSIF error == "tracking_flags_duplicate_visibility" %]
+ [% title = "Duplicate visibility" %]
+ The visibility '[% name FILTER html %]' has been provided more than once.
+
+[% ELSIF error == "tracking_flags_invalid_flag_type" %]
+ [% title = "Invalid flag type" %]
+ The flag type '[% type FILTER html %]' is invalid.
+
+[% END %]
diff --git a/extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_edit.html.tmpl b/extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_edit.html.tmpl
new file mode 100644
index 000000000..d9f11fa2a
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_edit.html.tmpl
@@ -0,0 +1,194 @@
+[%# 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.
+ #%]
+
+[% js_data = BLOCK %]
+var useclassification = false;
+var first_load = true;
+var last_sel = [];
+var cpts = new Array();
+[% n = 1 %]
+[% FOREACH p = user.get_selectable_products %]
+ cpts['[% n FILTER js %]'] = [
+ [%- FOREACH c = p.components %]'[% c.name FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ];
+ [% n = n+1 %]
+[% END %]
+var selected_components = [
+ [%- FOREACH c = input.component %]'[% c FILTER js %]'
+ [%- ',' UNLESS loop.last %] [%- END ~%] ];
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Release Tracking Flags"
+ javascript = js_data
+ javascript_urls = [ 'extensions/TrackingFlags/web/js/admin.js', 'js/productform.js' ]
+ style_urls = [ 'extensions/TrackingFlags/web/styles/admin.css' ]
+%]
+
+[% IF mode == 'deleted' %]
+
+ <p>The tracking flag [% flag_name FILTER html %] has been deleted.</p>
+ <hr>
+ <p>Return to the <a href="page.cgi?id=tracking_flags_admin_list.html">list of Tracking Flags</a>.</p>
+
+ [% RETURN %]
+
+[% END %]
+
+<script>
+ var groups = [% groups || '[]' FILTER none %];
+ var flag_values = [% values || '[]' FILTER none %];
+ var flag_visibility = [% visibility || '[]' FILTER none %];
+</script>
+
+<div id="edit_mode">
+ [% IF mode == 'edit' %]
+ Editing <b>[% flag.name FILTER html %]</b>.
+ [% ELSE %]
+ New flag
+ [% END %]
+</div>
+
+<form method="POST" action="page.cgi" onsubmit="return on_submit()">
+<input type="hidden" name="id" value="tracking_flags_admin_edit.html">
+<input type="hidden" name="mode" value="[% mode FILTER html %]">
+<input type="hidden" name="flag_id" value="[% mode == 'edit' ? flag.flag_id : 0 FILTER html %]">
+<input type="hidden" name="values" id="values" value="">
+<input type="hidden" name="visibility" id="visibility" value="">
+<input type="hidden" name="save" value="1">
+
+[%# name/desc/etc %]
+
+<table class="edit" cellspacing="0">
+
+<tr class="header">
+ <th colspan="3">Flag</th>
+</tr>
+
+<tr>
+ <th>Name</th>
+ <td><input name="flag_name" id="flag_name" value="[% flag.name FILTER html %]"></td>
+ <td class="help">database field name</td>
+</tr>
+
+<tr>
+ <th>Description</th>
+ <td><input name="flag_desc" id="flag_desc" value="[% flag.description FILTER html %]"></td>
+ <td class="help">visible name</td>
+</tr>
+
+<tr>
+ <th>Type</th>
+ <td>
+ <select name="flag_type" id="flag_type">
+ <option value=""></option>
+ [% FOREACH type = tracking_flag_types %]
+ <option value="[% type.name FILTER html %]"
+ [% 'selected="selected"' IF flag.flag_type == type.name %]>
+ [% type.name FILTER html %]</option>
+ [% END %]
+ </select>
+ </td>
+ <td class="help">flag type used for grouping</td>
+</tr>
+
+<tr>
+ <th>Sort Key</th>
+ <td>
+ <input name="flag_sort" id="flag_sort" value="[% flag.sortkey FILTER html %]">
+ [
+ <a class="txt_icon" href="#" onclick="inc_field('flag_sort', 5);return false">+5</a>
+ | <a class="txt_icon" href="#" onclick="inc_field('flag_sort', -5);return false">-5</a>
+ ]
+ </td>
+</tr>
+
+<tr>
+ <th>Active</th>
+ <td><input type="checkbox" name="flag_active" id="flag_active" value="1" [% "checked" IF flag.is_active %]></td>
+</tr>
+
+</table>
+
+[%# values %]
+
+<table id="flag_values" class="edit" cellspacing="0">
+
+<tr class="header">
+ <th colspan="4">Values</th>
+</tr>
+
+<tr>
+ <th>Value</th>
+ <th>Setter</th>
+ <th>Active</th>
+</tr>
+
+<tr>
+ <td colspan="4">
+ [ <a href="#" onclick="add_value();return false">New Value</a> ]
+ </td>
+</tr>
+
+</table>
+
+[%# visibility %]
+
+<table id="flag_visibility" class="edit" cellspacing="0">
+
+<tr class="header">
+ <th colspan="3">Visibility</th>
+</tr>
+
+<tr>
+ <th>Product</th>
+ <th>Component</th>
+</tr>
+
+<tr id="flag_visibility_add">
+ <td>
+ <select id="product" onChange="selectProduct(Dom.get('product'), Dom.get('component'), null, null, '-- Any --')">
+ <option value=""></option>
+ [% FOREACH p = user.get_selectable_products %]
+ <option value="[% p.name FILTER html %]"
+ [% " selected" IF input.product == p.name %]>
+ [% p.name FILTER html %]
+ </option>
+ [% END %]
+ </select>
+ </td>
+ <td>
+ <select id="component">
+ </select>
+ </td>
+ <td>
+ [ <a href="#" onclick="add_visibility();return false">Add</a> ]
+ <td>
+</tr>
+
+</table>
+
+
+[%# submit %]
+
+<div>
+ <input type="submit" name="submit" id="submit" value="[% mode == 'edit' ? 'Save Changes' : 'Add' %]">
+ [% IF can_delete %]
+ <input type="hidden" name="delete" id="delete" value="">
+ <input type="submit" value="Delete"
+ onclick="return delete_confirm('[% flag.name FILTER js FILTER html %]')">
+ [% END %]
+</div>
+
+</form>
+
+<hr>
+<p>
+Return to the <a href="page.cgi?id=tracking_flags_admin_list.html">list of Tracking Flags</a>.
+</p>
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_list.html.tmpl b/extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_list.html.tmpl
new file mode 100644
index 000000000..6c2ecc915
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_list.html.tmpl
@@ -0,0 +1,56 @@
+[%# 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/header.html.tmpl
+ title = "Release Tracking Flags"
+ style_urls = [ 'extensions/TrackingFlags/web/styles/admin.css' ]
+%]
+
+<table class="list" cellspacing="0">
+
+<tr>
+ <th>Name</th>
+ <th>Description</th>
+ <th>Type</th>
+ <th>Sort Key</th>
+ <th>Active</th>
+ <th>&nbsp;</th>
+</tr>
+
+[% FOREACH flag = flags %]
+ <tr class="[% loop.count % 2 == 1 ? "odd_row" : "even_row" %]">
+ <td [% 'class="disabled"' UNLESS flag.is_active %]>
+ <a href="page.cgi?id=tracking_flags_admin_edit.html&amp;mode=edit&amp;flag_id=[% flag.flag_id FILTER uri %]">
+ [% flag.name FILTER html %]
+ </a>
+ </td>
+ <td [% 'class="disabled"' UNLESS flag.is_active %]>
+ [% flag.description FILTER html %]
+ </td>
+ <td [% 'class="disabled"' UNLESS flag.is_active %]>
+ [% flag.flag_type FILTER html %]
+ </td>
+ <td [% 'class="disabled"' UNLESS flag.is_active %]>
+ [% flag.sortkey FILTER html %]
+ </td>
+ <td>
+ [% flag.is_active ? "Yes" : "No" %]
+ </td>
+ <td>
+ <a href="page.cgi?id=tracking_flags_admin_edit.html&amp;mode=copy&amp;copy_from=[% flag.flag_id FILTER uri %]">Copy</a>
+ </td>
+ </tr>
+[% END %]
+
+</table>
+
+<div id="new_flag">
+<a href="page.cgi?id=tracking_flags_admin_edit.html">Add Flag</a>
+</div>
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/TrackingFlags/web/js/admin.js b/extensions/TrackingFlags/web/js/admin.js
new file mode 100644
index 000000000..461ef7c63
--- /dev/null
+++ b/extensions/TrackingFlags/web/js/admin.js
@@ -0,0 +1,384 @@
+/* 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. */
+
+// init
+
+var Dom = YAHOO.util.Dom;
+var Event = YAHOO.util.Event;
+
+Event.onDOMReady(function() {
+ try {
+ if (!JSON)
+ JSON = YAHOO.lang.JSON;
+
+ Event.addListener('flag_name', 'change', change_flag_name, Dom.get('flag_name'));
+ Event.addListener('flag_desc', 'change', change_string_value, Dom.get('flag_desc'));
+ Event.addListener('flag_type', 'change', change_select_value, Dom.get('flag_type'));
+ Event.addListener('flag_sort', 'change', change_int_value, Dom.get('flag_sort'));
+
+ Event.addListener('product', 'change', function() {
+ if (Dom.get('product').value == '')
+ Dom.get('component').options.length = 0;
+ });
+
+ update_flag_values();
+ update_flag_visibility();
+ tag_missing_values();
+ } catch(e) {
+ console.error(e);
+ }
+});
+
+// field
+
+function change_flag_name(e, o) {
+ change_string_value(e, o);
+ if (o.value == '')
+ return;
+ o.value = o.value.replace(/[^a-z0-9_]/g, '_');
+ if (!o.value.match(/^cf_/))
+ o.value = 'cf_' + o.value;
+ if (Dom.get('flag_desc').value == '') {
+ var desc = o.value;
+ desc = desc.replace(/^cf_/, '');
+ desc = desc.replace(/_/g, '-');
+ Dom.get('flag_desc').value = desc;
+ tag_missing_value(Dom.get('flag_desc'));
+ }
+}
+
+function inc_field(id, amount) {
+ var el = Dom.get(id);
+ el.value = el.value.match(/-?\d+/) * 1 + amount;
+ change_int_value(null, el);
+}
+
+// values
+
+function update_flag_values() {
+ // update the values table from the flag_values global
+
+ var tbl = Dom.get('flag_values');
+ if (!tbl)
+ return;
+
+ // remove current entries
+ while (tbl.rows.length > 3) {
+ tbl.deleteRow(2);
+ }
+
+ // add all entries
+
+ for (var i = 0, l = flag_values.length; i < l; i++) {
+ var value = flag_values[i];
+
+ var row = tbl.insertRow(2 + i);
+ var cell;
+
+ // value
+ cell = row.insertCell(0);
+ if (value.value == '---') {
+ cell.innerHTML = '---';
+ } else {
+ var inputEl = document.createElement('input');
+ inputEl.id = 'value_' + i;
+ inputEl.type = 'text';
+ inputEl.className = 'option_value';
+ inputEl.value = value.value;
+ Event.addListener(inputEl, 'change', change_string_value, inputEl);
+ Event.addListener(inputEl, 'change', function(e, o) {
+ flag_values[o.id.match(/\d+$/)].value = o.value;
+ tag_invalid_values();
+ }, inputEl);
+ Event.addListener(inputEl, 'keyup', function(e, o) {
+ if ((e.key || e.keyCode) == 27 && o.value == '')
+ remove_value(o.id.match(/\d+$/));
+ }, inputEl);
+ cell.appendChild(inputEl);
+ }
+
+ // setter
+ cell = row.insertCell(1);
+ var selectEl = document.createElement('select');
+ selectEl.id = 'setter_' + i;
+ Event.addListener(selectEl, 'change', change_select_value, selectEl);
+ var optionEl = document.createElement('option');
+ optionEl.value = '';
+ selectEl.appendChild(optionEl);
+ for (var j = 0, m = groups.length; j < m; j++) {
+ var group = groups[j];
+ optionEl = document.createElement('option');
+ optionEl.value = group.id;
+ optionEl.innerHTML = YAHOO.lang.escapeHTML(group.name);
+ optionEl.selected = group.id == value.setter_group_id;
+ selectEl.appendChild(optionEl);
+ }
+ Event.addListener(selectEl, 'change', function(e, o) {
+ flag_values[o.id.match(/\d+$/)].setter_group_id = o.value;
+ tag_invalid_values();
+ }, selectEl);
+ cell.appendChild(selectEl);
+
+ // active
+ cell = row.insertCell(2);
+ if (value.value == '---') {
+ cell.innerHTML = 'Yes';
+ } else {
+ var inputEl = document.createElement('input');
+ inputEl.type = 'checkbox';
+ inputEl.id = 'is_active_' + i;
+ inputEl.checked = value.is_active;
+ Event.addListener(inputEl, 'change', function(e, o) {
+ flag_values[o.id.match(/\d+$/)].is_active = o.checked;
+ }, inputEl);
+ cell.appendChild(inputEl);
+ }
+
+ // actions
+ cell = row.insertCell(3);
+ var html =
+ '[' +
+ (i == 0
+ ? '<span class="txt_icon">&nbsp;-&nbsp;</span>'
+ : '<a class="txt_icon" href="#" onclick="value_move_up(' + i + ');return false"> &Delta; </a>'
+ ) +
+ '|' +
+ ( i == l - 1
+ ? '<span class="txt_icon">&nbsp;-&nbsp;</span>'
+ : '<a class="txt_icon" href="#" onclick="value_move_down(' + i + ');return false"> &nabla; </a>'
+ );
+ if (value.value != '---')
+ html += '| <a href="#" onclick="remove_value(' + i + ');return false">Remove</a>';
+ html += ']';
+ cell.innerHTML = html;
+ }
+
+ tag_invalid_values();
+}
+
+function tag_invalid_values() {
+ // reset
+ for (var i = 0, l = flag_values.length; i < l; i++) {
+ Dom.removeClass('value_' + i, 'admin_error');
+ }
+
+ for (var i = 0, l = flag_values.length; i < l; i++) {
+ // missing
+ if (flag_values[i].value == '')
+ Dom.addClass('value_' + i, 'admin_error');
+ if (!flag_values[i].setter_group_id)
+ Dom.addClass('setter_' + i, 'admin_error');
+
+ // duplicate values
+ for (var j = i; j < l; j++) {
+ if (i != j && flag_values[i].value == flag_values[j].value) {
+ Dom.addClass('value_' + i, 'admin_error');
+ Dom.addClass('value_' + j, 'admin_error');
+ }
+ }
+ }
+}
+
+function value_move_up(idx) {
+ if (idx == 0)
+ return;
+ var tmp = flag_values[idx];
+ flag_values[idx] = flag_values[idx - 1];
+ flag_values[idx - 1] = tmp;
+ update_flag_values();
+}
+
+function value_move_down(idx) {
+ if (idx == flag_values.length - 1)
+ return;
+ var tmp = flag_values[idx];
+ flag_values[idx] = flag_values[idx + 1];
+ flag_values[idx + 1] = tmp;
+ update_flag_values();
+}
+
+function add_value() {
+ var value = new Object();
+ value.id = 0;
+ value.value = '';
+ value.setter_group_id = '';
+ value.is_active = true;
+ var idx = flag_values.length;
+ flag_values[idx] = value;
+ update_flag_values();
+ Dom.get('value_' + idx).focus();
+}
+
+function remove_value(idx) {
+ flag_values.splice(idx, 1);
+ update_flag_values();
+}
+
+function update_value(e, o) {
+ var i = o.value.match(/\d+/);
+ flag_values[i].value = o.value;
+}
+
+// visibility
+
+function update_flag_visibility() {
+ // update the visibility table from the flag_visibility global
+
+ var tbl = Dom.get('flag_visibility');
+ if (!tbl)
+ return;
+
+ // remove current entries
+ while (tbl.rows.length > 3) {
+ tbl.deleteRow(2);
+ }
+
+ // show something if there aren't any components
+
+ if (!flag_visibility.length) {
+ var row = tbl.insertRow(2);
+ var cell = row.insertCell(0);
+ cell.innerHTML = '<i class="admin_error_text">missing</i>';
+ }
+
+ // add all entries
+
+ for (var i = 0, l = flag_visibility.length; i < l; i++) {
+ var visibility = flag_visibility[i];
+
+ var row = tbl.insertRow(2 + i);
+ var cell;
+
+ // product
+ cell = row.insertCell(0);
+ cell.innerHTML = visibility.product;
+
+ // component
+ cell = row.insertCell(1);
+ cell.innerHTML = visibility.component
+ ? visibility.component
+ : '<i>-- Any --</i>';
+
+ // actions
+ cell = row.insertCell(2);
+ cell.innerHTML = '[ <a href="#" onclick="remove_visibility(' + i + ');return false">Remove</a> ]';
+ }
+}
+
+function add_visibility() {
+ // validation
+ var product = Dom.get('product').value;
+ var component = Dom.get('component').value;
+ if (!product) {
+ alert('Please select a product.');
+ return;
+ }
+
+ // don't allow duplicates
+ for (var i = 0, l = flag_visibility.length; i < l; i++) {
+ if (flag_visibility[i].product == product && flag_visibility[i].component == component) {
+ Dom.get('product').value = '';
+ Dom.get('component').options.length = 0;
+ return;
+ }
+ }
+
+ if (component == '') {
+ // if we're adding an "any" component, remove non-any components
+ for (var i = 0; i < flag_visibility.length; i++) {
+ var visibility = flag_visibility[i];
+ if (visibility.product == product) {
+ flag_visibility.splice(i, 1);
+ i--;
+ }
+ }
+ } else {
+ // don't add non-any components if an "any" component exists
+ for (var i = 0, l = flag_visibility.length; i < l; i++) {
+ var visibility = flag_visibility[i];
+ if (visibility.product == product && !visibility.component) {
+ return;
+ }
+ }
+ }
+
+ // add to model
+ var visibility = new Object();
+ visibility.id = 0;
+ visibility.product = product;
+ visibility.component = component;
+ flag_visibility[flag_visibility.length] = visibility;
+
+ // update ui
+ update_flag_visibility();
+ Dom.get('product').value = '';
+ Dom.get('component').options.length = 0;
+}
+
+function remove_visibility(idx) {
+ flag_visibility.splice(idx, 1);
+ update_flag_visibility();
+}
+
+// validation and submission
+
+function tag_missing_values() {
+ var els = document.getElementsByTagName('input');
+ for (var i = 0, l = els.length; i < l; i++) {
+ var el = els[i];
+ if (el.id.match(/^(flag|value)_/))
+ tag_missing_value(el);
+ }
+ tag_missing_value(Dom.get('flag_type'));
+}
+
+function tag_missing_value(el) {
+ el.value == ''
+ ? Dom.addClass(el, 'admin_error')
+ : Dom.removeClass(el, 'admin_error');
+}
+
+function delete_confirm(flag) {
+ if (confirm('Are you sure you want to delete the flag ' + flag + ' ?')) {
+ Dom.get('delete').value = 1;
+ return true;
+ } else {
+ return false;
+ }
+}
+
+function on_submit() {
+ if (Dom.get('delete') && Dom.get('delete').value)
+ return;
+ // let perl manage most validation errors, because they are clearly marked
+ // the exception is an empty visibility list, so catch that here as well
+ if (!flag_visibility.length) {
+ alert('You must provide at least one product for visibility.');
+ return false;
+ }
+
+ Dom.get('values').value = JSON.stringify(flag_values);
+ Dom.get('visibility').value = JSON.stringify(flag_visibility);
+ return true;
+}
+
+// utils
+
+function change_string_value(e, o) {
+ o.value = YAHOO.lang.trim(o.value);
+ tag_missing_value(o);
+}
+
+function change_int_value(e, o) {
+ o.value = o.value.match(/-?\d+/);
+ tag_missing_value(o);
+}
+
+function change_select_value(e, o) {
+ tag_missing_value(o);
+}
+
diff --git a/extensions/TrackingFlags/web/js/tracking_flags.js b/extensions/TrackingFlags/web/js/tracking_flags.js
new file mode 100644
index 000000000..135b93dba
--- /dev/null
+++ b/extensions/TrackingFlags/web/js/tracking_flags.js
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+var Dom = YAHOO.util.Dom;
+
+var TrackingFlags = {
+ flags: {},
+ types: []
+};
+
+function hide_tracking_flags() {
+ for (var i = 0, l = TrackingFlags.types.length; i < l; i++) {
+ var flag_type = TrackingFlags.types[i];
+ for (var field in TrackingFlags.flags[flag_type]) {
+ var el = Dom.get(field);
+ var value = el ? el.value : TrackingFlags.flags[flag_type][field];
+ if (el && (value != TrackingFlags.flags[flag_type][field])) {
+ show_tracking_flags(flag_type);
+ return;
+ }
+ if (value == '---') {
+ Dom.addClass('row_' + field, 'bz_default_hidden');
+ } else {
+ Dom.addClass(field, 'bz_default_hidden');
+ Dom.removeClass('ro_' + field, 'bz_default_hidden');
+ }
+ }
+ }
+}
+
+function show_tracking_flags(flag_type) {
+ Dom.addClass('edit_' + flag_type + '_flags_action', 'bz_default_hidden');
+ for (var field in TrackingFlags.flags[flag_type]) {
+ if (Dom.get(field).value == '---') {
+ Dom.removeClass('row_' + field, 'bz_default_hidden');
+ } else {
+ Dom.removeClass(field, 'bz_default_hidden');
+ Dom.addClass('ro_' + field, 'bz_default_hidden');
+ }
+ }
+}
+
+YAHOO.util.Event.onDOMReady(function() {
+ var edit_tracking_links = Dom.getElementsByClassName('edit_tracking_flags_link');
+ for (var i = 0, l = edit_tracking_links.length; i < l; i++) {
+ YAHOO.util.Event.addListener(edit_tracking_links[i], 'click', function(e) {
+ e.preventDefault();
+ show_tracking_flags(this.name);
+ });
+ }
+});
diff --git a/extensions/TrackingFlags/web/styles/admin.css b/extensions/TrackingFlags/web/styles/admin.css
new file mode 100644
index 000000000..374409ce6
--- /dev/null
+++ b/extensions/TrackingFlags/web/styles/admin.css
@@ -0,0 +1,107 @@
+/* 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. */
+
+/* list */
+
+.list {
+ border: 1px solid #888888;
+}
+
+.list td, .list th {
+ padding: 3px 10px 3px 3px;
+ border: 1px solid #888888;
+}
+
+.list .odd_row {
+ background-color: #ffffff;
+ color: #000000;
+}
+
+.list .even_row {
+ background-color: #eeeeee;
+ color: #000000;
+}
+
+.list tr:hover {
+ background-color: #ccddee;
+}
+
+
+.list th {
+ text-align: left;
+ background: #dddddd;
+}
+
+.list .disabled {
+ color: #888888;
+ text-decoration: line-through;
+}
+
+#new_flag {
+ margin: 1em 0em;
+}
+
+/* edit */
+
+.edit {
+ margin-bottom: 2em;
+}
+
+.edit .header {
+ background: #dddddd;
+}
+
+.edit .help {
+ font-style: italic;
+}
+
+.edit td, .edit th {
+ padding: 1px 5px;
+}
+
+.edit th {
+ text-align: left;
+}
+
+#edit_mode {
+ margin: 1em 0em;
+}
+
+#flag_name {
+ width: 20em;
+}
+
+#flag_desc {
+ width: 20em;
+}
+
+#flag_sort {
+ width: 10em;
+}
+
+.option_value {
+ width: 10em;
+}
+
+.hidden {
+ display: none;
+}
+
+.txt_icon {
+ font-family: monospace;
+}
+
+.admin_error {
+ border: 1px solid red;
+ box-shadow: 0px 0px 4px #ff0000;
+ -webkit-box-shadow: 0px 0px 4px #ff0000;
+ -moz-box-shadow: 0px 0px 4px #ff0000;
+}
+
+.admin_error_text {
+ color: #cc0000;
+}
diff --git a/extensions/TrackingFlags/web/styles/edit_bug.css b/extensions/TrackingFlags/web/styles/edit_bug.css
new file mode 100644
index 000000000..132a6a1ca
--- /dev/null
+++ b/extensions/TrackingFlags/web/styles/edit_bug.css
@@ -0,0 +1,18 @@
+/* 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. */
+
+.tracking_flags {
+ width: auto !important;
+}
+
+.tracking_flags .field_label {
+ font-weight: normal !important;
+}
+
+#Create .tracking_flags th {
+ text-align: left;
+}