summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPetr Vobornik <pvoborni@redhat.com>2013-11-13 15:49:25 +0100
committerPetr Vobornik <pvoborni@redhat.com>2014-04-03 12:40:37 +0200
commit0d05a50e19b71cade636d9ca4882e453f614a78c (patch)
tree8b7fee3645c6c08f0a90be334ecd11543a6c2f91
parent66fb4d5e849a049e95d3ef4fcf2b86217488634d (diff)
downloadfreeipa-0d05a50e19b71cade636d9ca4882e453f614a78c.tar.gz
freeipa-0d05a50e19b71cade636d9ca4882e453f614a78c.tar.xz
freeipa-0d05a50e19b71cade636d9ca4882e453f614a78c.zip
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>
-rw-r--r--install/ui/doc/categories.json26
-rw-r--r--install/ui/less/forms-override.less22
-rw-r--r--install/ui/src/freeipa/FieldBinder.js336
-rw-r--r--install/ui/src/freeipa/_base/Builder.js2
-rw-r--r--install/ui/src/freeipa/_base/Provider.js8
-rw-r--r--install/ui/src/freeipa/_base/debug.js41
-rw-r--r--install/ui/src/freeipa/aci.js23
-rw-r--r--install/ui/src/freeipa/automember.js31
-rwxr-xr-xinstall/ui/src/freeipa/certificate.js7
-rw-r--r--install/ui/src/freeipa/details.js13
-rw-r--r--install/ui/src/freeipa/dialog.js9
-rw-r--r--install/ui/src/freeipa/dns.js115
-rw-r--r--install/ui/src/freeipa/field.js1142
-rw-r--r--install/ui/src/freeipa/host.js11
-rw-r--r--install/ui/src/freeipa/rule.js2
-rw-r--r--install/ui/src/freeipa/service.js60
-rw-r--r--install/ui/src/freeipa/user.js6
-rw-r--r--install/ui/src/freeipa/util.js338
-rw-r--r--install/ui/src/freeipa/widget.js386
-rw-r--r--install/ui/test/aci_tests.js15
-rw-r--r--install/ui/test/data/ipa_init.json2
-rw-r--r--install/ui/test/details_tests.js10
-rw-r--r--install/ui/test/utils_tests.js20
-rw-r--r--install/ui/test/widget_tests.js35
-rw-r--r--ipalib/plugins/internal.py2
25 files changed, 1741 insertions, 921 deletions
diff --git a/install/ui/doc/categories.json b/install/ui/doc/categories.json
index 2634986f2..ec0776687 100644
--- a/install/ui/doc/categories.json
+++ b/install/ui/doc/categories.json
@@ -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,11 +181,23 @@
{
"name": "Fields",
"classes": [
- "IPA.field",
+ "field.field",
"*_field"
]
},
{
+ "name": "Binders",
+ "classes": [
+ "*Binder"
+ ]
+ },
+ {
+ "name": "Adapters",
+ "classes": [
+ "*Adapter"
+ ]
+ },
+ {
"name": "Formatters",
"classes": [
"IPA.formatter",
@@ -193,7 +207,7 @@
{
"name": "Validators",
"classes": [
- "IPA.validator",
+ "field.validator",
"*_validator"
]
}
diff --git a/install/ui/less/forms-override.less b/install/ui/less/forms-override.less
index b8c2e5d35..b987ed59f 100644
--- a/install/ui/less/forms-override.less
+++ b/install/ui/less/forms-override.less
@@ -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);
+ }
+}
diff --git a/install/ui/src/freeipa/FieldBinder.js b/install/ui/src/freeipa/FieldBinder.js
new file mode 100644
index 000000000..ed05d2531
--- /dev/null
+++ b/install/ui/src/freeipa/FieldBinder.js
@@ -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;
+});
diff --git a/install/ui/src/freeipa/_base/Builder.js b/install/ui/src/freeipa/_base/Builder.js
index e487aa542..890a98a49 100644
--- a/install/ui/src/freeipa/_base/Builder.js
+++ b/install/ui/src/freeipa/_base/Builder.js
@@ -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;
diff --git a/install/ui/src/freeipa/_base/Provider.js b/install/ui/src/freeipa/_base/Provider.js
index b2e55aed3..fddb45aba 100644
--- a/install/ui/src/freeipa/_base/Provider.js
+++ b/install/ui/src/freeipa/_base/Provider.js
@@ -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);
}
diff --git a/install/ui/src/freeipa/_base/debug.js b/install/ui/src/freeipa/_base/debug.js
new file mode 100644
index 000000000..1332aa7dd
--- /dev/null
+++ b/install/ui/src/freeipa/_base/debug.js
@@ -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
+ };
+}); \ No newline at end of file
diff --git a/install/ui/src/freeipa/aci.js b/install/ui/src/freeipa/aci.js
index 9aab2d7ec..0615184c0 100644
--- a/install/ui/src/freeipa/aci.js
+++ b/install/ui/src/freeipa/aci.js
@@ -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');
-
- type_select.value_changed.attach(function() {
- that.apply_type();
- });
+ var type_f = that.container.fields.get_field('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);
};
diff --git a/install/ui/src/freeipa/automember.js b/install/ui/src/freeipa/automember.js
index e9619b731..ae7304d95 100644
--- a/install/ui/src/freeipa/automember.js
+++ b/install/ui/src/freeipa/automember.js
@@ -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,28 +448,26 @@ 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);
+ return that;
+};
- that.attr_name = spec.attribute || that.name;
-
- that.load = function(record) {
-
- var regexes = record[that.attr_name];
- that.values = [];
+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]);
- that.values.push(condition);
+ values.push(condition);
}
}
-
- that.load_writable(record);
- that.reset();
- };
-
- return that;
-};
+ return values;
+ }
+});
IPA.automember.condition_widget = function(spec) {
diff --git a/install/ui/src/freeipa/certificate.js b/install/ui/src/freeipa/certificate.js
index 7a7ece0e1..c2e229302 100755
--- a/install/ui/src/freeipa/certificate.js
+++ b/install/ui/src/freeipa/certificate.js
@@ -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() {
diff --git a/install/ui/src/freeipa/details.js b/install/ui/src/freeipa/details.js
index 8b3eddb1f..279b76a1a 100644
--- a/install/ui/src/freeipa/details.js
+++ b/install/ui/src/freeipa/details.js
@@ -591,6 +591,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
*/
that.create = function(container) {
@@ -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;
diff --git a/install/ui/src/freeipa/dialog.js b/install/ui/src/freeipa/dialog.js
index 941ff8a29..39c48efd3 100644
--- a/install/ui/src/freeipa/dialog.js
+++ b/install/ui/src/freeipa/dialog.js
@@ -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;
diff --git a/install/ui/src/freeipa/dns.js b/install/ui/src/freeipa/dns.js
index 1432eb910..5b8c5c090 100644
--- a/install/ui/src/freeipa/dns.js
+++ b/install/ui/src/freeipa/dns.js
@@ -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,69 +2046,34 @@ IPA.dns.record_type_table_widget = function(spec) {
IPA.dns.netaddr_field = function(spec) {
spec = spec || {};
+ spec.adapter = IPA.dns.netaddr_adapter;
+ var that = IPA.field(spec);
+ return that;
+};
- 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;
+IPA.dns.netaddr_adapter = declare([field_mod.Adapter], {
- //compare values in array
- if (values.length !== that.values.length) return true;
+ separator: ';',
- for (var i=0; i<values.length; i++) {
- if (values[i] != that.values[i]) {
- return true;
+ 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;
+ },
- 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;
+ save: function(value, record) {
+ if (value[0]) {
+ value = [value.join(this.separator)];
}
-
- return [new_val];
- };
-
- that.validate = function() {
-
- var values = that.field_save();
-
- return that.validate_core(values);
- };
-
- return that;
-};
+ return this.inherited(arguments, [value, record]);
+ }
+});
IPA.dns.record_modify_column = function(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);
diff --git a/install/ui/src/freeipa/field.js b/install/ui/src/freeipa/field.js
index ab04fcacf..52cd2b18f 100644
--- a/install/ui/src/freeipa/field.js
+++ b/install/ui/src/freeipa/field.js
@@ -24,7 +24,9 @@
define([
'dojo/_base/array',
+ 'dojo/_base/declare',
'dojo/_base/lang',
+ 'dojo/Evented',
'./metadata',
'./builder',
'./datetime',
@@ -34,25 +36,31 @@ define([
'./phases',
'./reg',
'./rpc',
- './text'],
- function(array, lang, metadata_provider, builder, datetime, IPA, $,
- navigation, phases, reg, rpc, text) {
+ './text',
+ './util',
+ './FieldBinder'],
+ function(array, declare, lang, Evented, metadata_provider, builder, datetime,
+ IPA, $, navigation, phases, reg, rpc, text, util, FieldBinder) {
/**
* Field module
- * @class field
+ *
+ * Contains basic fields, adapters and validators.
+ *
+ * @class
* @singleton
*/
-var exp = {};
+var field = {};
/**
* Field
- * @class IPA.field
+ * @class
+ * @alternateClassName IPA.field
*/
-IPA.field = function(spec) {
+field.field = IPA.field = function(spec) {
spec = spec || {};
- var that = IPA.object();
+ var that = new Evented();
/**
* Entity
@@ -70,7 +78,7 @@ IPA.field = function(spec) {
* Container
* @property {facet.facet|IPA.dialog}
*/
- that.container = null;
+ that.container = spec.container;
/**
* Name
@@ -116,22 +124,48 @@ IPA.field = function(spec) {
that.measurement_unit = spec.measurement_unit;
/**
- * Formatter
+ * Data parser
+ *
+ * - transforms datasource value to field value
+ * @property {IPA.formatter}
+ */
+ that.data_parser = builder.build('formatter', spec.data_parser);
+
+ /**
+ * Data formatter
+ *
+ * - formats field value to datasource value
+ *
+ * @property {IPA.formatter}
+ */
+ that.data_formatter = builder.build('formatter', spec.data_formatter);
+
+ /**
+ * UI parser
+ *
+ * - formats widget value to field value
*
- * - transforms field value to widget value
- * - use corresponding output_formatter if field is not read-only and
- * backend can't handle the different format
* @property {IPA.formatter}
*/
- that.formatter = builder.build('formatter', spec.formatter);
+ that.ui_parser = builder.build('formatter', spec.ui_parser);
/**
- * Output formatter
+ * UI formatter
+ *
+ * - formats field value to widget value
+ * - in spec one can use also `formatter` instead of `ui_formatter`
*
- * - transforms widget value into value for backend
* @property {IPA.formatter}
*/
- that.output_formatter = builder.build('formatter', spec.output_formatter);
+ that.ui_formatter = builder.build('formatter', spec.ui_formatter || spec.formatter);
+
+
+ /**
+ * Adapter whích selected values from record on load.
+ *
+ * @property {field.Adapter}
+ */
+ that.adapter = builder.build('adapter', spec.adapter || 'adapter', { context: that });
/**
* Widget
@@ -205,10 +239,20 @@ IPA.field = function(spec) {
that.priority = spec.priority;
/**
- * Loaded values
+ * Loaded value
+ *
+ * - currently value is supposed to be an Array. This might change in a
+ * future.
+ *
* @property {Array.<Object>}
*/
- that.values = [];
+ that.value = [];
+
+ /**
+ * Default value
+ * @property {Mixed}
+ */
+ that.default_value = null;
/**
* Field is dirty (value is modified)
@@ -230,6 +274,20 @@ IPA.field = function(spec) {
*/
that.dirty_changed = IPA.observer();
+ /**
+ * Last validation result
+ * @property {Object}
+ */
+ that.validation_result = null;
+
+ /**
+ * Controls if field should perform validation when it's not supposed to
+ * be edited by user (`is_editable()`).
+ * @property {boolean}
+ */
+ that.validate_noneditable = spec.validate_noneditable !== undefined ?
+ spec.validate_noneditable : false;
+
var init = function() {
if (typeof that.metadata === 'string') {
that.metadata = metadata_provider.get(that.metadata);
@@ -246,6 +304,7 @@ IPA.field = function(spec) {
}
}
+ that.set_value([], true); // default value
that.validators.push(IPA.metadata_validator());
};
@@ -263,107 +322,109 @@ IPA.field = function(spec) {
/**
* Required setter
+ *
+ * Note that final required state also depends on `read_only` and
+ * `writable` states.
+ *
* @param {boolean} required
*/
that.set_required = function(required) {
+ var old = that.is_required();
that.required = required;
+ var current = that.is_required();
- that.update_required();
- };
-
- /**
- * Update required state in widget to match field's
- * @protected
- */
- that.update_required = function() {
- if(that.widget && that.widget.set_required) {
- that.widget.set_required(that.is_required());
+ if (current !== old) {
+ that.emit('require-change', { source: that, required: current });
}
};
/**
- * Check if value is set when it has to be. Show error if not.
- * @return {boolean}
+ * Check if value is set when it has to be. Report if not.
+ * @return {boolean} value passes the require check
*/
that.validate_required = function() {
- var values = that.save();
- if (IPA.is_empty(values) && that.is_required() && that.enabled) {
- that.valid = false;
- var message = text.get('@i18n:widget.validation.required',
+ var values = that.get_value();
+ var result = { valid: true, message: null };
+ if ((that.validate_noneditable || that.is_editable()) &&
+ util.is_empty(values) && that.is_required()) {
+ result.valid = false;
+ result.message = text.get('@i18n:widget.validation.required',
"Required field");
- that.show_error(message);
- return false;
+ that.set_valid(result);
}
- return true;
+ return result.valid;
};
/**
- * Returns true and clears the error message if the field value passes
- * the validation pattern. If the field value does not pass validation,
- * displays the error message and returns false.
- * @return {boolean}
+ * Validates the field.
+ * Sets the result by `set_valid` call.
+ * @return {boolean} field is valid
*/
that.validate = function() {
- that.hide_error();
- that.valid = true;
-
- if (!that.enabled) return that.valid;
-
- var values = that.get_widget_values();
-
- if (IPA.is_empty(values)) {
- return that.valid;
- }
-
- var value = values[0];
- for (var i=0; i<that.validators.length; i++) {
- var validation_result = that.validators[i].validate(value, that);
- that.valid = validation_result.valid;
- if (!that.valid) {
- that.show_error(validation_result.message);
- break;
+ var result = { valid: true, message: null, errors: [], results: []};
+ var values = that.get_value();
+
+ if ((that.validate_noneditable || that.is_editable()) && !util.is_empty(values)) {
+
+ // validate all values
+ for (var i=0, il=values.length; i<il; i++) {
+ for (var j=0, jl=that.validators.length; j<jl; j++) {
+ var res = that.validators[j].validate(values[i], that);
+ result.results[i] = res;
+ if (!res.valid) {
+ result.valid = false;
+ result.errors[i] = res;
+ // set error message only for first error
+ if (!result.message) result.message = res.message;
+ break; // report only one error per value
+ }
+ }
}
}
- return that.valid;
+ that.set_valid(result);
+ return result.valid;
};
/**
- * This function stores the entire record and the values
- * of the field, then invoke `reset()` to update the UI.
+ * Set valid state and validation error message
+ * @param {Object|null} result Validation result
+ * @fires valid-change
*/
- that.load = function(record) {
- that.record = record;
-
- that.values = that.get_value(record, that.param);
+ that.set_valid = function(result) {
- that.load_writable(record);
+ var old_result = that.validation_result;
+ that.valid = result.valid;
+ that.validation_result = result;
- that.reset();
+ if (!util.equals(old_result, result)) {
+ that.emit('valid-change', {
+ source: that,
+ valid: result.valid,
+ result: result
+ });
+ }
};
/**
- * Get value of attribute with given name from record (during `load`)
- *
- * @protected
- * @param {Object} record
- * @param {string} name
- * @return {Array} array of values
+ * This function calls adapter to get value from record and date_parser to
+ * process it. The it sets is as `value`.
*/
- that.get_value = function(record, name) {
-
- var value = record[name];
+ that.load = function(record) {
- if (!(value instanceof Array)) {
- value = value !== undefined ? [value] : [];
+ var value = that.adapter.load(record);
+ var parsed = util.parse(that.data_parser, value, "Parse error:"+that.name);
+ value = parsed.value;
+ if (!parsed.ok) {
+ window.console.warn(parsed.message);
}
- if (!value.length) {
- value = [''];
- }
+ // this call is quite application specific and should be moved to
+ // different component
+ that.load_writable(record);
- return value;
+ that.set_value(value, true);
};
/**
@@ -386,15 +447,15 @@ IPA.field = function(spec) {
*/
that.load_writable = function(record) {
- that.writable = true;
+ var writable = true;
if (that.metadata) {
if (that.metadata.primary_key) {
- that.writable = false;
+ writable = false;
}
if (that.metadata.flags && array.indexOf(that.metadata.flags, 'no_update') > -1) {
- that.writable = false;
+ writable = false;
}
}
@@ -411,44 +472,62 @@ IPA.field = function(spec) {
// For all others, lack of rights means no write.
if ((!rights && !(that.flags.indexOf('w_if_no_aci') > -1 && write_oc)) ||
(rights && rights.indexOf('w') < 0)) {
- that.writable = false;
+ writable = false;
}
}
+
+ that.set_writable(writable);
};
/**
- * Reset field and widget to loaded values
+ * Set writable
+ * @fires writable-change
+ * @param {boolean} writable
*/
- that.reset = function() {
- that.set_widget_flags();
- that.update_required();
- that.update();
- that.validate();
- that.set_dirty(false);
+ that.set_writable = function(writable) {
+
+ var old = !!that.writable;
+ that.writable = writable;
+ if (old !== writable) {
+ that.emit('writable-change', { source: that, writable: writable });
+ }
+
+ that.set_required(that.required); // force update of required
};
/**
- * Update widget with loaded values.
+ * Set read only
+ * @fires readonly-change
+ * @param {boolean} writable
*/
- that.update = function() {
+ that.set_read_only = function(read_only) {
- if (!that.widget || !that.widget.update) return;
+ var old = !!that.read_only;
+ that.read_only = read_only;
+ if (old !== read_only) {
+ that.emit('readonly-change', { source: that, readonly: read_only });
+ }
+ that.set_required(that.required); // force update of required
+ };
- var formatted_values;
+ /**
+ * Get if field is intended to be edited
+ *
+ * It's a combination of `enabled`, 'writable` and `read_only` state.
+ *
+ * @returns {Boolean}
+ */
+ that.is_editable = function() {
- // Change loaded value to human readable value
- if (that.formatter) {
- formatted_values = [];
- for (var i=0; that.values && i<that.values.length; i++) {
- var value = that.values[i];
- var formatted_value = that.formatter.format(value);
- formatted_values.push(formatted_value);
- }
- } else {
- formatted_values = that.values;
- }
+ return that.enabled && that.writable && !that.read_only;
+ };
- that.widget.update(formatted_values);
+ /**
+ * Reset field and widget to loaded values
+ */
+ that.reset = function() {
+ that.emit('reset', { source: that });
+ that.set_value(that.get_pristine_value(), true);
};
/**
@@ -461,7 +540,7 @@ IPA.field = function(spec) {
that.get_update_info = function() {
var update_info = IPA.update_info_builder.new_update_info();
- if (that.is_dirty()) {
+ if (that.dirty) {
var values = that.save();
var field_info = IPA.update_info_builder.new_field_info(that, values);
update_info.fields.push(field_info);
@@ -470,27 +549,95 @@ IPA.field = function(spec) {
};
/**
- * This function saves the values entered in the UI.
- * It returns the values in an array, or null if
- * the field should not be saved.
+ * Prepare value for persistor.
+ *
+ * Sets `record[param]` option if `record` is supplied.
+ *
+ * Returns `['']` when disabled. Otherwise value formatted by
+ * `data_formatter` and `adapter`.
+ *
+ * @param {Object} [record]
* @return {Array} values
*/
that.save = function(record) {
- var values = that.values;
+ if (!that.enabled) return ['']; // not pretty, maybe leave it for caller
- if (!that.enabled) return [''];
+ var value = that.get_value();
+ var formatted = util.format(that.data_formatter, value);
+ if (formatted.ok) {
+ value = formatted.value;
+ } else {
+ window.console.warn('Output data format error:\n'+
+ JSON.stringify(formatted));
+ }
- if (that.widget) {
- values = that.get_widget_values();
- values = that.format_output(values);
+ var diff = that.adapter.save(value, record);
+ value = diff[that.param]; // a hack which should be removed. This
+ // function should not return any value. But
+ // current consumers expect it.
+ return value;
+ };
+
+ /**
+ * Get field's value
+ *
+ * Returns pure value; doesn't use any formatter.
+ *
+ * @returns {Mixed} field's value
+ */
+ that.get_value = function() {
+ return that.value;
+ };
+
+ /**
+ * Set value
+ *
+ * Always raises value-change when setting pristine value
+ *
+ * @param {Mixed} value
+ * @param {boolean} pristine - value is pristine
+ * @fires value-change
+ * @fires dirty-change
+ */
+ that.set_value = function(value, pristine) {
+
+ that.set_previous_value(that.value);
+ that.value = value;
+
+ if (util.dirty(that.value, that.previous_value, that.get_dirty_check_options()) ||
+ pristine) {
+ that.emit('value-change', {
+ source: that,
+ value: that.value,
+ previous: that.previous_value
+ });
}
- if (record) {
- record[that.param] = values;
+ var dirty = false;
+ if (pristine) {
+ that.set_pristine_value(value);
+ } else {
+ dirty = that.test_dirty();
}
+ that.set_dirty(dirty);
+ that.validate();
+ };
- return values;
+ that.get_previous_value = function() {
+ return that.previous_value;
+ };
+
+ that.set_previous_value = function(value) {
+ that.previous_value = value;
+ };
+
+ that.get_pristine_value = function() {
+ return that.pristine_value;
+ };
+
+ that.set_pristine_value = function(value) {
+ that.pristine_value = value;
};
/**
@@ -509,26 +656,6 @@ IPA.field = function(spec) {
};
/**
- * Use output formatter to transform value entered into UI to
- * value used by backend
- *
- * @param {Array} values
- * @return {Array} formatted values
- */
- that.format_output = function(values) {
-
- if (that.output_formatter) {
- var formatted_values = [];
- for (var i=0; values && i<values.length; i++) {
- var formatted_value = that.output_formatter.format(values[i]);
- formatted_values.push(formatted_value);
- }
- return formatted_values;
- }
- return values;
- };
-
- /**
* This function compares the original values and the
* values entered in the UI. If the values have changed
* it will return true.
@@ -537,52 +664,25 @@ IPA.field = function(spec) {
*/
that.test_dirty = function() {
+ // remove? this check should part of container which cares, the
+ // field should not care
if (that.read_only || !that.writable) return false;
- var values = that.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;
+ var pristine = that.get_pristine_value();
+ var value = that.get_value();
- //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;
-
- return !that.dirty_are_equal(that.values, values);
+ return util.dirty(value, pristine, that.get_dirty_check_options());
};
/**
- * Compares values in two arrays
- * @protected
- * @param {Array} orig_vals
- * @param {Array} new_vals
- * @return {boolean} values are equal
+ * Returns options for dirty check
+ * @returns {Object}
*/
- that.dirty_are_equal = function(orig_vals, new_vals) {
-
- orig_vals.sort();
- new_vals.sort();
-
- for (var i=0; i<orig_vals.length; i++) {
- if (orig_vals[i] !== new_vals[i]) {
- return false;
- }
- }
+ that.get_dirty_check_options = function() {
- return true;
- };
-
- /**
- * Getter for `dirty`
- * @return {boolean}
- */
- that.is_dirty = function() {
- return that.dirty;
+ return {
+ unordered: !that.ordered
+ };
};
/**
@@ -592,42 +692,10 @@ IPA.field = function(spec) {
that.set_dirty = function(dirty) {
var old = that.dirty;
that.dirty = dirty;
- if (that.undo) {
- that.show_undo(dirty);
- }
if (old !== dirty) {
that.dirty_changed.notify([], that);
- }
- };
-
-
- /**
- * Display validation error
- * @protected
- * @param {string} message
- */
- that.show_error = function(message) {
- if (that.widget && that.widget.show_error) that.widget.show_error(message);
- };
-
- /**
- * Hide validation error
- * @protected
- */
- that.hide_error = function() {
- if (that.widget && that.widget.hide_error) that.widget.hide_error();
- };
-
- /**
- * Show/hide undo button
- * @protected
- * @param {boolean} value true:show, false:hide
- */
- that.show_undo = function(value) {
- if (that.widget && that.widget.show_undo) {
- if(value) { that.widget.show_undo(); }
- else { that.widget.hide_undo(); }
+ that.emit('dirty-change', { source: that, dirty: dirty });
}
};
@@ -636,32 +704,10 @@ IPA.field = function(spec) {
* @param {boolean} value
*/
that.set_enabled = function(value) {
+ var old = !!that.enabled;
that.enabled = value;
- if (that.widget && that.widget.set_enabled) {
- that.widget.set_enabled(value);
- }
- };
-
- /**
- * Subject to removal
- * @deprecated
- */
- that.refresh = function() {
- };
-
- /**
- * Reflect `label`, `tooltip`, `measurement_unit`, `undo`, `writable`,
- * `read_only` into widget.
- */
- that.set_widget_flags = function() {
-
- if (that.widget) {
- if (that.label) that.widget.label = that.label;
- if (that.tooltip) that.widget.tooltip = that.tooltip;
- if (that.measurement_unit) that.widget.measurement_unit = that.measurement_unit;
- that.widget.undo = that.undo;
- that.widget.writable = that.writable;
- that.widget.read_only = that.read_only;
+ if (old !== that.enabled) {
+ that.emit('enable-change', { source: that, enabled: that.enabled });
}
};
@@ -671,30 +717,13 @@ IPA.field = function(spec) {
that.widgets_created = function() {
that.widget = that.container.widgets.get_widget(that.widget_name);
-
- if(that.widget) {
- that.set_widget_flags();
-
- that.widget.value_changed.attach(that.widget_value_changed);
- that.widget.undo_clicked.attach(that.widget_undo_clicked);
+ if (that.widget) {
+ that._binder = new FieldBinder(that, that.widget);
+ that._binder.bind();
+ that._binder.copy_properties();
}
};
- /**
- * Handler for widget's `value_changed` event
- */
- that.widget_value_changed = function() {
- that.set_dirty(that.test_dirty());
- that.validate();
- };
-
- /**
- * Handler for widget's `undo_clicked` event
- */
- that.widget_undo_clicked = function() {
- that.reset();
- };
-
init();
// methods that should be invoked by subclasses
@@ -711,13 +740,91 @@ IPA.field = function(spec) {
};
/**
+ * Adapter's task is to select wanted data from record and vice-versa.
+ *
+ * This default adapter expects that context will be field and record
+ * will be FreeIPA JsonRPC result.
+ *
+ * @class
+ */
+field.Adapter = declare(null, {
+
+ /**
+ * Adapter's context; e.g., field
+ *
+ * @property {Object}
+ */
+ context: null,
+
+ /**
+ * Get single value from record
+ * @param {Object} record Record
+ * @param {string} name Attribute name
+ * @returns {Array} attribute value
+ * @protected
+ */
+ get_value: function(record, name) {
+ var value = record[name];
+ return util.normalize_value(value);
+ },
+
+ /**
+ * By default just select attribute with name defined by `context.param`
+ * from a record. Uses default value if value is not in record and context
+ * defines it.
+ * @param {Object} record
+ * @returns {Array} attribute value
+ */
+ load: function(record) {
+ var value = this.get_value(record, this.context.param);
+ if (util.is_empty(value) && !util.is_empty(this.context.default_value)) {
+ value = util.normalize_value(this.context.default_value);
+ }
+ return value;
+ },
+
+ /**
+ * Save value into record
+ *
+ * Default behavior is to save it as property which name is defined by
+ * contex's param.
+ * @param {Object} value Value to save
+ * @param {Object} record Record to save the value into
+ * @returns {Object} what was saved
+ */
+ save: function(value, record) {
+
+ var diff = {};
+ diff[this.context.param] = value;
+ if (record) {
+ lang.mixin(record, diff);
+ }
+ return diff;
+ },
+
+ constructor: function(spec) {
+ this.context = spec.context || {};
+ }
+});
+
+/**
* Validator
*
* - base class, always returns positive result
*
- * @class IPA.validator
+ * Result format
+ *
+ * - validation result is an object with mandatory `valid` property which
+ * has to be set to a boolean value. True if value is valid, false otherwise.
+ * - if `valid === false` result should also contain `message` property with
+ * human readable error text
+ * - it may contain also other properties; e.g., `errors` which contains an
+ * array with other validation result objects in case of complex validation.
+ *
+ * @class
+ * @alternateClassName IPA.validator
*/
-IPA.validator = function(spec) {
+field.validator = IPA.validator = function(spec) {
spec = spec || {};
@@ -768,10 +875,11 @@ IPA.validator = function(spec) {
*
* Validates value according to supplied metadata
*
- * @class IPA.metadata_validator
+ * @class
+ * @alternateClassName IPA.metadata_validator
* @extends IPA.validator
*/
-IPA.metadata_validator = function(spec) {
+field.metadata_validator = IPA.metadata_validator = function(spec) {
var that = IPA.validator(spec);
@@ -784,7 +892,7 @@ IPA.metadata_validator = function(spec) {
var metadata = context.metadata;
var number = false;
- if (!metadata || IPA.is_empty(value)) return that.true_result();
+ if (!metadata || util.is_empty(value)) return that.true_result();
if (metadata.type === 'int') {
number = true;
@@ -829,10 +937,11 @@ IPA.metadata_validator = function(spec) {
/**
* Checks if value is supported
*
- * @class IPA.unsupported_validator
+ * @class
+ * @alternateClassName IPA.unsupported_validator
* @extends IPA.validator
*/
-IPA.unsupported_validator = function(spec) {
+field.unsupported_validator = IPA.unsupported_validator = function(spec) {
spec.message = spec.message ||'@i18n:widgets.validation.unsupported';
@@ -849,7 +958,7 @@ IPA.unsupported_validator = function(spec) {
*/
that.validate = function(value, context) {
- if (IPA.is_empty(value)) return that.true_result();
+ if (util.is_empty(value)) return that.true_result();
if (that.unsupported.indexOf(value) > -1) return that.false_result();
@@ -864,10 +973,11 @@ IPA.unsupported_validator = function(spec) {
*
* - designed for password confirmation
*
- * @class IPA.same_password_validator
+ * @class
+ * @alternateClassName IPA.same_password_validator
* @extends IPA.validator
*/
-IPA.same_password_validator = function(spec) {
+field.same_password_validator = IPA.same_password_validator = function(spec) {
spec = spec || {};
@@ -887,7 +997,7 @@ IPA.same_password_validator = function(spec) {
*/
that.validate = function(value, context) {
- var other_field = context.container.fields.get_field(that.other_field);
+ var other_field = context.container.get_field(that.other_field);
var other_value = other_field.save();
var this_value = context.save();
@@ -900,48 +1010,25 @@ IPA.same_password_validator = function(spec) {
};
/**
- * Check if input value is a valid datetime
- *
- * @class IPA.datetime_validator
- * @extends IPA.validator
- */
-IPA.datetime_validator = function(spec) {
-
- spec = spec || {};
-
- var that = IPA.validator(spec);
-
- that.message = text.get(spec.message || '@i18n:widget.validation.datetime');
-
- /**
- * @inheritDoc
- */
- that.validate = function(value, context) {
-
- var valid = datetime.parse(value) !== null;
- if (!valid) return that.false_result();
-
- return that.true_result();
- };
-
- return that;
-};
-
-/**
* Used along with checkbox widget
*
- * @class IPA.checkbox_field
+ * @class
+ * @alternateClassName IPA.datetime_field
* @extends IPA.field
*/
-IPA.datetime_field = function(spec) {
+field.datetime_field = IPA.datetime_field = function(spec) {
spec = spec || {};
- spec.validators = spec.validators || ['datetime'];
- spec.output_formatter = spec.output_formatter || {
+ spec.data_formatter = spec.data_formatter || {
$type: 'datetime',
template: datetime.templates.generalized
};
- spec.formatter = spec.formatter || 'datetime';
+ spec.data_parser = spec.formatter || 'datetime';
+ spec.ui_formatter = spec.ui_formatter || spec.formatter || {
+ $type: 'datetime',
+ template: datetime.templates.human
+ };
+ spec.ui_parser = spec.ui_parser || 'datetime';
var that = IPA.field(spec);
return that;
@@ -950,56 +1037,18 @@ IPA.datetime_field = function(spec) {
/**
* Used along with checkbox widget
*
- * @class IPA.checkbox_field
+ * @class
+ * @alternateClassName IPA.checkbox_field
* @extends IPA.field
*/
-IPA.checkbox_field = function(spec) {
+field.checkbox_field = IPA.checkbox_field = function(spec) {
spec = spec || {};
+ spec.data_parser = 'boolean';
var that = IPA.field(spec);
/**
- * Check value by default
- * @property {boolean}
- */
- that.checked = spec.checked || false;
-
- /**
- * Boolean formatter for parsing loaded values.
- * @property {IPA.boolean_formatter}
- */
- that.boolean_formatter = IPA.boolean_formatter();
-
- /**
- * @inheritDoc
- */
- that.load = function(record) {
-
- that.record = record;
-
- that.values = that.get_value(record, that.param);
-
- var value = that.boolean_formatter.parse(that.values);
- if (value === '') value = that.widget.checked; //default value
-
- that.values = [value];
-
- that.load_writable(record);
-
- that.reset();
- };
-
- /**
- * @inheritDoc
- */
- that.widgets_created = function() {
-
- that.field_widgets_created();
- that.widget.checked = that.checked;
- };
-
- /**
* A checkbox will always have a value, so it's never required.
*
* @return {boolean} false
@@ -1008,33 +1057,17 @@ IPA.checkbox_field = function(spec) {
return false;
};
- that.checkbox_load = that.load;
-
- return that;
-};
-
-/**
- * Used along with checkboxes widget
- *
- * @class IPA.checkboxes_field
- * @extends IPA.field
- */
-IPA.checkboxes_field = function(spec) {
-
- spec = spec || {};
-
- var that = IPA.field(spec);
-
return that;
};
/**
* Used along with radio widget
*
- * @class IPA.radio_field
+ * @class
+ * @alternateClassName IPA.radio_field
* @extends IPA.field
*/
-IPA.radio_field = function(spec) {
+field.radio_field = IPA.radio_field = function(spec) {
spec = spec || {};
@@ -1048,125 +1081,55 @@ IPA.radio_field = function(spec) {
return false;
};
- /**
- * @inheritDoc
- */
- that.widgets_created = function() {
-
- that.field_widgets_created();
- };
-
return that;
};
/**
- * Used along with multivalued widget
+ * Used along with ssh key widget
+ *
+ * - by default has `w_if_no_aci` to workaround missing object class
*
- * @class IPA.multivalued_field
+ * @class
+ * @alternateClassName IPA.sshkeys_field
* @extends IPA.field
*/
-IPA.multivalued_field = function(spec) {
+field.sshkeys_field = IPA.sshkeys_field = function(spec) {
spec = spec || {};
+ spec.adapter = spec.adapter || field.SshKeysAdapter;
+ spec.flags = spec.flags || ['w_if_no_aci'];
var that = IPA.field(spec);
-
- /**
- * @inheritDoc
- */
- that.load = function(record) {
-
- that.field_load(record);
- };
-
- /**
- * @inheritDoc
- */
- that.test_dirty = function() {
- var dirty = that.field_test_dirty();
- dirty = dirty || that.widget.test_dirty(); //also checks order
- return dirty;
- };
-
- /**
- * @inheritDoc
- */
- that.validate = function() {
-
- var values = that.save();
-
- return that.validate_core(values);
- };
-
- /**
- * Validate each value separately.
- * @protected
- * @param {Array} values
- * @return {boolean} valid
- */
- that.validate_core = function(values) {
-
- that.hide_error();
- that.valid = true;
-
- if (IPA.is_empty(values)) {
- return that.valid;
- }
-
- for (var i=0; i<values.length; i++) {
-
- for (var j=0; j<that.validators.length; j++) {
-
- var validation_result = that.validators[j].validate(values[i], that);
- if (!validation_result.valid) {
- that.valid = false;
- var row_index = that.widget.get_saved_value_row_index(i);
- that.widget.show_child_error(row_index, validation_result.message);
- break;
- }
- }
- }
-
- return that.valid;
- };
-
return that;
};
/**
- * Used along with ssh key widget
- *
- * @class IPA.sshkeys_field
- * @extends IPA.multivalued_field
+ * SSH Keys Adapter
+ * @class
+ * @extends field.Adapter
*/
-IPA.sshkeys_field = function(spec) {
-
- spec = spec || {};
-
- var that = IPA.multivalued_field(spec);
+field.SshKeysAdapter = declare([field.Adapter], {
/**
- * By default has 'w_if_no_aci' flag.
+ * Transforms record into array of key, fingerprint pairs
*
- * - fixes upgrade issue. When attr rights are missing due to lack of
- * object class.
- */
- that.flags = spec.flags || ['w_if_no_aci'];
-
- /**
- * Name of fingerprint param
- * @property {string}
- */
- that.sshfp_attr = spec.sshfp_attr || 'sshpubkeyfp';
-
- /**
- * @inheritDoc
- */
- that.load = function(record) {
-
- var keys = that.get_value(record, that.param);
- var fingerprints = that.get_value(record, that.sshfp_attr);
-
+ * """
+ * // input:
+ * {
+ * 'ipasshpubkey': [ 'foo', 'foo1'],
+ * 'sshpubkeyfp': ['fooFP', 'fooFP2']
+ * }
+ *
+ * // output:
+ * [
+ * { key: 'foo', fingerprint: 'fooFP'},
+ * { key: 'foo1', fingerprint: 'fooFP2'},
+ * ]
+ * """
+ */
+ load: function(record) {
+ var keys = this.get_value(record, this.context.param);
+ var fingerprints = this.get_value(record, 'sshpubkeyfp');
var values = [];
if (keys.length === fingerprints.length) {
@@ -1181,148 +1144,24 @@ IPA.sshkeys_field = function(spec) {
values.push(value);
}
}
-
- that.values = values;
-
- that.load_writable(record);
-
- that.reset();
- };
+ return values;
+ },
/**
- * @inheritDoc
+ * Transforms array of pairs into array of keys and save it into record.
+ * @param {Array} values Source values
+ * @param {Object} record Target record.
+ * @returns {Array} saved value
*/
- that.dirty_are_equal = function(orig_vals, new_vals) {
+ save: function(values, record) {
- var i;
- var orig_keys = [];
-
- for (i=0; i<orig_vals.length; i++) {
- orig_keys.push(orig_vals[i].key);
+ var ret = [];
+ for (var i=0; i<values.length; i++) {
+ ret.push(values[i].key);
}
-
- return that.field_dirty_are_equal(orig_keys, new_vals);
- };
-
- return that;
-};
-
-/**
- * Used along with select widget
- *
- * @class IPA.select_field
- * @extends IPA.field
- */
-IPA.select_field = function(spec) {
-
- spec = spec || {};
-
- var that = IPA.field(spec);
-
- /**
- * @inheritDoc
- */
- that.widgets_created = function() {
-
- that.field_widgets_created();
- };
-
- return that;
-};
-
-/**
- * Used along with link widget
- *
- * @class IPA.link_field
- * @extends IPA.field
- */
-IPA.link_field = function(spec) {
-
- spec = spec || {};
-
- var that = IPA.field(spec);
-
- /**
- * Entity a link points to
- * @property {entity.entity}
- */
- that.other_entity = IPA.get_entity(spec.other_entity);
-
- function other_pkeys () {
- return that.facet.get_pkeys();
+ return this.inherited(arguments, [ret, record]);
}
-
- /**
- * Function which should return primary keys of link target in case of
- * link points to an entity.
- * @property {Function}
- */
- that.other_pkeys = spec.other_pkeys || other_pkeys;
-
- /**
- * Handler for widget `link_click` event
- */
- that.on_link_clicked = function() {
-
- navigation.show_entity(
- that.other_entity.name,
- 'default',
- that.other_pkeys());
- };
-
- /**
- * @inheritDoc
- */
- that.load = function(record) {
-
- that.field_load(record);
- that.check_entity_link();
- };
-
- /**
- * Check if entity exists
- *
- * - only if link points to an entity
- * - update widget's `is_link` 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.widget.is_link = false;
- that.widget.update(that.values);
- return;
- }
-
- rpc.command({
- entity: that.other_entity.name,
- method: 'show',
- args: that.other_pkeys(),
- options: {},
- retry: false,
- on_success: function(data) {
- that.widget.is_link = data.result && data.result.result;
- that.widget.update(that.values);
- },
- on_error: function() {
- that.widget.is_link = false;
- that.widget.update(that.values);
- }
- }).execute();
- };
-
- /**
- * @inheritDoc
- */
- that.widgets_created = function() {
- that.field_widgets_created();
- that.widget.link_clicked.attach(that.on_link_clicked);
- };
-
-
- return that;
-};
+});
/**
* Field for enabling/disabling entity
@@ -1330,10 +1169,11 @@ IPA.link_field = function(spec) {
* - expects radio widget
* - requires facet to use 'update_info' update method
*
- * @class IPA.enable_field
+ * @class
+ * @alternateClassName IPA.enable_field
* @extends IPA.field
*/
-IPA.enable_field = function(spec) {
+field.enable_field = IPA.enable_field = function(spec) {
spec = spec || {};
@@ -1390,9 +1230,10 @@ IPA.enable_field = function(spec) {
/**
* Collection of fields
- * @class IPA.field_container
+ * @class
+ * @alternateClassName IPA.field_container
*/
-IPA.field_container = function(spec) {
+field.field_container = IPA.field_container = function(spec) {
spec = spec || {};
@@ -1456,9 +1297,10 @@ IPA.field_container = function(spec) {
/**
* Old field builder
- * @class IPA.field_builder
+ * @class
+ * @alternateClassName IPA.field_builder
*/
-IPA.field_builder = function(spec) {
+field.field_builder = IPA.field_builder = function(spec) {
spec = spec || {};
@@ -1507,7 +1349,7 @@ IPA.field_builder = function(spec) {
* @member field
* @return spec
*/
-exp.pre_op = function(spec, context) {
+field.pre_op = function(spec, context) {
if (context.facet) spec.facet = context.facet;
if (context.entity) spec.entity = context.entity;
@@ -1520,7 +1362,7 @@ exp.pre_op = function(spec, context) {
* @member field
* @return obj
*/
-exp.post_op = function(obj, spec, context) {
+field.post_op = function(obj, spec, context) {
if (context.container) context.container.add_field(obj);
return obj;
@@ -1530,52 +1372,66 @@ exp.post_op = function(obj, spec, context) {
* Field builder with registry
* @member field
*/
-exp.builder = builder.get('field');
-exp.builder.factory = IPA.field;
-exp.builder.string_mode = 'property';
-exp.builder.string_property = 'name';
-reg.set('field', exp.builder.registry);
-exp.builder.pre_ops.push(exp.pre_op);
-exp.builder.post_ops.push(exp.post_op);
+field.builder = builder.get('field');
+field.builder.factory = field.field;
+field.builder.string_mode = 'property';
+field.builder.string_property = 'name';
+reg.set('field', field.builder.registry);
+field.builder.pre_ops.push(field.pre_op);
+field.builder.post_ops.push(field.post_op);
/**
* Validator builder with registry
* @member field
*/
-exp.validator_builder = builder.get('validator');
-reg.set('validator', exp.validator_builder.registry);
+field.validator_builder = builder.get('validator');
+reg.set('validator', field.validator_builder.registry);
+
+/**
+ * Adapter builder with registry
+ * @member field
+ */
+field.adapter_builder = builder.get('adapter');
+field.adapter_builder.post_ops.push(function(obj, spec, context) {
+ obj.context = context.context;
+ return obj;
+ }
+);
+reg.set('adapter', field.adapter_builder.registry);
/**
* Register fields and validators to global registry
* @member field
*/
-exp.register = function() {
+field.register = function() {
var f = reg.field;
var v = reg.validator;
-
- f.register('checkbox', IPA.checkbox_field);
- f.register('checkboxes', IPA.checkboxes_field);
- f.register('combobox', IPA.field);
- f.register('datetime', IPA.datetime_field);
- f.register('enable', IPA.enable_field);
- f.register('entity_select', IPA.field);
- f.register('field', IPA.field);
- f.register('link', IPA.link_field);
- f.register('multivalued', IPA.multivalued_field);
- f.register('password', IPA.field);
- f.register('radio', IPA.radio_field);
- f.register('select', IPA.select_field);
- f.register('sshkeys', IPA.sshkeys_field);
- f.register('textarea', IPA.field);
- f.register('text', IPA.field);
- f.register('value_map', IPA.field);
-
- v.register('metadata', IPA.metadata_validator);
- v.register('unsupported', IPA.unsupported_validator);
- v.register('same_password', IPA.same_password_validator);
- v.register('datetime', IPA.datetime_validator);
+ var l = reg.adapter;
+
+ f.register('checkbox', field.checkbox_field);
+ f.register('checkboxes', field.field);
+ f.register('combobox', field.field);
+ f.register('datetime', field.datetime_field);
+ f.register('enable', field.enable_field);
+ f.register('entity_select', field.field);
+ f.register('field', field.field);
+ f.register('link', field.field);
+ f.register('multivalued', field.field);
+ f.register('password', field.field);
+ f.register('radio', field.radio_field);
+ f.register('select', field.field);
+ f.register('sshkeys', field.sshkeys_field);
+ f.register('textarea', field.field);
+ f.register('text', field.field);
+ f.register('value_map', field.field);
+
+ v.register('metadata', field.metadata_validator);
+ v.register('unsupported', field.unsupported_validator);
+ v.register('same_password', field.same_password_validator);
+
+ l.register('adapter', field.Adapter);
};
-phases.on('registration', exp.register);
+phases.on('registration', field.register);
-return exp;
+return field;
});
diff --git a/install/ui/src/freeipa/host.js b/install/ui/src/freeipa/host.js
index c76b7ca8c..ab37b8771 100644
--- a/install/ui/src/freeipa/host.js
+++ b/install/ui/src/freeipa/host.js
@@ -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);
diff --git a/install/ui/src/freeipa/rule.js b/install/ui/src/freeipa/rule.js
index c262c6a86..c7bc8b0e9 100644
--- a/install/ui/src/freeipa/rule.js
+++ b/install/ui/src/freeipa/rule.js
@@ -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
diff --git a/install/ui/src/freeipa/service.js b/install/ui/src/freeipa/service.js
index 09880a937..bd1d3842b 100644
--- a/install/ui/src/freeipa/service.js
+++ b/install/ui/src/freeipa/service.js
@@ -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);
diff --git a/install/ui/src/freeipa/user.js b/install/ui/src/freeipa/user.js
index aee1b694e..e3ada9844 100644
--- a/install/ui/src/freeipa/user.js
+++ b/install/ui/src/freeipa/user.js
@@ -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;
}
diff --git a/install/ui/src/freeipa/util.js b/install/ui/src/freeipa/util.js
new file mode 100644
index 000000000..d6fbbf4f1
--- /dev/null
+++ b/install/ui/src/freeipa/util.js
@@ -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;
+});
diff --git a/install/ui/src/freeipa/widget.js b/install/ui/src/freeipa/widget.js
index a9b77694e..9b04acc91 100644
--- a/install/ui/src/freeipa/widget.js
+++ b/install/ui/src/freeipa/widget.js
@@ -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
@@ -128,6 +130,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
* @param {HTMLElement} container - Container node
@@ -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.
@@ -327,6 +356,13 @@ IPA.input_widget = function(spec) {
};
/**
+ * 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
* callback. If a callback isn't set, default callback is used. If
@@ -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 });
};
@@ -458,6 +527,38 @@ IPA.input_widget = function(spec) {
};
/**
+ * 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.length > 0) {
+ var error_link = that.get_error_link();
+ error_link.css('display', 'none');
+ error_link.html('');
+ } else {
+ that.show_error(result.message);
+ }
- if(that.rows[i].deleted) index++;
- if(i === index) return i;
+ } 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;
-
- if (row.original_values.length !== values.length) return true;
+ var value = row.widget.save();
- 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) {
+
+ that.value = util.normalize_value(values)[0] || '';
+ that.link.html(that.value);
+ that.nonlink.html(that.value);
- if (values || values.length > 0) {
- that.nonlink.text(values[0]);
- that.link.text(values[0]);
+ 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() {
diff --git a/install/ui/test/aci_tests.js b/install/ui/test/aci_tests.js
index 17db5b5d6..e82dd86df 100644
--- a/install/ui/test/aci_tests.js
+++ b/install/ui/test/aci_tests.js
@@ -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");
});
diff --git a/install/ui/test/data/ipa_init.json b/install/ui/test/data/ipa_init.json
index e7c58e66c..059726fea 100644
--- a/install/ui/test/data/ipa_init.json
+++ b/install/ui/test/data/ipa_init.json
@@ -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"
diff --git a/install/ui/test/details_tests.js b/install/ui/test/details_tests.js
index c6c33bd17..33d49aa47 100644
--- a/install/ui/test/details_tests.js
+++ b/install/ui/test/details_tests.js
@@ -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;},
diff --git a/install/ui/test/utils_tests.js b/install/ui/test/utils_tests.js
index b725f55eb..84424a2e1 100644
--- a/install/ui/test/utils_tests.js
+++ b/install/ui/test/utils_tests.js
@@ -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 = [
diff --git a/install/ui/test/widget_tests.js b/install/ui/test/widget_tests.js
index 5499f5529..d24ab4cbe 100644
--- a/install/ui/test/widget_tests.js
+++ b/install/ui/test/widget_tests.js
@@ -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")');
diff --git a/ipalib/plugins/internal.py b/ipalib/plugins/internal.py
index b20597ef2..a34d2cb16 100644
--- a/ipalib/plugins/internal.py
+++ b/ipalib/plugins/internal.py
@@ -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"),