webui: field and widget binding refactoring

This is a Web UI wide change. Fields and Widgets binding was refactored
to enable proper two-way binding between them. This should allow to have
one source of truth (field) for multiple consumers - widgets or something
else. One of the goal is to have fields and widget implementations
independent on each other. So that one could use a widget without field
or use one field for multiple widgets, etc..

Basically a fields logic was split into separate components:
- adapters
- parsers & formatters
- binder

Adapters
- extract data from data source (FreeIPA RPC command result)
- prepares them for commands.

Parsers
- parse extracted data to format expected by field
- parse widget value to format expected by field

Formatters
- format field value to format suitable for widgets
- format field value to format suitable for adapter

Binder
- is a communication bridge between field and widget
- listens to field's and widget's events and call appropriate methods

Some side benefits:
- better validation reporting in multivalued widget

Reviewed-By: Adam Misnyovszki <amisnyov@redhat.com>
This commit is contained in:
Petr Vobornik
2013-11-13 15:49:25 +01:00
parent 66fb4d5e84
commit 0d05a50e19
25 changed files with 1764 additions and 944 deletions

View File

@@ -74,8 +74,8 @@
"reg",
"details.details_builder",
"details.section_builder",
"IPA.field_builder",
"IPA.widget_builder"
"field.field_builder",
"widget.widget_builder"
]
},
{
@@ -96,8 +96,10 @@
"IPA.bulk_associator",
"IPA.association_config",
"spec_util",
"_base.debug",
"_base.Spec_mod",
"datetime"
"datetime",
"util"
]
}
]
@@ -115,7 +117,7 @@
"facet.FacetState",
"facet.action_holder",
"details.facet_policies",
"IPA.field_container",
"field.field_container",
"IPA.widget_container",
"details.update_info",
"details.command_info",
@@ -179,10 +181,22 @@
{
"name": "Fields",
"classes": [
"IPA.field",
"field.field",
"*_field"
]
},
{
"name": "Binders",
"classes": [
"*Binder"
]
},
{
"name": "Adapters",
"classes": [
"*Adapter"
]
},
{
"name": "Formatters",
"classes": [
@@ -193,7 +207,7 @@
{
"name": "Validators",
"classes": [
"IPA.validator",
"field.validator",
"*_validator"
]
}

View File

@@ -184,3 +184,25 @@ input[type="radio"]:checked + label:before {
right: -2px;
}
}
// do not show error hint for valid value in multivalued widget if some
// other value is invalid
.control-group.error .valid .help-inline {
display: none;
}
// only type text for now - it should be replaced when we adopt Bootstrap 3
// based RCUE (patternfly)
.control-group.error .valid input[type=text] {
border-color: #62afdb !important;
&:focus {
border-color: rgba(82, 168, 236, 0.8);
outline: 0;
outline: thin dotted \9;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
-moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
}
}

View File

@@ -0,0 +1,336 @@
/* Authors:
* Petr Vobornik <pvoborni@redhat.com>
*
* Copyright (C) 2013 Red Hat
* see file 'COPYING' for use and warranty information
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
define(['dojo/_base/declare',
'dojo/_base/lang',
'dojo/on',
'./util'
],
function(declare, lang, on, util) {
/**
* Field binder
*
* Binds input widget with field - defines standard communication logic
* between widget and a field.
*
* Usage:
*
* var binder = new FieldBinder(widget, field).bind();
*
* // or
* var binder = new FieldBinder({
* field: field,
* widget: widget
* });
* binder.bind()
*
* @class FieldBinder
*/
var FieldBinder = declare([], {
/**
* Field
* @property {IPA.field}
*/
field: null,
/**
* Widget
* @property {IPA.input_widget}
*/
widget: null,
/**
* Binder is enabled
*
* Handlers are not be called when set to false.
*
* @property {boolean}
*/
enabled: true,
/**
* Handlers
* @protected
* @property {Function[]}
*/
handlers: null,
/**
* Value update is in progress
*
* When set, binder should not react to field's nor widget's value-change
* event.
*
* @property {boolean}
*/
updating: false,
/**
* Bind widget with field
*
* Listens for field's:
*
* - enable-change
* - valid-change
* - value-change
* - dirty-change
* - require-change
* - writable-change
* - readonly-change
* - reset
*
* Listens for widget's:
*
* - value-change
* - undo-click
*
* @param {boolean} hard
* Hard binding. Sets `field.widget` to `this.widget`.
* This option is for backward compatibility.
*/
bind: function(hard) {
var field = this.field;
var widget = this.widget;
if (hard) field.widget = widget;
this.handle(field, 'enable-change', this.on_field_enable_change);
this.handle(field, 'valid-change', this.on_field_valid_change);
this.handle(field, 'value-change', this.on_field_value_change);
this.handle(field, 'dirty-change', this.on_field_dirty_change);
this.handle(field, 'require-change', this.on_field_require_change);
this.handle(field, 'writable-change', this.on_field_writable_change);
this.handle(field, 'readonly-change', this.on_field_readonly_change);
this.handle(field, 'reset', this.on_field_reset);
this.handle(widget, 'value-change', this.on_widget_value_change);
this.handle(widget, 'undo-click', this.on_widget_undo_click);
return this;
},
/**
* Unbind all handlers
*/
unbind: function() {
var handler;
while ((handler = this.handlers.pop())) {
handler.remove();
}
},
/**
* Creates and registers the handler.
* Handler will be called in binder context and only if
* `this.enabled === true`.
*
* Do not use `on(target, type, handler)` directly.
*
* @param {Function} handler
* @return {Function} context bound handler
* @protected
*/
handle: function(target, type, handler) {
var _this = this;
var hndlr = function() {
if (_this.enabled !== true) return;
else {
handler.apply(_this, Array.prototype.slice.call(arguments, 0));
}
};
var reg_hndl = on(target, type, hndlr);
this.handlers.push(reg_hndl);
return hndlr;
},
/**
* Field enable change handler
*
* Reflect enabled state to widget
*
* @protected
*/
on_field_enable_change: function(event) {
this.widget.set_enabled(event.enabled);
},
/**
* Field valid change handler
* @protected
*/
on_field_valid_change: function(event) {
this.widget.set_valid(event.result);
},
/**
* Field dirty change handler
*
* Controls showing of widget's undo button
*
* @protected
*/
on_field_dirty_change: function(event) {
if (!this.field.undo) return;
if (event.dirty) {
this.widget.show_undo();
} else {
this.widget.hide_undo();
}
},
/**
* Field require change handler
*
* Updates widget's require state
*
* @protected
*/
on_field_require_change: function(event) {
this.widget.set_required(event.required);
},
/**
* Field require change handler
*
* Updates widget's require state
*
* @protected
*/
on_field_writable_change: function(event) {
this.widget.set_writable(event.writable);
},
/**
* Field require change handler
*
* Updates widget's require state
*
* @protected
*/
on_field_readonly_change: function(event) {
this.widget.set_read_only(event.read_only);
},
/**
* Field reset handler
*
* @param {Object} event
* @protected
*/
on_field_reset: function(event) {
this.copy_properties();
},
/**
* Field value change handler
* @protected
*/
on_field_value_change: function(event) {
if (this.updating) return;
var format_result = util.format(this.field.ui_formatter, event.value);
if (format_result.ok) {
this.updating = true;
this.widget.update(format_result.value);
this.updating = false;
} else {
// this should not happen in ideal world
window.console.warn('field format error: '+this.field.name);
}
},
/**
* Widget value change handler
* @protected
*/
on_widget_value_change: function(event) {
if (this.updating) return;
var val = this.widget.save();
var format_result = util.parse(this.field.ui_parser, val);
if (format_result.ok) {
this.updating = true;
this.field.set_value(format_result.value);
this.updating = false;
} else {
this.field.set_valid(format_result);
}
},
/**
* Widget undo click handler
* @protected
*/
on_widget_undo_click: function(event) {
this.field.reset();
},
/**
* Copies `label`, `tooltip`, `measurement_unit`, `undo`, `writable`,
* `read_only` from field to widget
*/
copy_properties: function() {
var field = this.field;
var widget = this.widget;
if (field.label) widget.label = field.label;
if (field.tooltip) widget.tooltip = field.tooltip;
if (field.measurement_unit) widget.measurement_unit = field.measurement_unit;
widget.undo = field.undo;
widget.set_writable(field.writable);
widget.set_read_only(field.read_only);
widget.set_required(field.is_required());
return this;
},
constructor: function(arg1, arg2) {
this.handlers = [];
if (arg2) {
this.field = arg1;
this.widget = arg2;
} else {
arg1 = arg1 || {};
this.field = arg1.field;
this.widget = arg1.widget;
}
}
});
return FieldBinder;
});

View File

@@ -366,7 +366,7 @@ define(['dojo/_base/declare',
var temp = lang.clone(preop);
this.spec_mod.mod(spec, temp);
this.spec_mod.del_rules(temp);
lang.mixin(spec, preop);
lang.mixin(spec, temp);
}
}
return spec;

View File

@@ -44,7 +44,11 @@
* @class _base.Provider
*
*/
define(['dojo/_base/declare','dojo/_base/lang'], function(declare, lang) {
define([
'dojo/_base/declare',
'dojo/_base/lang',
'./debug'],
function(declare, lang, debug) {
var undefined;
var Provider = declare(null, {
@@ -177,7 +181,7 @@ define(['dojo/_base/declare','dojo/_base/lang'], function(declare, lang) {
}
var ret = value || alternate;
if (!ret && key) {
if (!ret && key && debug.provider_missing_value) {
window.console.log('No value for:'+key);
}

View File

@@ -0,0 +1,41 @@
/* Authors:
* Petr Vobornik <pvoborni@redhat.com>
*
* Copyright (C) 2014 Red Hat
* see file 'COPYING' for use and warranty information
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
define([], function() {
/**
* Debug module
*
* One can set flags to enable console output of various messages.
*
* """
* var debug = require('freeipa._base.debug');
* debug.provider_missing_value = true;
* """
*
* Currently used flags
*
* - provider_missing_value
*
* @class _base.debug
*/
return {
provider_missing_value: false
};
});

View File

@@ -20,6 +20,7 @@
*/
define([
'dojo/on',
'./metadata',
'./ipa',
'./jquery',
@@ -30,7 +31,7 @@ define([
'./search',
'./association',
'./entity'],
function(metadata_provider, IPA, $, phases, reg, text) {
function(on, metadata_provider, IPA, $, phases, reg, text) {
/**
* Widgets, entities and fields related to Access Control that means
@@ -577,6 +578,7 @@ aci.attributes_widget = function(spec) {
$('.aci-attribute', that.table).
prop('checked', $(this).prop('checked'));
that.value_changed.notify([], that);
that.emit('value-change', { source: that });
}
}, th);
@@ -615,6 +617,7 @@ aci.attributes_widget = function(spec) {
'class': 'aci-attribute',
change: function() {
that.value_changed.notify([], that);
that.emit('value-change', { source: that });
}
}, td);
td = $('<td/>').appendTo(tr);
@@ -827,15 +830,9 @@ aci.permission_target_policy = function (spec) {
that.init = function() {
that.permission_target = that.container.widgets.get_widget(that.widget_name);
var type_select = that.permission_target.widgets.get_widget('type');
var type_f = that.container.fields.get_field('type');
type_select.value_changed.attach(function() {
that.apply_type();
});
type_select.undo_clicked.attach(function() {
that.apply_type();
});
on(type_f, 'value-change', that.apply_type);
};
that.apply_type = function () {
@@ -861,9 +858,7 @@ aci.permission_target_policy = function (spec) {
// permission plugin resets ipapermlocation to basedn when
// type is unset. -> use it as pristine value so undo will
// work correctly.
var loc = [IPA.env.basedn];
loc_w.update(loc);
loc_f.values = loc;
loc_f.set_value([IPA.env.basedn], true);
} else {
attrs = attr_multi.save();
attr_table.update(attrs);
@@ -1077,9 +1072,9 @@ aci.register = function() {
e.register({ type: 'delegation', spec: aci.delegation_entity_spec });
w.register('attributes', aci.attributes_widget);
f.register('attributes', IPA.checkboxes_field);
f.register('attributes', IPA.field);
w.register('rights', aci.rights_widget);
f.register('rights', IPA.checkboxes_field);
f.register('rights', IPA.field);
w.register('permission_target', aci.permission_target_widget);
};

View File

@@ -19,6 +19,8 @@
*/
define([
'dojo/_base/declare',
'./field',
'./metadata',
'./ipa',
'./jquery',
@@ -31,7 +33,8 @@ define([
'./search',
'./association',
'./entity'],
function(metadata_provider, IPA, $, navigation, phases, reg, rpc, text) {
function(declare, field_mod, metadata_provider, IPA, $, navigation,
phases, reg, rpc, text) {
var exp = IPA.automember = {};
@@ -445,29 +448,27 @@ IPA.automember.parse_condition_regex = function(regex) {
IPA.automember.condition_field = function(spec) {
spec = spec || {};
spec.adapter = spec.adapter || IPA.automember.condition_adapter;
var that = IPA.field(spec);
that.attr_name = spec.attribute || that.name;
that.load = function(record) {
var regexes = record[that.attr_name];
that.values = [];
if (regexes) {
for (var i=0, j=0; i<regexes.length; i++) {
var condition = IPA.automember.parse_condition_regex(regexes[i]);
that.values.push(condition);
}
}
that.load_writable(record);
that.reset();
};
return that;
};
IPA.automember.condition_adapter = declare([field_mod.Adapter], {
load: function(record) {
var regexes = this.inherited(arguments);
var values = [];
if (regexes) {
for (var i=0, j=0; i<regexes.length; i++) {
if (regexes[i] === '') continue;
var condition = IPA.automember.parse_condition_regex(regexes[i]);
values.push(condition);
}
}
return values;
}
});
IPA.automember.condition_widget = function(spec) {
spec = spec || {};

View File

@@ -523,6 +523,8 @@ IPA.cert.load_policy = function(spec) {
method: 'show',
args: [serial_number],
on_success: function(data, text_status, xhr) {
// copy it so consumers can notice the difference
that.container.certificate = lang.clone(that.container.certificate);
var cert = that.container.certificate;
cert.revocation_reason = data.result.result.revocation_reason;
that.notify_loaded();
@@ -914,12 +916,11 @@ IPA.cert.status_field = function(spec) {
that.load = function(result) {
that.register_listener();
that.reset();
that.field_load(result);
};
that.set_certificate = function(certificate) {
that.values = certificate;
that.reset();
that.set_value(certificate);
};
that.register_listener = function() {

View File

@@ -590,6 +590,15 @@ exp.details_facet = IPA.details_facet = function(spec, no_init) {
*/
that.dirty_changed = IPA.observer();
/**
* Get field
* @param {string} name Field name
* @returns {IPA.field}
*/
that.get_field = function(name) {
return that.fields.get_field(name);
};
/**
* @inheritDoc
*/
@@ -685,7 +694,7 @@ exp.details_facet = IPA.details_facet = function(spec, no_init) {
that.is_dirty = function() {
var fields = that.fields.get_fields();
for (var i=0; i<fields.length; i++) {
if (fields[i].enabled && fields[i].is_dirty()) {
if (fields[i].enabled && fields[i].dirty) {
return true;
}
}
@@ -741,7 +750,7 @@ exp.details_facet = IPA.details_facet = function(spec, no_init) {
for (var i=0; i<fields.length; i++) {
var field = fields[i];
if (!field.enabled || only_dirty && !field.is_dirty()) continue;
if (!field.enabled || only_dirty && !field.dirty) continue;
var values = field.save();
if (require_value && !values) continue;

View File

@@ -204,6 +204,15 @@ IPA.dialog = function(spec) {
return that;
};
/**
* Get field
* @param {string} name Field name
* @returns {IPA.field}
*/
that.get_field = function(name) {
return that.fields.get_field(name);
};
/** Validate dialog fields */
that.validate = function() {
var valid = true;

View File

@@ -21,20 +21,24 @@
define([
'dojo/_base/declare',
'./ipa',
'./jquery',
'./net',
'./field',
'./navigation',
'./menu',
'./phases',
'./reg',
'./rpc',
'./util',
'./text',
'./details',
'./search',
'./association',
'./entity'],
function(IPA, $, NET, navigation, menu, phases, reg, rpc, text) {
function(declare, IPA, $, NET, field_mod, navigation, menu, phases,
reg, rpc, util, text) {
var exp = IPA.dns = {
zone_permission_name: 'Manage DNS zone ${dnszone}'
@@ -1434,8 +1438,8 @@ IPA.dns.record_prepare_details_for_type = function(type, fields, container) {
*/
IPA.dnsrecord_host_link_field = function(spec) {
var that = IPA.link_field(spec);
IPA.dnsrecord_host_link_widget = function(spec) {
var that = IPA.link_widget(spec);
that.other_pkeys = function() {
var pkey = that.facet.get_pkeys();
return [pkey[1]+'.'+pkey[0]];
@@ -1587,12 +1591,20 @@ IPA.dnsrecord_adder_dialog_type_policy = function(spec) {
IPA.dns.record_type_table_field = function(spec) {
spec = spec || {};
spec.adapter = spec.adapter || IPA.dns.record_type_adapter;
var that = IPA.field(spec);
that.dnstype = spec.dnstype;
that.load = function(record) {
return that;
};
IPA.dns.record_type_adapter = declare([field_mod.Adapter], {
separator: ';',
load: function(record) {
var data = {};
@@ -1602,22 +1614,16 @@ IPA.dns.record_type_table_field = function(spec) {
for (var i=0, j=0; i<record.dnsrecords.length; i++) {
var dnsrecord = record.dnsrecords[i];
if(dnsrecord.dnstype === that.dnstype) {
if(dnsrecord.dnstype === this.context.dnstype) {
dnsrecord.position = j;
j++;
data.dnsrecords.push(dnsrecord);
}
}
that.values = data;
that.load_writable(record);
that.reset();
};
return that;
};
return data;
}
});
IPA.dns.record_type_table_widget = function(spec) {
@@ -2040,70 +2046,35 @@ IPA.dns.record_type_table_widget = function(spec) {
IPA.dns.netaddr_field = function(spec) {
spec = spec || {};
var that = IPA.multivalued_field(spec);
that.load = function(record) {
that.record = record;
that.values = that.get_value(record, that.name);
that.values = that.values[0].split(';');
that.load_writable(record);
that.reset();
};
that.test_dirty = function() {
if (that.read_only) return false;
var values = that.field_save();
//check for empty value: null, [''], '', []
var orig_empty = IPA.is_empty(that.values);
var new_empty= IPA.is_empty(values);
if (orig_empty && new_empty) return false;
if (orig_empty != new_empty) return true;
//strict equality - checks object's ref equality, numbers, strings
if (values === that.values) return false;
//compare values in array
if (values.length !== that.values.length) return true;
for (var i=0; i<values.length; i++) {
if (values[i] != that.values[i]) {
return true;
}
}
return that.widget.test_dirty();
};
that.save = function(record) {
var values = that.field_save();
var new_val = values.join(';');
if (record) {
record[that.name] = new_val;
}
return [new_val];
};
that.validate = function() {
var values = that.field_save();
return that.validate_core(values);
};
spec.adapter = IPA.dns.netaddr_adapter;
var that = IPA.field(spec);
return that;
};
IPA.dns.netaddr_adapter = declare([field_mod.Adapter], {
separator: ';',
load: function(record) {
var value = this.inherited(arguments)[0];
if (value) {
if (value[value.length-1] === this.separator) {
value = value.substring(0, value.length-1);
}
value = value.split(this.separator);
}
value = util.normalize_value(value);
return value;
},
save: function(value, record) {
if (value[0]) {
value = [value.join(this.separator)];
}
return this.inherited(arguments, [value, record]);
}
});
IPA.dns.record_modify_column = function(spec) {
spec = spec || {};
@@ -2519,8 +2490,8 @@ exp.register = function() {
w.register('dnszone_name', IPA.dnszone_name_widget);
w.register('force_dnszone_add_checkbox', IPA.force_dnszone_add_checkbox_widget);
f.register('force_dnszone_add_checkbox', IPA.checkbox_field);
w.register('dnsrecord_host_link', IPA.link_widget);
f.register('dnsrecord_host_link', IPA.dnsrecord_host_link_field);
w.register('dnsrecord_host_link', IPA.dnsrecord_host_link_widget);
f.register('dnsrecord_host_link', IPA.field);
w.register('dnsrecord_type', IPA.dnsrecord_type_widget);
f.register('dnsrecord_type', IPA.dnsrecord_type_field);
w.register('dnsrecord_type_table', IPA.dns.record_type_table_widget);

File diff suppressed because it is too large Load Diff

View File

@@ -430,7 +430,7 @@ IPA.host_fqdn_field = function(spec) {
};
that.child_value_changed = function() {
that.validate();
that.set_value(that.widget.save());
};
return that;
@@ -523,8 +523,9 @@ IPA.dnszone_select_widget = function(spec) {
return that;
};
IPA.host_dnsrecord_entity_link_field = function(spec){
var that = IPA.link_field(spec);
IPA.host_dnsrecord_entity_link_widget = function(spec) {
var that = IPA.link_widget(spec);
that.other_pkeys = function(){
var pkey = that.facet.get_pkey();
@@ -852,8 +853,8 @@ exp.register = function() {
w.register('host_fqdn', IPA.host_fqdn_widget);
f.register('dnszone_select', IPA.field);
w.register('dnszone_select', IPA.dnszone_select_widget);
f.register('host_dnsrecord_entity_link', IPA.host_dnsrecord_entity_link_field);
w.register('host_dnsrecord_entity_link', IPA.link_widget);
f.register('host_dnsrecord_entity_link', IPA.field);
w.register('host_dnsrecord_entity_link', IPA.host_dnsrecord_entity_link_widget);
f.register('force_host_add_checkbox', IPA.checkbox_field);
w.register('force_host_add_checkbox', IPA.force_host_add_checkbox_widget);
f.register('host_password', IPA.field);

View File

@@ -218,7 +218,7 @@ IPA.rule_association_table_field = function(spec) {
//performs delete operation.
if (!that.widget.enabled) {
var values = that.save();
var values = that.widget.save();
if (values.length > 0) { //no need to delete if has no values

View File

@@ -19,6 +19,8 @@
*/
define([
'dojo/_base/declare',
'./field',
'./ipa',
'./jquery',
'./phases',
@@ -29,7 +31,7 @@ define([
'./search',
'./association',
'./entity'],
function(IPA, $, phases, reg, rpc, text) {
function(declare, field_mod, IPA, $, phases, reg, rpc, text) {
var exp =IPA.service = {};
@@ -66,16 +68,16 @@ return {
fields: [
'krbprincipalname',
{
$type: 'service_name',
name: 'service',
label: '@i18n:objects.service.service',
read_only: true
read_only: true,
adapter: IPA.service_name_adapter
},
{
$type: 'service_host',
name: 'host',
label: '@i18n:objects.service.host',
read_only: true
read_only: true,
adapter: IPA.service_host_adapter
},
{
name: 'ipakrbauthzdata',
@@ -271,45 +273,21 @@ IPA.service_adder_dialog = function(spec) {
return that;
};
IPA.service_name_field = function(spec) {
spec = spec || {};
var that = IPA.field(spec);
that.load = function(record) {
that.field_load(record);
IPA.service_name_adapter = declare([field_mod.Adapter], {
load: function(record) {
var krbprincipalname = record.krbprincipalname[0];
var value = krbprincipalname.replace(/\/.*$/, '');
that.values = [value];
that.reset();
};
return that;
};
IPA.service_host_field = function(spec) {
spec = spec || {};
var that = IPA.field(spec);
that.load = function(record) {
that.field_load(record);
return [value];
}
});
IPA.service_host_adapter = declare([field_mod.Adapter], {
load: function(record) {
var krbprincipalname = record.krbprincipalname[0];
var value = krbprincipalname.replace(/^.*\//, '').replace(/@.*$/, '');
that.values = [value];
that.reset();
};
return that;
};
return [value];
}
});
IPA.service_provisioning_status_widget = function (spec) {
@@ -512,10 +490,6 @@ phases.on('registration', function() {
e.register({type: 'service', spec: exp.entity_spec});
f.register('service_name', IPA.service_name_field);
w.register('service_name', IPA.text_widget);
f.register('service_host', IPA.service_host_field);
w.register('service_host', IPA.text_widget);
f.register('service_provisioning_status', IPA.field);
w.register('service_provisioning_status', IPA.service_provisioning_status_widget);
a.register('service_unprovision', IPA.service.unprovision_action);

View File

@@ -133,10 +133,10 @@ return {
metadata: '@mo-param:user:userpassword'
},
{
$type: 'datetime',
name: 'krbpasswordexpiration',
label: '@i18n:objects.user.krbpasswordexpiration',
read_only: true,
formatter: 'datetime'
read_only: true
},
'uidnumber',
'gidnumber',
@@ -473,7 +473,7 @@ IPA.user_adder_dialog = function(spec) {
var password2 = field2.save()[0];
if (password1 !== password2) {
field2.show_error(text.get('@i18n:password.password_must_match'));
field2.set_valid({ valid: false, message: text.get('@i18n:password.password_must_match') });
valid = false;
}

View File

@@ -0,0 +1,338 @@
/* Authors:
* Petr Vobornik <pvoborni@redhat.com>
*
* Copyright (C) 2014 Red Hat
* see file 'COPYING' for use and warranty information
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
define([
'dojo/_base/lang',
'./text'
],
function(lang, text) {
function equals_obj_def(a, b, options) {
var same = true;
var checked = {};
var check_same = function(a, b, skip) {
var same = true;
skip = skip || {};
for (var key in a) {
if (a.hasOwnProperty(key) && !(key in skip)) {
var va = a[key];
var vb = b[key];
if (!equals(va, vb, options)) {
same = false;
skip[a] = true;
break;
}
}
}
return same;
};
same = check_same(a,b, checked);
same = same && check_same(b,a, checked);
return same;
}
function equals_obj(a, b, options) {
if (options.comparator) {
return options.comparator(a, b, options);
} else {
return equals_obj_def(a, b, options);
}
}
function equals_array(a1, b1, options) {
var a = a1,
b = b1;
if (!a || !b) return false;
if (a1.length !== b1.length) return false;
if (options.unordered) {
a = a1.slice(0);
b = b1.slice(0);
a.sort();
b.sort();
}
for (var i=0; i<a.length; i++) {
if (!equals(a[i], b[i], options)) return false;
}
return true;
}
function equals(a, b, options) {
var a_t = typeof a;
var b_t = typeof b;
options = options || {};
if (a_t !== b_t) return false;
if (a === b) return true;
if (['string', 'number', 'function', 'boolean',
'undefined'].indexOf(a_t) > -1) {
return false;
} else if (a === null || b === null) {
return false;
} else if (lang.isArray(a)) {
return equals_array(a, b, options);
} else if (a instanceof Date) {
return a.getTime() === b.getTime();
} else { // objects
return equals_obj(a, b, options);
}
}
function is_empty(value) {
var empty = false;
if (!value) empty = true;
if (lang.isArray(value)) {
empty = empty || value.length === 0 ||
(value.length === 1) && (value[0] === '');
} else if (typeof value === 'object') {
var has_p = false;
for (var p in value) {
if (value.hasOwnProperty(p)) {
has_p = true;
break;
}
}
empty = !has_p;
} else if (value === '') empty = true;
return empty;
}
function dirty(value, pristine, options) {
// check for empty value: null, [''], '', []
var orig_empty = is_empty(pristine);
var new_empty= is_empty(value);
if (orig_empty && new_empty) return false;
if (orig_empty != new_empty) return true;
// strict equality - checks object's ref equality, numbers, strings
if (value === pristine) return false;
return !equals(value, pristine, options);
}
function format_single(formatter, value, error_text, method) {
var val = value,
ok = true,
msg = null;
try {
if (method === 'format') {
val = formatter.format(val);
} else {
val = formatter.parse(val);
}
} catch (e) {
if (e.reason !== method) throw e;
ok = false;
value = e.value;
msg = e.message || error_text;
}
return {
ok: ok,
value: val,
message: msg
};
}
function format_core(formatter, value, error_text, method) {
if (!formatter) return { ok: true, value: value };
if (lang.isArray(value)) {
var res = {
ok: true,
value: [],
messages: []
};
for (var i=0, l=value.length; i<l; i++) {
var single_res = format_single(formatter, value[i], error_text, method);
res.ok = res.ok && single_res.ok;
res.value[i] =single_res.value;
res.messages[i] = single_res.message;
if (!res.ok) {
if (l > 1) res.message = error_text;
else res.message = res.messages[0];
}
}
return res;
} else {
return format_single(formatter, value, error_text, method);
}
}
function format(formatter, value, error_text) {
var err = error_text || text.get('@i18n:widget.validation.format');
return format_core(formatter, value, err, 'format');
}
function parse(formatter, value, error_text) {
var err = error_text || text.get('@i18n:widget.validation.parse');
return format_core(formatter, value, err, 'parse');
}
function normalize_value(value) {
if (!(value instanceof Array)) {
value = value !== undefined ? [value] : [];
}
if (!value.length) {
value = [''];
}
return value;
}
function emit_delayed(target, type, event, delay) {
delay = delay || 0;
window.setTimeout(function() {
target.emit(type, event);
}, 0);
}
/**
* Module with utility functions
* @class
* @singleton
*/
var util = {
/**
* Checks if two variables have equal value
*
* - `string`, `number`, `function`, `boolean`, `null`,
* `undefined` are compared with strict equality
* - 'object' and arrays are compared by values
*
* Available options:
*
* - `unordered` - boolean, sort arrays before value comparison. Does
* not modify original values.
* - `comparator`- function(a,b), returns bool - custom object comparator
*
* @param {Mixed} a
* @param {Mixed} b
* @param {String[]} [options]
* @return {boolean} `a` and `b` are value-equal
*/
equals: equals,
/**
* Check if value is empty.
*
* True when:
*
* - value is undefined or `null` or `''`
* - value is empty Array
* - value is Array with an empty string (`''`)
* - value is empty Object- `{}`
* @param value - value to check
* @return {boolean}
*/
is_empty: is_empty,
/**
* Special kind of negative `equals` where variants of `empty_value` are
* considered same.
*
* @param {Mixed} value New value
* @param {Mixed} pristine Pristine value
* @param {String[]} [options] control options, same as in `equals`
* @return {boolean} `value` and `pristine` differs
*/
dirty: dirty,
/**
* Format value or values using a formatter
*
* Output format for single values:
*
* {
* ok: true|false,
* value: null | formatted value,
* message: null | string
* }
*
* Output form for array:
*
* {
* ok: true|false,
* value: array of formatted values,
* messages: array of error messages
* message: null | string
* }
*
* @param {IPA.formatter} formatter
* @param {Mixed} value
* @param {string} error Default error message
* @return {Object}
*/
format: format,
/**
* Basically the same as format method, just uses formatter's `parse`
* method instead of `format` method.
*
* @param {IPA.formatter} formatter
* @param {Mixed} value
* @param {string} error Default error message
* @return {Object}
*/
parse: parse,
/**
* Encapsulates value into array if it's not already an array.
*
* @param {Mixed} value
* @returns {Array} normalized value
*/
normalize_value: normalize_value,
/**
* Emit delayed event
*
* Uses timer in order to wait for current processing to finish.
*
* @param {Evented} object Source object which emits the event
* @param {String} type Name of the event to emit
* @param {Object} event Event object
* @param {Number} [delay=0]
*/
emit_delayed: emit_delayed
};
return util;
});

View File

@@ -32,13 +32,15 @@ define(['dojo/_base/array',
'./datetime',
'./ipa',
'./jquery',
'./navigation',
'./phases',
'./reg',
'./rpc',
'./text'
'./text',
'./util'
],
function(array, lang, Evented, has, keys, on, builder, datetime, IPA, $,
phases, reg, rpc, text) {
function(array, lang, Evented, has, keys, on, builder, datetime,
IPA, $, navigation, phases, reg, rpc, text, util) {
/**
* Widget module
@@ -127,6 +129,12 @@ IPA.widget = function(spec) {
*/
that.enabled = spec.enabled === undefined ? true : spec.enabled;
/**
* Enables showing of validation errors
* @property {boolean}
*/
that.show_errors = spec.show_errors === undefined ? true : spec.show_errors;
/**
* Create HTML representation of a widget.
* @method
@@ -190,6 +198,24 @@ IPA.widget = function(spec) {
return child;
};
that.add_class = function(cls) {
if (that.container) {
that.container.addClass(cls);
}
};
that.remove_class = function(cls) {
if (that.container) {
that.container.removeClass(cls);
}
};
that.toggle_class = function(cls, flag) {
if (that.container) {
that.container.toggleClass(cls, flag);
}
};
that.widget_create = that.create;
that.widget_set_enabled = that.set_enabled;
@@ -259,6 +285,7 @@ IPA.input_widget = function(spec) {
* Value changed event.
*
* Raised when user modifies data by hand.
* @deprecated
*
* @event
*/
@@ -266,6 +293,7 @@ IPA.input_widget = function(spec) {
/**
* Undo clicked event.
* @deprecated
*
* @event
*/
@@ -273,6 +301,7 @@ IPA.input_widget = function(spec) {
/**
* Updated event.
* @deprecated
*
* Raised when widget content gets updated - raised by
* {@link IPA.input_widget#update} method.
@@ -326,6 +355,13 @@ IPA.input_widget = function(spec) {
return [];
};
/**
* Alias of save
*/
that.get_value = function() {
return that.save();
};
/**
* This function creates an undo link in the container.
* On_undo is a link click callback. It can be specified to custom
@@ -383,27 +419,60 @@ IPA.input_widget = function(spec) {
* @return {jQuery} error link jQuery reference
*/
that.get_error_link = function() {
return $('span[name="error_link"]', that.container);
return $('span[name="error_link"]', that.container).eq(0);
};
/**
* Set's validity of widget's value. Usually checked by outside logic.
* @param {Object} result Validation result as defined in IPA.validator
*/
that.set_valid = function(result) {
var old = that.valid;
that.valid = result.valid;
that.toggle_class('valid', that.valid);
if (!that.valid) {
that.show_error(result.message);
} else {
that.hide_error();
}
if (old !== that.valid) {
that.emit("valid-change", {
source: that,
valid: that.valid,
result: result
});
}
};
/**
* Show error message
* @protected
* @param {string} message
* @fires error-show
* @param {Object} error
*/
that.show_error = function(message) {
var error_link = that.get_error_link();
error_link.html(message);
error_link.css('display', '');
that.emit('error-show', { source: that, error: message });
if (that.show_errors) {
var error_link = that.get_error_link();
error_link.html(message);
error_link.css('display', '');
}
that.emit('error-show', {
source: that,
error: message,
displayed: that.show_errors
});
};
/**
* Hide error message
* @protected
* @fires error-hide
*/
that.hide_error = function() {
var error_link = that.get_error_link();
error_link.html('');
error_link.css('display', 'none');
that.emit('error-hide', { source: that });
};
@@ -457,6 +526,38 @@ IPA.input_widget = function(spec) {
return !that.read_only && !!that.writable;
};
/**
* Set writable
* @fires writable-change
* @param {boolean} writable
*/
that.set_writable = function(writable) {
var changed = writable !== that.writable;
that.writable = writable;
if (changed) {
that.emit('writable-change', { source: that, writable: writable });
}
};
/**
* Set read only
* @fires readonly-change
* @param {boolean} writable
*/
that.set_read_only = function(read_only) {
var changed = read_only !== that.read_only;
that.read_only = read_only;
if (changed) {
that.emit('readonly-change', { source: that, read_only: read_only });
}
};
/**
* Focus input element
* @abstract
@@ -499,6 +600,7 @@ IPA.input_widget = function(spec) {
// methods that should be invoked by subclasses
that.widget_hide_error = that.hide_error;
that.widget_show_error = that.show_error;
that.widget_set_valid = that.set_valid;
return that;
};
@@ -680,7 +782,7 @@ IPA.multivalued_widget = function(spec) {
that.child_spec = spec.child_spec;
that.size = spec.size || 30;
that.undo_control;
that.initialized = false;
that.initialized = true;
that.rows = [];
@@ -693,24 +795,16 @@ IPA.multivalued_widget = function(spec) {
row.remove_link.show();
}
that.value_changed.notify([], that);
that.emit('child-value-change', { source: that, row: row });
that.emit('value-change', { source: that });
that.emit('child-value-change', { source: that, row: row });
};
that.on_child_undo_clicked = function(row) {
if (row.is_new) {
that.remove_row(row);
} else {
//reset
row.widget.update(row.original_values);
row.widget.set_deleted(false);
row.deleted = false;
row.remove_link.show();
that.reset_row(row);
}
row.widget.hide_undo();
that.value_changed.notify([], that);
that.emit('child-undo-click', { source: that, row: row });
};
@@ -745,20 +839,45 @@ IPA.multivalued_widget = function(spec) {
}
};
that.show_child_error = function(index, error) {
that.set_valid = function (result) {
that.rows[index].widget.show_error(error);
};
var old = that.valid;
that.valid = result.valid;
that.get_saved_value_row_index = function(index) {
if (!result.valid && result.errors) {
var offset = 0;
for (var i=0; i<that.rows.length; i++) {
for (var i=0; i<that.rows.length;i++) {
var val_result = null;
if (that.rows[i].deleted) {
offset++;
val_result = { valid: true };
} else {
val_result = result.results[i-offset];
}
var widget = that.rows[i].widget;
if (val_result) widget.set_valid(val_result);
}
if(that.rows[i].deleted) index++;
if(i === index) return i;
if (that.rows.length > 0) {
var error_link = that.get_error_link();
error_link.css('display', 'none');
error_link.html('');
} else {
that.show_error(result.message);
}
} else {
that.hide_error();
}
return -1; //error state
if (old !== that.valid) {
that.emit("valid-change", {
source: that,
valid: that.valid,
result: result
});
}
};
that.save = function() {
@@ -829,10 +948,10 @@ IPA.multivalued_widget = function(spec) {
row.original_values = values;
row.widget.update(values);
row.widget.value_changed.attach(function() {
on(row.widget, 'value-change', function() {
that.on_child_value_changed(row);
});
row.widget.undo_clicked.attach(function() {
on(row.widget, 'undo-click', function() {
that.on_child_undo_clicked(row);
});
on(row.widget, 'error-show', function() {
@@ -847,8 +966,6 @@ IPA.multivalued_widget = function(spec) {
html: text.get('@i18n:buttons.remove'),
click: function () {
that.remove_row(row);
that.value_changed.notify([], that);
that.emit('value-change', { source: that });
return false;
}
}).appendTo(row.container);
@@ -902,6 +1019,17 @@ IPA.multivalued_widget = function(spec) {
}).appendTo(container);
};
that.reset_row = function(row) {
row.widget.update(row.original_values);
row.widget.set_deleted(false);
row.deleted = false;
row.remove_link.show();
row.widget.hide_undo();
that.value_changed.notify([], that);
that.emit('value-change', { source: that });
};
that.remove_row = function(row) {
if (row.is_new) {
row.container.remove();
@@ -912,6 +1040,8 @@ IPA.multivalued_widget = function(spec) {
row.remove_link.hide();
row.widget.show_undo();
}
that.value_changed.notify([], that);
that.emit('value-change', { source: that });
};
that.remove_rows = function() {
@@ -929,30 +1059,14 @@ IPA.multivalued_widget = function(spec) {
if (row.deleted || row.is_new) return true;
var values = row.widget.save();
if (!values) return false;
var value = row.widget.save();
if (row.original_values.length !== values.length) return true;
for (var i=0; i<values.length; i++) {
if (values[i] !== row.original_values[i]) {
return true;
}
if (util.dirty(value, row.original_values, { unordered: true })) {
return true;
}
return false;
};
that.test_dirty = function() {
var dirty = false;
for(var i=0; i < that.rows.length; i++) {
dirty = dirty || that.test_dirty_row(that.rows[i]);
}
return dirty;
};
that.update = function(values, index) {
var value;
@@ -1693,10 +1807,15 @@ IPA.select_widget = function(spec) {
};
that.update = function(values) {
var old = that.save()[0];
var value = values[0];
var option = $('option[value="'+value+'"]', that.select);
if (!option.length) return;
option.prop('selected', true);
if (option.length) {
option.prop('selected', true);
} else {
// default was selected instead of supplied value, hence notify
util.emit_delayed(that,'value-change', { source: that });
}
that.updated.notify([], that);
that.emit('update', { source: that });
};
@@ -1869,7 +1988,8 @@ IPA.boolean_formatter = function(spec) {
spec = spec || {};
var that = IPA.formatter(spec);
/** Parse error */
that.parse_error = text.get(spec.parse_error || 'Boolean value expected');
/** Formatted value for true */
that.true_value = text.get(spec.true_value || '@i18n:true');
/** Formatted value for false */
@@ -1881,9 +2001,9 @@ IPA.boolean_formatter = function(spec) {
/**
* Result of parse of `undefined` or `null` value will be `empty_value`
* if set.
* @property {boolean|undefined}
* @property {boolean}
*/
that.empty_value = spec.empty_value;
that.empty_value = spec.empty_value !== undefined ? spec.empty_value : false;
/**
* Convert string boolean value into real boolean value, or keep
@@ -1894,9 +2014,8 @@ IPA.boolean_formatter = function(spec) {
*/
that.parse = function(value) {
if (value === undefined || value === null) {
if (that.empty_value !== undefined) value = that.empty_value;
else return '';
if (util.is_empty(value)) {
value = that.empty_value;
}
if (value instanceof Array) {
@@ -1910,11 +2029,17 @@ IPA.boolean_formatter = function(spec) {
value = true;
} else if (value === 'false') {
value = false;
} // leave other values unchanged
}
}
if (typeof value === 'boolean') {
if (that.invert_value) value = !value;
} else {
throw {
reason: 'parse',
value: that.empty_value,
message: that.parse_error
};
}
return value;
@@ -1987,13 +2112,32 @@ IPA.datetime_formatter = function(spec) {
var that = IPA.formatter(spec);
that.template = spec.template;
that.parse_error = text.get(spec.parse_error || '@i18n:widget.validation.datetime');
that.parse = function(value) {
if (value === '') return null;
var date = datetime.parse(value);
if (!date) {
throw {
reason: 'parse',
value: null,
message: that.parse_error
};
}
return date;
};
that.format = function(value) {
if (!value) return '';
var date = datetime.parse(value);
if (!date) return value;
var str = datetime.format(date, that.template);
if (!(value instanceof Date)) {
throw {
reason: 'format',
value: '',
message: 'Input value is not of Date type'
};
}
var str = datetime.format(value, that.template);
return str;
};
return that;
@@ -2659,12 +2803,6 @@ IPA.table_widget = function (spec) {
return rows;
};
that.show_error = function(message) {
var error_link = that.get_error_link();
error_link.html(message);
error_link.css('display', '');
};
that.set_enabled = function(enabled) {
that.widget_set_enabled(enabled);
@@ -3599,13 +3737,26 @@ IPA.entity_select_widget = function(spec) {
IPA.link_widget = function(spec) {
var that = IPA.input_widget(spec);
that.is_link = spec.is_link || false;
/**
* Entity a link points to
* @property {entity.entity}
*/
that.other_entity = IPA.get_entity(spec.other_entity);
/**
* Raised when link is clicked
* @event
* Function which should return primary keys of link target in case of
* link points to an entity.
* @property {Function}
*/
that.link_clicked = IPA.observer();
that.other_pkeys = spec.other_pkeys || other_pkeys;
that.is_link = spec.is_link || false;
that.value = [];
function other_pkeys () {
return that.facet.get_pkeys();
}
/** @inheritDoc */
that.create = function(container) {
@@ -3617,8 +3768,7 @@ IPA.link_widget = function(spec) {
html: '',
'class': 'link-btn',
click: function() {
that.link_clicked.notify([], that);
that.emit('link-click', { source: that });
that.on_link_clicked();
return false;
}
}).appendTo(container);
@@ -3628,11 +3778,19 @@ IPA.link_widget = function(spec) {
};
/** @inheritDoc */
that.update = function (values){
that.update = function(values) {
if (values || values.length > 0) {
that.nonlink.text(values[0]);
that.link.text(values[0]);
that.value = util.normalize_value(values)[0] || '';
that.link.html(that.value);
that.nonlink.html(that.value);
that.check_entity_link();
that.updated.notify([], that);
that.emit('update', { source: that });
};
that.update_link = function() {
if (that.value) {
if(that.is_link) {
that.link.css('display','');
that.nonlink.css('display','none');
@@ -3641,13 +3799,54 @@ IPA.link_widget = function(spec) {
that.nonlink.css('display','');
}
} else {
that.link.html('');
that.nonlink.html('');
that.link.css('display','none');
that.nonlink.css('display','none');
}
that.updated.notify([], that);
that.emit('update', { source: that });
};
/**
* Handler for widget `link_click` event
*/
that.on_link_clicked = function() {
navigation.show_entity(
that.other_entity.name,
'default',
that.other_pkeys());
};
/**
* Check if entity exists
*
* - only if link points to an entity
* - updates link visibility accordingly
*/
that.check_entity_link = function() {
//In some cases other entity may not be present.
//For example when DNS is not configured.
if (!that.other_entity) {
that.is_link = false;
return;
}
rpc.command({
entity: that.other_entity.name,
method: 'show',
args: that.other_pkeys(),
options: {},
retry: false,
on_success: function(data) {
that.is_link = data.result && data.result.result;
that.update_link();
},
on_error: function() {
that.is_link = false;
that.update_link();
}
}).execute();
that.update_link();
};
/** @inheritDoc */
@@ -4119,7 +4318,7 @@ exp.fluid_layout = IPA.fluid_layout = function(spec) {
text: label_text
}).appendTo(label_cont);
var input = widget.get_input();
var input = widget.get_input && widget.get_input();
if (input && input.length) input = input[0];
@@ -4150,6 +4349,8 @@ exp.fluid_layout = IPA.fluid_layout = function(spec) {
that.register_state_handlers = function(widget) {
on(widget, 'require-change', that.on_require_change);
on(widget, 'enabled-change', that.on_enabled_change);
on(widget, 'readonly-change', that.on_require_change);
on(widget, 'writable-change', that.on_require_change);
on(widget, 'error-show', that.on_error_show);
on(widget, 'error-hide', that.on_error_hide);
};
@@ -4165,7 +4366,7 @@ exp.fluid_layout = IPA.fluid_layout = function(spec) {
var row = that._get_row(event);
if (!row) return;
row.toggleClass('required', !!event.required);
row.toggleClass('required', !!event.required && event.source.is_writable());
};
that.on_error_show = function(event) {
@@ -4184,7 +4385,7 @@ exp.fluid_layout = IPA.fluid_layout = function(spec) {
that.update_state = function(row, widget) {
row.toggleClass('disabled', !widget.enabled);
row.toggleClass('required', !!widget.required);
row.toggleClass('required', !!widget.required && widget.is_writable());
};
that._get_row = function(event) {
@@ -4685,9 +4886,10 @@ IPA.widget_container = function(spec) {
/**
* Widget builder
* @class
* @class widget.widget_builder
* @alternateClassName IPA.widget_builder
*/
IPA.widget_builder = function(spec) {
exp.widget_builder = IPA.widget_builder = function(spec) {
spec = spec || {};
@@ -4792,9 +4994,9 @@ IPA.sshkey_widget = function(spec) {
that.create_error_link(container);
};
that.update = function(values) {
that.update = function(value) {
var key = values && values.length ? values[0] : null;
var key = value[0];
if (!key || key === '') {
key = {};
@@ -4821,9 +5023,7 @@ IPA.sshkey_widget = function(spec) {
};
that.save = function() {
var value = that.key.key;
value = value ? [value] : [''];
return value;
return that.key;
};
that.update_link = function() {

View File

@@ -239,7 +239,17 @@ test("Testing type target.", function() {
same(target_widget.target, 'type', 'type selected');
$("input[type=checkbox]").attr("checked",true);
var attrs_w = target_widget.widgets.get_widget('attrs');
var options = attrs_w.options;
ok(options.length > 0, "Attrs has some options");
// check them all
var values = [];
for (var i=0,l=options.length; i<l; i++) {
values.push(options[i].value);
}
attrs_w.update(values);
attrs_w.emit('value-change', { source: attrs_w });
var record = {};
target_facet.save(record);
@@ -250,8 +260,7 @@ test("Testing type target.", function() {
'ipapermtarget', 'memberof', 'attrs'],
'type and attrs rows visible');
ok((record.attrs.length > 10),
"response length shows some attrs set");
same(record.attrs.length, options.length, "response contains all checked attrs");
});

View File

@@ -560,6 +560,7 @@
"error": "Text does not match field pattern",
"datetime": "Must be an UTC date/time value (e.g., \"2014-01-20 17:58:01Z\")",
"decimal": "Must be a decimal number",
"format": "Format error",
"integer": "Must be an integer",
"ip_address": "Not a valid IP address",
"ip_v4_address": "Not a valid IPv4 address",
@@ -567,6 +568,7 @@
"max_value": "Maximum value is ${value}",
"min_value": "Minimum value is ${value}",
"net_address": "Not a valid network address",
"parse": "Parse error",
"port": "'${port}' is not a valid port",
"required": "Required field",
"unsupported": "Unsupported value"

View File

@@ -24,12 +24,12 @@ define([
'freeipa/jquery',
'freeipa/details',
'freeipa/facet',
'freeipa/field',
'freeipa/reg',
'freeipa/rpc',
'freeipa/entity',
'freeipa/field',
'freeipa/widget'],
function(md, IPA, $, mod_details, mod_facet, reg, rpc) {
function(md, IPA, $, mod_details, mod_facet, mod_field, reg, rpc) {
return function() {
var details_container;
@@ -41,6 +41,7 @@ module('details', {
mod_facet.register();
mod_details.register();
mod_field.register();
IPA.init({
url: 'data',
@@ -255,7 +256,10 @@ test("Testing details lifecycle: create, load.", function(){
ok (load_called, 'load manager called');
var field = facet.fields.get_field('test');
field.set_dirty(true);
field.set_value("foo");
var widget = facet.widgets.get_widget('contact.test');
// simulate user change
widget.emit('value-change', { source: widget, value: "foo" });
facet.update(
function(){update_success_called = true;},

View File

@@ -22,9 +22,10 @@ define([
'freeipa/ipa',
'freeipa/jquery',
'freeipa/datetime',
'freeipa/util',
'freeipa/field',
'freeipa/widget'],
function(IPA, $, datetime) { return function() {
function(IPA, $, datetime, util) { return function() {
var old;
@@ -143,6 +144,23 @@ test('Testing IPA.defined', function() {
same(IPA.defined(null), false, 'null');
});
test('Testing util.equals', function() {
ok(util.equals([], []), 'Empty Arrays');
ok(util.equals([1, "a", false, true], [1, "a", false, true]), 'Arrays');
ok(util.equals(true, true), 'Boolean: true');
ok(util.equals(false, false), 'Boolean: false');
ok(!util.equals(true, false), 'Negative: boolean');
ok(!util.equals(false, true), 'Negative: boolean');
ok(util.equals("abc", "abc"), 'Positive: strings');
ok(!util.equals("abc", "aBC"), 'Negative: string casing');
ok(util.equals(1, 1), 'Positive: number');
ok(util.equals(1.0, 1), 'Positive: number');
ok(util.equals(2.2, 2.2), 'Positive: number');
ok(!util.equals([], [""]), 'Negative: empty array');
});
test('Testing datetime', function() {
var valid = [

View File

@@ -18,9 +18,15 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
define(['freeipa/ipa', 'freeipa/jquery', 'freeipa/field', 'freeipa/widget',
'freeipa/entity'],
function(IPA, $) { return function() {
define([
'dojo/on',
'freeipa/ipa',
'freeipa/jquery',
'freeipa/group',
'freeipa/field',
'freeipa/widget',
'freeipa/entity'
], function(on, IPA, $, group) { return function() {
var widget_container;
var widget;
@@ -43,6 +49,7 @@ module('widget',{
factory = null;
spec = null;
group.register();
},
teardown: function() {
@@ -119,23 +126,28 @@ function text_tests(widget,input){
function multivalued_text_tests(widget) {
var values = ['val1', 'val2', 'val3'];
var changed = false;
function on_change (event) {
changed = true;
}
on(widget, 'value-change', on_change);
widget.update(values);
same(widget.save(), values, "All values loaded");
same(widget.test_dirty(), false, "Field initially clean");
values = ['val1', 'val2', 'val3', 'val4'];
widget.add_row(['val4']);
same(widget.save(), values, "Value added");
same(widget.test_dirty(), true, "Field is dirty");
ok(changed, "Value changed");
changed = false;
values = ['val1', 'val3', 'val4'];
widget.remove_row(widget.rows[1]);
same(widget.save(), values, "Value removed");
same(widget.test_dirty(), true, "Field is dirty");
ok(changed, "Value changed");
changed = false;
}
test("IPA.table_widget" ,function(){
@@ -294,7 +306,10 @@ test("IPA.entity_link_widget" ,function(){
factory = IPA.link_widget;
spec = {
name: 'gidnumber',
other_entity:'group'
other_entity:'group',
other_pkeys: function() {
return ['kfrog'];
}
};
base_widget_test(widget,'user','test_value');
@@ -304,8 +319,6 @@ test("IPA.entity_link_widget" ,function(){
}
};
var mock_record = { uid: ['kfrog'], gidnumber: ['123456']};
widget.entity = mock_entity;
widget.create(widget_container);
@@ -315,7 +328,7 @@ test("IPA.entity_link_widget" ,function(){
ok(nonlink.length > 1);
ok(link.length > 1);
widget.is_link = true; //setting is_link is responsibility of field
var mock_record = { gidnumber: ['123456']};
widget.update(mock_record.gidnumber);
link = widget_container.find('a:contains("123456")');

View File

@@ -696,6 +696,7 @@ class i18n_messages(Command):
"error": _("Text does not match field pattern"),
"datetime": _("Must be an UTC date/time value (e.g., \"2014-01-20 17:58:01Z\")"),
"decimal": _("Must be a decimal number"),
"format": _("Format error"),
"integer": _("Must be an integer"),
"ip_address": _('Not a valid IP address'),
"ip_v4_address": _('Not a valid IPv4 address'),
@@ -703,6 +704,7 @@ class i18n_messages(Command):
"max_value": _("Maximum value is ${value}"),
"min_value": _("Minimum value is ${value}"),
"net_address": _("Not a valid network address"),
"parse": _("Parse error"),
"port": _("'${port}' is not a valid port"),
"required": _("Required field"),
"unsupported": _("Unsupported value"),