diff options
author | Petr Vobornik <pvoborni@redhat.com> | 2013-11-13 15:49:25 +0100 |
---|---|---|
committer | Petr Vobornik <pvoborni@redhat.com> | 2014-04-03 12:40:37 +0200 |
commit | 0d05a50e19b71cade636d9ca4882e453f614a78c (patch) | |
tree | 8b7fee3645c6c08f0a90be334ecd11543a6c2f91 /install/ui/src/freeipa | |
parent | 66fb4d5e849a049e95d3ef4fcf2b86217488634d (diff) | |
download | freeipa-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>
Diffstat (limited to 'install/ui/src/freeipa')
-rw-r--r-- | install/ui/src/freeipa/FieldBinder.js | 336 | ||||
-rw-r--r-- | install/ui/src/freeipa/_base/Builder.js | 2 | ||||
-rw-r--r-- | install/ui/src/freeipa/_base/Provider.js | 8 | ||||
-rw-r--r-- | install/ui/src/freeipa/_base/debug.js | 41 | ||||
-rw-r--r-- | install/ui/src/freeipa/aci.js | 23 | ||||
-rw-r--r-- | install/ui/src/freeipa/automember.js | 31 | ||||
-rwxr-xr-x | install/ui/src/freeipa/certificate.js | 7 | ||||
-rw-r--r-- | install/ui/src/freeipa/details.js | 13 | ||||
-rw-r--r-- | install/ui/src/freeipa/dialog.js | 9 | ||||
-rw-r--r-- | install/ui/src/freeipa/dns.js | 115 | ||||
-rw-r--r-- | install/ui/src/freeipa/field.js | 1142 | ||||
-rw-r--r-- | install/ui/src/freeipa/host.js | 11 | ||||
-rw-r--r-- | install/ui/src/freeipa/rule.js | 2 | ||||
-rw-r--r-- | install/ui/src/freeipa/service.js | 60 | ||||
-rw-r--r-- | install/ui/src/freeipa/user.js | 6 | ||||
-rw-r--r-- | install/ui/src/freeipa/util.js | 338 | ||||
-rw-r--r-- | install/ui/src/freeipa/widget.js | 386 |
17 files changed, 1633 insertions, 897 deletions
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() { |