Allow keywords to depend on other fields (for example product)

hinted-selects
Vitaliy Filippov 2014-09-02 18:48:19 +04:00
parent 33b65171e8
commit 6425bfe311
9 changed files with 198 additions and 103 deletions

View File

@ -888,6 +888,12 @@ sub check_dependent_fields
{
my $vv = $self->get_ids($field_obj->value_field->name);
my @bad = grep { !ref $_ || !$field_obj->is_value_enabled($_, $vv) } list($value_objs);
if ($fn eq 'keywords')
{
# Keywords are silently enabled for any new visibility value.
$field_obj->add_visibility_values($_->id, [ list $vv ]) for grep { ref $_ } @bad;
@bad = grep { !ref $_ } @bad;
}
if (@bad)
{
my $n = $field_obj->value_field->name;

View File

@ -215,8 +215,8 @@ use constant CAN_TWEAK => {
keywords bug_severity priority component assigned_to votes qa_contact dependson blocked target_milestone estimated_time remaining_time see_also) },
default_value => { map { $_ => 1 } qw(bug_severity deadline keywords op_sys priority rep_platform short_desc status_whiteboard target_milestone version) },
nullable => { map { $_ => 1 } qw(alias bug_severity deadline keywords op_sys priority rep_platform status_whiteboard target_milestone version) },
visibility_field_id => { map { $_ => 1 } qw(bug_severity op_sys priority rep_platform status_whiteboard target_milestone version) },
value_field_id => { map { $_ => 1 } qw(bug_severity op_sys priority rep_platform) },
visibility_field_id => { map { $_ => 1 } qw(bug_severity op_sys priority rep_platform status_whiteboard target_milestone version keywords) },
value_field_id => { map { $_ => 1 } qw(bug_severity op_sys priority rep_platform keywords) },
default_field_id => { map { $_ => 1 } qw(bug_severity keywords op_sys priority component rep_platform status_whiteboard target_milestone version) },
};
@ -1004,20 +1004,52 @@ sub update_visibility_values
map { $_->id } @{ $type->new_from_list($visibility_value_ids) }
];
}
Bugzilla->dbh->do(
"DELETE FROM fieldvaluecontrol WHERE field_id=? AND value_id=?",
undef, $self->id, $controlled_value_id);
if (@$visibility_value_ids)
my $h = Bugzilla->fieldvaluecontrol->{$vis_field->id};
$h = $h->{values}->{$self->id}->{$controlled_value_id} if $controlled_value_id > 0;
$h = $h->{fields}->{$self->id} if $controlled_value_id == FLAG_VISIBLE;
$h = $h->{null}->{$self->id} if $controlled_value_id == FLAG_NULLABLE;
$h = $h->{clone}->{$self->id} if $controlled_value_id == FLAG_CLONED;
$h = { %$h };
my $add = [];
for (@$visibility_value_ids)
{
$h->{$_} ? delete $h->{$_} : push @$add, $_;
}
my $del = [ keys %$h ];
return 0 if !@$add && !@$del;
Bugzilla->dbh->do(
"DELETE FROM fieldvaluecontrol WHERE field_id=? AND value_id=?".
" AND visibility_value_id IN (".join(", ", @$del).")",
undef, $self->id, $controlled_value_id
);
$self->add_visibility_values($controlled_value_id, $add);
return 1;
}
sub add_visibility_values
{
my $self = shift;
my ($controlled_value_id, $visibility_value_ids) = @_;
my $f = $self->id;
for ($controlled_value_id)
{
$_ eq FLAG_VISIBLE or $_ = int($_) or return 0;
}
for (@$visibility_value_ids)
{
($_ = int($_)) > 0 or return 0;
}
# Ignore duplicate row errors
eval
{
my $f = $self->id;
Bugzilla->dbh->do(
"INSERT INTO fieldvaluecontrol (field_id, value_id, visibility_value_id) VALUES ".
join(",", map { "($f, $controlled_value_id, $_)" } @$visibility_value_ids)
);
}
# Touch the field
};
my $ok = !$@;
$self->touch;
return 1;
return $ok;
}
sub update_control_lists

View File

@ -9,6 +9,7 @@ use strict;
use base qw(Bugzilla::WebService);
use Bugzilla::Field::Choice;
use Bugzilla::User;
use Bugzilla::Util;
use Bugzilla::WebService::Util qw(validate);
use Bugzilla::Error;
@ -46,8 +47,13 @@ sub _get_value
return $value;
}
# Get all values for a field. Arguments:
# field => <field_name>
# Get all or some values for a field. Arguments:
# field => <field name>
# optional:
# name => <name(s) to search for exact match>
# match => <string(s) to search in the beginning of value name>
# visibility_value_ids => <ID(s) of controlling value in which returned ones should be visible>
# limit => maximum number of matches
sub get_values
{
my ($self, $params) = @_;
@ -56,18 +62,48 @@ sub get_values
{
return {status => 'field_not_select'};
}
my $values;
my $join = '';
my $where = [];
my $bind = [];
my $type = $field->value_type;
my @vv = $field->value_field_id ? list $params->{visibility_value_ids} : ();
my @match = list $params->{match};
my @name = list $params->{name};
if (@match || @name)
{
my @m;
push @m, ('v.'.$type->NAME_FIELD.' LIKE ?') x @match;
push @m, 'v.'.$type->NAME_FIELD.' IN ('.join(', ', ('?') x @name).')' if @name;
push @$where, '('.join(' OR ', @m).')';
push @$bind, (map { $_.'%' } @match), @name;
}
if (@vv)
{
$join = " INNER JOIN fieldvaluecontrol fc ON fc.field_id=?".
" AND fc.value_id=v.id AND fc.visibility_value_id IN (".join(", ", ("?") x @vv).")";
unshift @$bind, $field->id, @vv;
}
$where = @$where ? join(' AND ', @$where) : '1=1';
my $order = $type->LIST_ORDER;
$order =~ s/(^|,)\s*(\S)/$1v.$2/gso;
trick_taint($_) for @$bind;
my $values = Bugzilla->dbh->selectall_arrayref(
"SELECT v.* FROM ".$type->DB_TABLE." v $join WHERE $where GROUP BY v.id".
" ORDER BY $order ".($params->{limit} ? Bugzilla->dbh->sql_limit(int($params->{limit})) : ''),
{Slice=>{}}, @$bind
);
bless $_, $type for @$values;
if ($field->value_field_id)
{
$values = [ map { {
id => $_->id,
name => $_->name,
visibility_value_ids => [ map { $_->id } @{$_->visibility_values} ],
} } @{$field->legal_values} ];
} } @$values ];
}
else
{
$values = [ map { { id => $_->id, name => $_->name } } @{$field->legal_values} ];
$values = [ map { { id => $_->id, name => $_->name } } @$values ];
}
return {
status => 'ok',

View File

@ -159,6 +159,10 @@ function handleControllerField(e, controller)
// It is more correct to match selected values on name, because a
// target_milestone/version/component with the same name may exist for a different product
controlled = document.getElementById(controlled_id);
if (controlled.nodeName != 'SELECT')
{
continue;
}
copt = getSelectedNames(controlled);
bz_clearOptions(controlled);
if (field_metadata[controlled.id].nullable && !controlled.multiple)

View File

@ -277,12 +277,77 @@ function _value_id(field_name, id)
return 'v' + id + '_' + field_name;
}
// Data loader for keyword autocomplete
function keywordAutocomplete(hint, emptyOptions)
{
if (!hint.input.value)
{
hint.emptyText = 'Type at least 3 letters';
if (emptyOptions)
hint.replaceItems(convertSimpleList(emptyOptions));
else if (field_metadata.keywords.value_field)
{
var vv = getSelectedIds(document.getElementById(field_metadata.keywords.value_field));
var h = field_metadata[field_metadata.keywords.value_field].values.keywords;
var o = [];
for (var i in field_metadata.keywords.legal)
{
var controlled_value = field_metadata.keywords.legal[i];
if (checkValueVisibility(vv, h[controlled_value[0]]))
{
o.push([ '<span class="hintRealname">' + htmlspecialchars(controlled_value[1]) + '</span>', controlled_value[1] ]);
}
}
hint.replaceItems(o);
}
else
hint.replaceItems(null);
return;
}
var u = window.location.href.replace(/[^\/]+$/, '');
u += 'xml.cgi?output=json&method=Field.get_values&field=keywords&limit=20';
if (field_metadata.keywords.value_field)
{
var vv = getSelectedIds(document.getElementById(field_metadata.keywords.value_field));
for (var v in vv)
{
u += '&visibility_value_ids='+v;
}
}
var l = hint.input.value.split(/[\s,]*,[\s,]*/);
for (var i = 0; i < l.length; i++)
{
u += '&match='+encodeURI(l[i]);
}
AjaxLoader(u, function(x)
{
var r = {};
try { eval('r = '+x.responseText+';'); } catch (e) { return; }
if (r.status == 'ok')
{
var data = convertSimpleList(r.values);
// FIXME "3" constant, messages: remove hardcode, also in Bugzilla::User::match()
if (data.length == 0 && hint.input.value.length < 3)
hint.emptyText = 'Type at least 3 letters';
else
hint.emptyText = 'No keywords found';
hint.replaceItems(data);
}
});
}
function addKeywordsAutocomplete()
{
var emptyKeywordsOptions = [];
for (var i = 0; i < field_metadata.keywords.legal.length; i++)
var emptyKeywordsOptions = null;
if (!field_metadata.keywords.value_field)
{
emptyKeywordsOptions.push({ name: field_metadata.keywords.legal[i][1] });
emptyKeywordsOptions = [];
for (var i = 0; i < field_metadata.keywords.legal.length; i++)
{
emptyKeywordsOptions.push({ name: field_metadata.keywords.legal[i][1] });
}
}
new SimpleAutocomplete("keywords",
function(h) { keywordAutocomplete(h, emptyKeywordsOptions); },
@ -290,27 +355,22 @@ function addKeywordsAutocomplete()
);
}
// CustIS bug 66910 - check new keywords and requery description for its
// CustIS bug 66910 - check new keywords and requery description for it
function check_new_keywords(form)
{
var non_exist_keywords = [];
var cnt_exist_keywords = 0;
var input_keywords = form.keywords.value.split(",");
var exist_keywords = [];
for(var i = 0; i < emptyKeywordsOptions.length; i++)
var input_kw = form.keywords.value.trim().split(/[,\s]*,[,\s]*/);
var kw_hash = {};
for (var i = 0; i < field_metadata.keywords.legal.length; i++)
{
exist_keywords[i] = emptyKeywordsOptions[i].name.trim();
kw_hash[field_metadata.keywords.legal[i][1].toLowerCase()] = true;
}
for(var i = 0; i < input_keywords.length; i++)
for (var i = 0; i < input_kw.length; i++)
{
if (input_keywords[i].trim() != "" && exist_keywords.indexOf(input_keywords[i].trim()) == -1)
if (!kw_hash[input_kw[i].toLowerCase()])
{
non_exist_keywords[cnt_exist_keywords] = input_keywords[i].trim();
cnt_exist_keywords++;
non_exist_keywords.push(input_kw[i]);
}
}
if (non_exist_keywords.length > 0)
{
var keywords_submit = true;

View File

@ -174,51 +174,15 @@ function userAutocomplete(hint, emptyOptions, loadAllOnEmpty)
});
}
// Convert keyword list from API format for SimpleAutocomplete
function convertSimpleList(k)
{
var data = [];
for (var i = 0; i < k.length; i++)
data.push([ '<span class="hintRealname">' + k[i].name + '</span>', k[i].name ]);
data.push([ '<span class="hintRealname">' + htmlspecialchars(k[i].name) + '</span>', k[i].name ]);
return data;
}
// Data loader for keyword autocomplete
function keywordAutocomplete(hint, emptyOptions)
{
if (!hint.input.value)
{
hint.emptyText = 'Type at least 3 letters';
if (emptyOptions)
hint.replaceItems(convertSimpleList(emptyOptions));
else
hint.replaceItems(null);
return;
}
var u = window.location.href.replace(/[^\/]+$/, '');
u += 'xml.cgi?method=Keyword.get&output=json&maxkeywordmatches=20';
var l = hint.input.value.split(/[\s,]*,[\s,]*/);
for (var i = 0; i < l.length; i++)
u += '&match='+encodeURI(l[i]);
AjaxLoader(u, function(x) {
var r = {};
try { eval('r = '+x.responseText+';'); } catch (e) { return; }
if (r.status == 'ok')
{
var data = convertSimpleList(r.keywords);
// FIXME "3" constant, messages: remove hardcode, also in Bugzilla::User::match()
if (data.length == 0 && hint.input.value.length < 3)
hint.emptyText = 'Type at least 3 letters';
else
hint.emptyText = 'No keywords found';
hint.replaceItems(data);
}
});
}
// Data loader for field in buglist autocomplete
function fieldBuglistAutocomplete(hint, field, emptyOptions)
{
@ -233,7 +197,7 @@ function fieldBuglistAutocomplete(hint, field, emptyOptions)
hint.replaceItems(data);
}
});
}
}
function showFullComment(oper_id)
{

View File

@ -3,7 +3,7 @@
# Authors: Vitaliy Filippov <vitalif@mail.ru>, Vladimir Koptev <vladimir.koptev@gmail.com>
#%]
[% SET title = "Select Active " _ field.description _ "s For " _ field.value_field.description _ ' ' _ visibility_value.name | html %]
[% SET title = "Select Active " _ field.description _ " Objects For " _ field.value_field.description _ ' ' _ visibility_value.name | html %]
[% PROCESS global/header.html.tmpl %]
@ -37,7 +37,7 @@
</form>
<p>
<a href="editvalues.cgi?field=[% field.name %]">Edit or add [% field.description | html %]s</a> |
<a href="editvalues.cgi?field=[% field.name %]">Edit or add [% field.description | html %] objects</a> |
[% IF field.value_field.name == 'product' %]
<a href="editproducts.cgi?action=edit&product=[% visibility_value.name | uri %]">Edit product [% visibility_value.name | html %]</a>
[% ELSIF field.value_field.name == 'classification' %]

View File

@ -71,29 +71,7 @@
[%+ 'checked="checked"' IF value.isactive || !value.id %] />
(this value is selected as default in the parameters for this field)
</td>
[% IF field.value_field %]
<tr>
<th colspan="2" align="left">
<label for="visibility_value_id">Only appears when [%+ field.value_field.description | html %] is set to:</label>
</td>
</tr>
<tr>
<td colspan="2">
<select name="visibility_value_id[]" id="visibility_value_id" multiple="multiple" size="15" style="width: 400px">
[% IF field.value_field.nullable %]
<option value="0"[% IF field.is_value_enabled(value.id, 0) %] selected[% END %]>---</option>
[% END %]
[% FOREACH field_value = field.value_field.legal_values %]
[% IF field.visibility_field_id != field.value_field_id || field.has_visibility_value(field_value.id) %]
<option value="[% field_value.id | none %]" [% ' selected="selected"' IF field.is_value_enabled(value.id, field_value.id) %]>
[%- field_value.name | html -%]
</option>
[% END %]
[% END %]
</select>
</td>
</tr>
[% END %]
</tr>
[% IF field.name == "keywords" %]
<tr>
<th align="left">Description:</th>
@ -121,8 +99,30 @@
</tr>
[% END %]
[% END %]
[% IF field.value_field %]
<tr>
<th colspan="2" align="left">
<label for="visibility_value_id">Only appears when [%+ field.value_field.description | html %] is set to:</label>
</td>
</tr>
<tr>
<td colspan="2">
<select name="visibility_value_id[]" id="visibility_value_id" multiple="multiple" size="15" style="width: 400px">
[% IF field.value_field.nullable %]
<option value="0"[% IF field.is_value_enabled(value.id, 0) %] selected[% END %]>---</option>
[% END %]
[% FOREACH field_value = field.value_field.legal_values %]
[% IF field.visibility_field_id != field.value_field_id || field.has_visibility_value(field_value.id) %]
<option value="[% field_value.id | none %]" [% ' selected="selected"' IF field.is_value_enabled(value.id, field_value.id) %]>
[%- field_value.name | html -%]
</option>
[% END %]
[% END %]
</select>
</td>
</tr>
[% END %]
[% INCLUDE "admin/fieldvalues/control-list-common.html.tmpl" this_field=field this_value=value %]
</tr>
[% Hook.process('fields') %]
</table>

View File

@ -212,13 +212,7 @@ document.write(' <input type="button" name="check_all" value="Check All" onclick
<tr>
<th>
<script type="text/javascript">
var emptyKeywordsOptions = "null";
addListener(window, 'load', function() {
new SimpleAutocomplete("keywords",
function(h) { keywordAutocomplete(h, emptyKeywordsOptions); },
{ emptyText: 'No keywords found', multipleDelimiter: "," }
);
});
addListener(window, 'load', addKeywordsAutocomplete);
</script>
<label for="keywords">
<a href="describekeywords.cgi">Keywords</a>:
@ -232,7 +226,6 @@ document.write(' <input type="button" name="check_all" value="Check All" onclick
<option value="makeexact">Make the keywords be exactly this list</option>
</select>
</td>
</tr>
[% END %]