summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_renderer.js25
-rw-r--r--addons/mail/static/src/js/form_renderer.js2
-rw-r--r--addons/sale/static/src/js/tour.js2
-rw-r--r--addons/web/static/src/js/fields/abstract_field.js83
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js38
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js58
-rw-r--r--addons/web/static/src/js/views/abstract_renderer.js1
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js15
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js4
-rw-r--r--addons/web/static/src/js/views/basic/basic_renderer.js520
-rw-r--r--addons/web/static/src/js/views/basic/basic_view.js3
-rw-r--r--addons/web/static/src/js/views/field_manager_mixin.js15
-rw-r--r--addons/web/static/src/js/views/form/form_controller.js15
-rw-r--r--addons/web/static/src/js/views/form/form_renderer.js265
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_renderer.js1
-rw-r--r--addons/web/static/src/js/views/list/list_controller.js13
-rw-r--r--addons/web/static/src/js/views/list/list_editable_renderer.js477
-rw-r--r--addons/web/static/src/js/views/list/list_renderer.js146
-rw-r--r--addons/web/static/src/less/new_views.less63
-rw-r--r--addons/web/static/tests/fields/basic_fields_tests.js90
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js22
-rw-r--r--addons/web/static/tests/helpers/mock_server.js57
-rw-r--r--addons/web/static/tests/views/form_tests.js119
-rw-r--r--addons/web/static/tests/views/list_tests.js237
24 files changed, 1445 insertions, 826 deletions
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
index 6a43610596d..c2174fdce7f 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
@@ -52,9 +52,11 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, {
self.handleNameRecord = recordID;
self.name = new basic_fields.FieldChar(self,
'name', self.model.get(self.handleNameRecord),
- {mode: 'edit', required: true});
+ {mode: 'edit'});
- self.name.appendTo(self.$('.statement_name_edition'));
+ self.name.appendTo(self.$('.statement_name_edition')).then(function () {
+ self.name.$el.addClass('o_form_required');
+ });
self.$('.statement_name').text(self._initialState.bank_statement_id.display_name);
});
defs.push(def);
@@ -422,7 +424,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
var record = self.model.get(self.handleCreateRecord);
self.fields.account_id = new relational_fields.FieldMany2One(self,
- 'account_id', record, {mode: 'edit', required: true});
+ 'account_id', record, {mode: 'edit'});
self.fields.journal_id = new relational_fields.FieldMany2One(self,
'journal_id', record, {mode: 'edit'});
@@ -434,19 +436,26 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
'analytic_account_id', record, {mode: 'edit'});
self.fields.label = new basic_fields.FieldChar(self,
- 'label', record, {mode: 'edit', required: true});
+ 'label', record, {mode: 'edit'});
self.fields.amount = new basic_fields.FieldFloat(self,
- 'amount', record, {mode: 'edit', required: true});
+ 'amount', record, {mode: 'edit'});
var $create = $(qweb.render("reconciliation.line.create", {'state': state}));
- self.fields.account_id.appendTo($create.find('.create_account_id .o_td_field'));
+ self.fields.account_id.appendTo($create.find('.create_account_id .o_td_field'))
+ .then(addRequiredStyle.bind(self, self.fields.account_id));
self.fields.journal_id.appendTo($create.find('.create_journal_id .o_td_field'));
self.fields.tax_id.appendTo($create.find('.create_tax_id .o_td_field'));
self.fields.analytic_account_id.appendTo($create.find('.create_analytic_account_id .o_td_field'));
- self.fields.label.appendTo($create.find('.create_label .o_td_field'));
- self.fields.amount.appendTo($create.find('.create_amount .o_td_field'));
+ self.fields.label.appendTo($create.find('.create_label .o_td_field'))
+ .then(addRequiredStyle.bind(self, self.fields.label));
+ self.fields.amount.appendTo($create.find('.create_amount .o_td_field'))
+ .then(addRequiredStyle.bind(self, self.fields.amount));
self.$('.create').append($create);
+
+ function addRequiredStyle(widget) {
+ widget.$el.addClass('o_form_required');
+ }
});
},
diff --git a/addons/mail/static/src/js/form_renderer.js b/addons/mail/static/src/js/form_renderer.js
index cca9d18cd15..4d07168e501 100644
--- a/addons/mail/static/src/js/form_renderer.js
+++ b/addons/mail/static/src/js/form_renderer.js
@@ -27,7 +27,7 @@ FormRenderer.include({
*
* @override
*/
- updateWidgets: function (fields, state) {
+ confirmChange: function (state, id, fields) {
if (this.chatter) {
var updatedMailFields = _.intersection(fields, _.values(this.mailFields));
if (updatedMailFields.length) {
diff --git a/addons/sale/static/src/js/tour.js b/addons/sale/static/src/js/tour.js
index 241014c74ce..2a3ec907b8b 100644
--- a/addons/sale/static/src/js/tour.js
+++ b/addons/sale/static/src/js/tour.js
@@ -39,7 +39,7 @@ tour.register('sale_tour', {
content: _t("Click here to add some lines to your quotations."),
position: "bottom",
}, {
- trigger: ".modal-body .o_form_required input, .o_list_view .o_required_field input",
+ trigger: ".modal-body .o_form_required input, .o_list_view .o_form_required input",
extra_trigger: ".o_sale_order",
content: _t("Select a product, or create a new one on the fly. The product will define the default sales price (that you can change), taxes and description automatically."),
position: "right",
diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js
index f36418e3b8b..0d7ed2f4fe2 100644
--- a/addons/web/static/src/js/fields/abstract_field.js
+++ b/addons/web/static/src/js/fields/abstract_field.js
@@ -32,7 +32,6 @@ odoo.define('web.AbstractField', function (require) {
var field_utils = require('web.field_utils');
var Widget = require('web.Widget');
-var Domain = require('web.Domain');
var AbstractField = Widget.extend({
className: 'o_field_widget',
@@ -40,22 +39,12 @@ var AbstractField = Widget.extend({
'keydown': '_onKeydown',
},
/**
- * if this flag is set to true, the rest of the web client will assume that
- * it is not editable. For example, the list view in editable mode will
- * skip the widget when pressing tab
- */
- readonly: false,
- /**
* If this flag is set to true, the field widget will be reset on every
* change which is made in the view (if the view supports it). This is
* currently a form view feature.
*/
resetOnAnyFieldChange: false,
/**
- * if true, the widget will replace a cell in an editable list view
- */
- replace_element: false,
- /**
* If this flag is given a string, the related BasicModel will be used to
* initialize specialData the field might need. This data will be available
* through this.record.specialData[this.name].
@@ -77,8 +66,6 @@ var AbstractField = Widget.extend({
* @param {Object} record A record object (result of the get method of a basic model)
* @param {Object} [options]
* @param {string} [options.mode=readonly] should be 'readonly' or 'edit'
- * @param {string} [options.required=false]
- * @param {string} [options.idForLabel]
*/
init: function (parent, name, record, options) {
this._super(parent);
@@ -103,9 +90,6 @@ var AbstractField = Widget.extend({
var fieldsInfo = record.fieldsInfo[this.viewType];
this.attrs = fieldsInfo && fieldsInfo[name] || {};
- // modifiers
- this.modifiers = JSON.parse(this.attrs.modifiers || "{}");
-
// this property tracks the current (parsed if needed) value of the field.
// Note that we don't use an event system anymore, using this.get('value')
// is no longer valid.
@@ -153,15 +137,6 @@ var AbstractField = Widget.extend({
// a string.
this._isValid = true;
- // the 'required' flag is basically only needed to determine if the widget
- // is in a valid state (not valid if empty and required)
- this.required = options.required || this.field.required;
-
- // the 'idForLabel' is the (html) id that should be set to the relevent
- // dom entity. If done correctly, clicking on the corresponding label
- // (in form view) will focus and select the value.
- this.idForLabel = options.idForLabel;
-
// this is the last value that was set by the user, unparsed. This is
// used to avoid setting the value twice in a row with the exact value.
this.lastSetValue = undefined;
@@ -175,8 +150,8 @@ var AbstractField = Widget.extend({
start: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
- self._render();
self.$el.attr('name', self.name);
+ return self._render();
});
},
@@ -185,11 +160,22 @@ var AbstractField = Widget.extend({
//--------------------------------------------------------------------------
/**
- * Right now, this function is only used in editable list view when the user
- * select a cell. The corresponding widget will be activated, which in general
- * means that the input text will be focused and selected
+ * Activates the field widget. By default, activation means focusing and
+ * selecting the associated focusable element.
+ *
+ * @returns {boolean} true if the widget was activated, false if the
+ * focusable element was not found or invisible
*/
activate: function () {
+ var $focusable = this.getFocusableElement();
+ if ($focusable.length && $focusable.is(':visible')) {
+ $focusable.focus();
+ if ($focusable.is('input[type="text"], textarea')) {
+ setTimeout($focusable.select.bind($focusable), 0);
+ }
+ return true;
+ }
+ return false;
},
/**
* This function should be implemented by widgets that are not able to
@@ -202,6 +188,15 @@ var AbstractField = Widget.extend({
commitChanges: function () {
},
/**
+ * Returns the main field's DOM element (jQuery form) which can be focused
+ * by the browser.
+ *
+ * @returns {jQuery} main focusable element inside the widget
+ */
+ getFocusableElement: function () {
+ return $();
+ },
+ /**
* this method is used to determine if the field value is set to a meaningful
* value. This is useful to determine if a field should be displayed as empty
*
@@ -211,23 +206,23 @@ var AbstractField = Widget.extend({
return !!this.value;
},
/**
- * a field widget is valid if its value is valid and if there is a value when
- * it is required. This is checked before saving a record, by the form view.
+ * A field widget is valid if it was checked as valid the last time its
+ * value was changed by the user. This is checked before saving a record, by
+ * the view.
*
- * Note: the return value may be a boolean. It is necessary for some fields
- * (x2many) to sometimes ask confirmation to the user if the current dirty
- * invalid line can be discarded. This is an asynchronous operation, so we
- * need the expressivity of something like a deferred.
+ * Note: the return value may not be a boolean. It is necessary for some
+ * fields (x2many) to sometimes ask confirmation to the user if the current
+ * dirty invalid line can be discarded. This is an asynchronous operation,
+ * so we need the expressivity of something like a deferred.
+ *
+ * Note2: this is the responsability of the view to check that required
+ * fields have a set value.
*
* @returns {boolean|Deferred} true/false if the widget is valid, or a
* deferred which resolves to true/false if the widget is valid or not
*/
isValid: function () {
- var is_required = this.required;
- if ('required' in this.modifiers) {
- is_required = new Domain(this.modifiers.required).compute(this.recordData);
- }
- return this._isValid && !(is_required && !this.isSet());
+ return this._isValid;
},
/**
* this method is supposed to be called from the outside of field widgets.
@@ -285,12 +280,6 @@ var AbstractField = Widget.extend({
* @returns {Deferred}
*/
_render: function () {
- var is_required = this.required;
- if ('required' in this.modifiers) {
- is_required = new Domain(this.modifiers.required).compute(this.recordData);
- }
- this.$el.toggleClass('o_form_required', is_required);
-
if (this.mode === 'edit') {
return this._renderEdit();
} else if (this.mode === 'readonly') {
@@ -347,7 +336,7 @@ var AbstractField = Widget.extend({
try {
value = this._parseValue(value);
this._isValid = true;
- } catch(e) {
+ } catch (e) {
this._isValid = false;
}
var changes = {};
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index d2649195111..9f37240e0cc 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -122,13 +122,12 @@ var InputField = DebouncedField.extend({
//--------------------------------------------------------------------------
/**
- * Automatically selects the content of the field.
+ * Returns the associated <input/> element.
*
* @override
*/
- activate: function () {
- this.$input.focus();
- setTimeout(this.$input.select.bind(this.$input), 0);
+ getFocusableElement: function () {
+ return this.$input || $();
},
/**
* Do not re-render this field if it was the origin of the onchange call.
@@ -171,7 +170,6 @@ var InputField = DebouncedField.extend({
this.$input.addClass('o_form_input');
this.$input.attr({
type: 'text',
- id: this.idForLabel,
placeholder: this.attrs.placeholder || "",
});
// Save cursor position to restore it after updating value
@@ -253,7 +251,6 @@ var FieldChar = InputField.extend({
var FieldDate = InputField.extend({
className: "o_form_field_date",
tagName: "span",
- replace_element: true,
supportedFieldTypes: ['date'],
/**
@@ -274,8 +271,7 @@ var FieldDate = InputField.extend({
});
def = this.datewidget.appendTo('<div>').done(function () {
self.datewidget.$el.addClass(self.$el.attr('class'));
- self.datewidget.$input.addClass('o_form_input');
- self.datewidget.$input.attr('id', self.idForLabel);
+ self._prepareInput(self.datewidget.$input);
self.replaceElement(self.datewidget.$el);
});
}
@@ -371,7 +367,6 @@ var FieldDateTime = FieldDate.extend({
var FieldMonetary = InputField.extend({
className: 'o_form_field_monetary o_list_number',
- replace_element: true,
supportedFieldTypes: ['float', 'monetary'],
resetOnAnyFieldChange: true, // Have to listen to currency changes
@@ -484,7 +479,6 @@ var FieldBoolean = AbstractField.extend({
events: _.extend({}, AbstractField.prototype.events, {
change: '_onChange',
}),
- replace_element: true,
supportedFieldTypes: ['boolean'],
//--------------------------------------------------------------------------
@@ -492,15 +486,12 @@ var FieldBoolean = AbstractField.extend({
//--------------------------------------------------------------------------
/**
- * Automatically selects the field.
- *
* @override
+ * @returns {jQuery} the focusable checkbox input
*/
- activate: function () {
- this.$input.focus();
- setTimeout(this.$input.select.bind(this.$input), 0);
+ getFocusableElement: function () {
+ return this.$input || $();
},
-
/**
* A boolean field is always set since false is a valid value.
*
@@ -686,7 +677,6 @@ var FieldText = DebouncedField.extend({
*/
start: function () {
this.$el.addClass('o_list_text o_form_textarea');
- this.$el.attr('id', this.idForLabel);
if (this.mode === 'edit') {
this.$textarea = $('<textarea>').appendTo(this.$el);
@@ -704,14 +694,12 @@ var FieldText = DebouncedField.extend({
//--------------------------------------------------------------------------
/**
- * Automatically selects the content of the field.
+ * Returns the associated <textarea/> element.
*
* @override
*/
- activate: function () {
- var $textarea = this.$('textarea');
- $textarea.focus();
- setTimeout($textarea.select.bind($textarea), 0);
+ getFocusableElement: function () {
+ return this.$textarea || $();
},
//--------------------------------------------------------------------------
@@ -753,8 +741,6 @@ var HandleWidget = AbstractField.extend({
tagName: 'span',
className: 'o_row_handle fa fa-arrows ui-sortable-handle',
description: "",
- readonly: true,
- replace_element: true,
supportedFieldTypes: ['integer'],
});
@@ -1093,7 +1079,6 @@ var PriorityWidget = AbstractField.extend({
'mouseout > a': '_onMouseOut',
'click > a': '_onClick',
},
- readonly: true,
supportedFieldTypes: ['selection'],
//--------------------------------------------------------------------------
@@ -1195,7 +1180,6 @@ var PriorityWidget = AbstractField.extend({
this.$('.o_priority_star').removeClass('fa-star-o').addClass('fa-star');
$(event.currentTarget).nextAll().removeClass('fa-star').addClass('fa-star-o');
},
-
});
var AttachmentImage = AbstractField.extend({
@@ -1210,7 +1194,6 @@ var StateSelectionWidget = AbstractField.extend({
},
'click li': '_setSelection'
},
- readonly: true,
supportedFieldTypes: ['selection'],
//--------------------------------------------------------------------------
@@ -1300,7 +1283,6 @@ var StateSelectionWidget = AbstractField.extend({
});
var LabelSelection = AbstractField.extend({
- readonly: true,
supportedFieldTypes: ['selection'],
//--------------------------------------------------------------------------
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 06f38abffbb..abe3de97779 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -116,18 +116,16 @@ var FieldMany2One = AbstractField.extend({
// Public
//--------------------------------------------------------------------------
- activate: function () {
- this.$input.focus();
- setTimeout(this.$input.select.bind(this.$input), 0);
- },
/**
- * Focuses the input.
+ * @override
+ * @returns {jQuery}
*/
- focus: function () {
- if (this.mode === "edit") {
- this.$input.focus();
- }
+ getFocusableElement: function () {
+ return this.$input || $();
},
+ /**
+ * TODO
+ */
reinitialize: function (value) {
this.floating = false;
this._setValue(value);
@@ -366,7 +364,7 @@ var FieldMany2One = AbstractField.extend({
disable_multiple_selection: true,
on_selected: function (element_ids) {
self.reinitialize({id: element_ids[0]});
- self.focus();
+ self.activate();
}
})).open();
},
@@ -409,7 +407,7 @@ var FieldMany2One = AbstractField.extend({
*/
_onExternalButtonClick: function () {
if (!this.value) {
- this.focus();
+ this.activate();
return;
}
var self = this;
@@ -578,8 +576,7 @@ var FieldX2Many = AbstractField.extend({
}
var def = $.Deferred();
this.renderer.canBeSaved()
- .then(def.resolve.bind(def, true))
- .fail(def.resolve.bind(def, false));
+ .then(def.resolve.bind(def, true), def.resolve.bind(def, false));
return def;
},
@@ -610,6 +607,7 @@ var FieldX2Many = AbstractField.extend({
mode: this.mode,
addCreateLine: !this.isReadonly && this.activeActions.create,
addTrashIcon: !this.isReadonly,
+ viewType: 'list',
});
}
if (arch.tag === 'kanban') {
@@ -620,7 +618,8 @@ var FieldX2Many = AbstractField.extend({
};
this.renderer = new KanbanRenderer(this, this.value, {
arch: arch,
- record_options: record_options
+ record_options: record_options,
+ viewType: 'kanban',
});
}
return this.renderer ? this.renderer.appendTo(this.$el) : this._super();
@@ -940,7 +939,6 @@ var FieldMany2Many = FieldX2Many.extend({
var FieldMany2ManyTags = AbstractField.extend({
tag_template: "FieldMany2ManyTag",
className: "o_form_field o_form_field_many2manytags",
- replace_element: true,
supportedFieldTypes: ['many2many'],
custom_events: {
field_changed: '_onFieldChanged',
@@ -957,9 +955,23 @@ var FieldMany2ManyTags = AbstractField.extend({
// Public
//--------------------------------------------------------------------------
+ /**
+ * @override
+ */
activate: function () {
- this.many2one.focus();
+ return this.many2one ? this.many2one.activate() : false;
+ },
+ /**
+ * @override
+ * @returns {jQuery}
+ */
+ getFocusableElement: function () {
+ return this.many2one ? this.many2one.getFocusableElement() : $();
},
+ /**
+ * @override
+ * @returns {boolean}
+ */
isSet: function () {
return !!this.value && this.value.count;
},
@@ -1012,7 +1024,6 @@ var FieldMany2ManyTags = AbstractField.extend({
this.many2one.destroy();
}
this.many2one = new FieldMany2One(this, this.name, this.record, {
- idForLabel: this.idForLabel,
mode: 'edit',
viewType: this.viewType,
});
@@ -1343,7 +1354,6 @@ var FieldSelection = AbstractField.extend({
events: _.extend({}, AbstractField.prototype.events, {
'change': '_onChange',
}),
- replace_element: true,
/**
* @override
*/
@@ -1365,6 +1375,18 @@ var FieldSelection = AbstractField.extend({
/**
* @override
+ * @returns {jQuery}
+ */
+ getFocusableElement: function () {
+ return this.$el.is('select') ? this.$el : $();
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
* @private
*/
_renderEdit: function () {
diff --git a/addons/web/static/src/js/views/abstract_renderer.js b/addons/web/static/src/js/views/abstract_renderer.js
index 83aafdfe310..70ad5350670 100644
--- a/addons/web/static/src/js/views/abstract_renderer.js
+++ b/addons/web/static/src/js/views/abstract_renderer.js
@@ -93,4 +93,3 @@ return Widget.extend({
});
});
-
diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js
index c21d7a43335..5293ee6f4b6 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -88,6 +88,21 @@ return AbstractController.extend(FieldManagerMixin, {
//--------------------------------------------------------------------------
/**
+ * Called by the field manager mixin to confirm that a change just occured
+ * (after that potential onchanges have been applied).
+ *
+ * Basically, this only relays the notification to the renderer with the
+ * new state.
+ *
+ * @param {string} id - the id of one of the view's records
+ * @param {string[]} fields - the changed fields
+ * @param {OdooEvent} e - the event that triggered the change
+ */
+ _confirmChange: function (id, fields, e) {
+ var state = this.model.get(this.handle);
+ this.renderer.confirmChange(state, id, fields, e);
+ },
+ /**
* Delete records (and ask for confirmation if necessary)
*
* @param {string[]} ids list of local record ids
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index cd1dedb2f6c..ba51819b218 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -27,6 +27,8 @@ odoo.define('web.BasicModel', function (require) {
* fieldsInfo: {Object},
* getContext: {function},
* getDomain: {function},
+ * getEvalContext: {function},
+ * getFieldNames: {function},
* groupedBy: {string[]},
* id: {integer},
* isOpen: {boolean},
@@ -2001,8 +2003,8 @@ var BasicModel = AbstractModel.extend({
};
dataPoint.getContext = this._getContext.bind(this, dataPoint);
- dataPoint.getEvalContext = this._getEvalContext.bind(this, dataPoint);
dataPoint.getDomain = this._getDomain.bind(this, dataPoint);
+ dataPoint.getEvalContext = this._getEvalContext.bind(this, dataPoint);
dataPoint.getFieldNames = this._getFieldNames.bind(this, dataPoint);
this.localData[dataPoint.id] = dataPoint;
diff --git a/addons/web/static/src/js/views/basic/basic_renderer.js b/addons/web/static/src/js/views/basic/basic_renderer.js
index 1db0718545e..a18a82e5745 100644
--- a/addons/web/static/src/js/views/basic/basic_renderer.js
+++ b/addons/web/static/src/js/views/basic/basic_renderer.js
@@ -3,27 +3,86 @@ odoo.define('web.BasicRenderer', function (require) {
/**
* The BasicRenderer is an abstract class designed to share code between all
- * views that uses a BasicModel. The main goal is to keep track of all field
- * widgets, and properly destroy them whenever a rerender is done.
- *
- * When a concrete view extends a BasicRenderer, it should add each field
- * widgets in the this.widget list.
+ * views that uses a BasicModel. The main goal is to keep track of all field
+ * widgets, and properly destroy them whenever a rerender is done. The widgets
+ * and modifiers updates mechanism is also shared in the BasicRenderer.
*/
-
var AbstractRenderer = require('web.AbstractRenderer');
var config = require('web.config');
var core = require('web.core');
+var Domain = require('web.Domain');
var qweb = core.qweb;
-return AbstractRenderer.extend({
+var BasicRenderer = AbstractRenderer.extend({
+ custom_events: {
+ /**
+ * All basic renderers should be able to handle widget navigation
+ * through the TAB key.
+ */
+ move_next: '_onMoveNext',
+ move_previous: '_onMovePrevious',
+ },
/**
+ * Basic renderers implements the concept of "mode", they can either be in
+ * readonly mode or editable mode.
+ *
* @override
*/
- init: function (paret, state, params) {
+ init: function (parent, state, params) {
this._super.apply(this, arguments);
- this.widgets = [];
this.activeActions = params.activeActions;
+ this.viewType = params.viewType;
+ this.mode = params.mode || 'readonly';
+ },
+ /**
+ * Updates the internal state of the renderer to the new state. By default,
+ * this also implements the recomputation of the modifiers and their
+ * application to the DOM and the reset of the field widgets if needed.
+ *
+ * In case the given record is not found anymore, a whole re-rendering is
+ * completed (possible if a change in a record caused an onchange which
+ * erased the current record).
+ *
+ * We could always rerender the view from scratch, but then it would not be
+ * as efficient, and we might lose some local state, such as the input focus
+ * cursor, or the scrolling position.
+ *
+ * @param {Object} state
+ * @param {string} id
+ * @param {string[]} fields
+ * @param {OdooEvent} ev
+ * @returns {Deferred<AbstractField[]>} resolved with the list of widgets
+ * that have been reset
+ */
+ confirmChange: function (state, id, fields, ev) {
+ this.state = state;
+
+ var record = state.id === id ? state : _.findWhere(state.data, {id: id});
+ if (!record) {
+ return this._render();
+ }
+
+ var defs = [];
+
+ // Reset all the field widgets that are marked as changed and the ones
+ // which are configured to always be reset on any change
+ var resetWidgets = [];
+ _.each(this.allFieldWidgets[id], function (widget) {
+ if (_.contains(fields, widget.name) || widget.resetOnAnyFieldChange) {
+ defs.push(widget.reset(record, ev));
+ resetWidgets.push(widget);
+ }
+ });
+
+ // The modifiers update is done after widget resets as modifiers
+ // associated callbacks need to have all the widgets with the proper
+ // state before evaluation
+ defs.push(this._updateAllModifiers(record));
+
+ return $.when.apply($, defs).then(function () {
+ return resetWidgets;
+ });
},
//--------------------------------------------------------------------------
@@ -52,15 +111,358 @@ return AbstractRenderer.extend({
});
},
/**
+ * Activates the widget at the given index for the given record if possible
+ * or the "next" possible one.
+ *
+ * @private
+ * @param {Object} record
+ * @param {integer} currentIndex
+ * @param {integer} [inc=1] - the increment to use when searching for the
+ * "next" possible one
+ * @returns {integer} the index of the widget that was activated or -1 if
+ * none was possible to activate
+ */
+ _activateFieldWidget: function (record, currentIndex, inc) {
+ inc = inc === undefined ? 1 : inc;
+
+ var activated;
+ var recordWidgets = this.allFieldWidgets[record.id] || [];
+ for (var i = 0 ; i < recordWidgets.length ; i++) {
+ activated = recordWidgets[currentIndex].activate();
+ if (activated) {
+ return currentIndex;
+ }
+
+ currentIndex += inc;
+ if (currentIndex >= recordWidgets.length) {
+ currentIndex -= recordWidgets.length;
+ } else if (currentIndex < 0) {
+ currentIndex += recordWidgets.length;
+ }
+ }
+ return -1;
+ },
+ /**
+ * This is a wrapper of the {@see _activateFieldWidget} function to select
+ * the next possible widget instead of the given one.
+ *
+ * @private
+ * @param {Object} record
+ * @param {integer} currentIndex
+ * @return {integer}
+ */
+ _activateNextFieldWidget: function (record, currentIndex) {
+ currentIndex = (currentIndex + 1) % (this.allFieldWidgets[record.id] || []).length;
+ return this._activateFieldWidget(record, currentIndex, +1);
+ },
+ /**
+ * This is a wrapper of the {@see _activateFieldWidget} function to select
+ * the previous possible widget instead of the given one.
+ *
+ * @private
+ * @param {Object} record
+ * @param {integer} currentIndex
+ * @return {integer}
+ */
+ _activatePreviousFieldWidget: function (record, currentIndex) {
+ currentIndex = currentIndex ? (currentIndex - 1) : ((this.allFieldWidgets[record.id] || []).length - 1);
+ return this._activateFieldWidget(record, currentIndex, -1);
+ },
+ /**
+ * Does the necessary DOM updates to match the given modifiers data. The
+ * modifiers data is supposed to contain the properly evaluated modifiers
+ * associated to the given records and elements.
+ *
+ * @param {Object} modifiersData
+ * @param {Object} record
+ * @param {Object} [element] - do the update only on this element if given
+ * @returns {Deferred} resolved once finished
+ */
+ _applyModifiers: function (modifiersData, record, element) {
+ var self = this;
+ var defs = [];
+ var modifiers = modifiersData.evaluatedModifiers[record.id] || {};
+
+ this.defs = defs; // Potentially filled by widget rerendering
+ if (element) {
+ _apply(element);
+ } else {
+ // Clone is necessary as the list might change during _.each
+ _.each(_.clone(modifiersData.elementsByRecord[record.id]), _apply);
+ }
+ delete this.defs;
+
+ return $.when.apply($, defs);
+
+ function _apply(element) {
+ // If the view is in edit mode and that a widget have to switch
+ // its "readonly" state, we have to re-render it completely
+ if ('readonly' in modifiers
+ && self.mode === "edit"
+ && element.widget
+ && (element.widget.mode === 'readonly') !== modifiers.readonly)
+ {
+ self._rerenderFieldWidget(element.widget, record);
+ return; // Rerendering already applied the modifiers, no need to go further
+ }
+
+ // Toggle modifiers CSS classes if necessary
+ element.$el.toggleClass("o_form_invisible", !!modifiers.invisible);
+ element.$el.toggleClass("o_readonly", !!modifiers.readonly);
+ element.$el.toggleClass("o_form_required", !!modifiers.required);
+
+ // Call associated callback
+ if (element.callback) {
+ element.callback(element, modifiers, record);
+ }
+ }
+ },
+ /**
+ * Determines if a given field widget value can be saved. For this to be
+ * true, the widget must be valid (properly parsed value) and have a value
+ * if the associated view field is required.
+ *
+ * @private
+ * @param {AbstractField} widget
+ * @returns {boolean|Deferred<boolean>} @see AbstractField.isValid
+ */
+ _canWidgetBeSaved: function (widget) {
+ var modifiers = this._getEvaluatedModifiers(widget.__node, widget.record);
+ var isSetOrNotRequired = (widget.isSet() || !modifiers.required);
+ var isValid = widget.isValid();
+ if (isValid instanceof $.Deferred) {
+ return isValid.then(function (isValid) {
+ return isValid && isSetOrNotRequired;
+ });
+ }
+ return isValid && isSetOrNotRequired;
+ },
+ /**
+ * Updates the modifiers evaluation associated to a given modifiers data and
+ * a given record. This only updates the modifiers values. To see associated
+ * DOM updates: @see _updateAllModifiers @see _applyModifiers.
+ *
+ * @private
+ * @param {Object} modifiersData
+ * @param {Object} record
+ */
+ _computeModifiers: function (modifiersData, record) {
+ var evalContext = record.getEvalContext();
+ modifiersData.evaluatedModifiers[record.id]
+ = _.mapObject(modifiersData.modifiers, function (modifier) {
+ return new Domain(modifier, evalContext).compute(evalContext);
+ });
+ },
+ /**
+ * Destroys a given widget associated to the given record and removes it
+ * from internal referencing.
+ *
+ * @private
+ * @param {Object} record
+ * @param {AbstractField} widget
+ * @returns {integer} the index of the removed widget
+ */
+ _destroyFieldWidget: function (record, widget) {
+ var recordWidgets = this.allFieldWidgets[record.id];
+ var index = recordWidgets.indexOf(widget);
+ if (index >= 0) {
+ recordWidgets.splice(index, 1);
+ }
+ this._unregisterModifiersElement(widget.__node, record, widget);
+ widget.$el.destroy();
+ return index;
+ },
+ /**
+ * Searches for the last evaluation of the modifiers associated to the given
+ * data (modifiers evaluation are supposed to always be up-to-date as soon
+ * as possible).
+ *
+ * @private
+ * @param {Object} node
+ * @param {Object} record
+ * @returns {Object} the evaluated modifiers associated to the given node
+ * and record (not recomputed by the call)
+ */
+ _getEvaluatedModifiers: function (node, record) {
+ var element = this._getModifiersData(node);
+ if (!element) {
+ return {};
+ }
+ return element.evaluatedModifiers[record.id] || {};
+ },
+ /**
+ * Searches through the registered modifiers data for the one which is
+ * related to the given node.
+ *
+ * @private
+ * @param {Object} node
+ * @returns {Object|undefined} related modifiers data if any
+ * undefined otherwise
+ */
+ _getModifiersData: function (node) {
+ return _.findWhere(this.allModifiersData, {node: node});
+ },
+ /**
+ * Registers or updates the modifiers data associated to the given node.
+ * This method is quiet complex as it handles all the needs of the basic
+ * renderers:
+ *
+ * - On first registration, the modifiers are evaluated thanks to the given
+ * record. This allows nodes that will produce an AbstractField instance
+ * to have their modifiers registered before this field creation as we
+ * need the readonly modifier to be able to instantiate the AbstractField.
+ * (@see _computeModifiers).
+ *
+ * - On additional registrations, if the node was already registered but the
+ * record is different, we evaluate the modifiers for this record and
+ * saves them in the same object (without reparsing the modifiers).
+ *
+ * - On additional registrations, the modifiers are not reparsed (or
+ * reevaluated for an already seen record) but the given widget or DOM
+ * element is associated to the node modifiers.
+ *
+ * - The new elements are immediately adapted to match the modifiers and the
+ * given associated callback is called even if there is no modifiers on
+ * the node (@see _applyModifiers). This is indeed necessary as the
+ * callback is a description of what to do when a modifier changes. Even
+ * if there is no modifiers, this action must be performed on first
+ * rendering to avoid code duplication. If there is no modifiers, they
+ * will however not be registered for modifiers updates.
+ *
+ * - When a new element is given, it does not replace the old one, it is
+ * added as an additional element. This is indeed useful for nodes that
+ * will produce multiple DOM (as a list cell and its internal widget or
+ * a form field and its associated label).
+ * (@see _unregisterModifiersElement for removing an associated element.)
+ *
+ * Note: also on view rerendering, all the modifiers are forgotten so that
+ * the renderer only keeps the ones associated to the current DOM state.
+ *
+ * @private
+ * @param {Object} node
+ * @param {Object} record
+ * @param {jQuery|AbstractField} [element]
+ * @param {Object} [options]
+ * @param {Object} [options.callback] - the callback to call on registration
+ * and on modifiers updates
+ * @returns {Object} for code efficiency, returns the last evaluated
+ * modifiers for the given node and record.
+ */
+ _registerModifiers: function (node, record, element, options) {
+ // Check if we already registered the modifiers for the given node
+ // If yes, this is simply an update of the related element
+ // If not, check the modifiers to see if it needs registration
+ var modifiersData = this._getModifiersData(node);
+ if (!modifiersData) {
+ var modifiers = JSON.parse(node.attrs.modifiers || "{}"); // FIXME parsed multiple times (record switching, no modifiers, ...)
+ modifiersData = {
+ node: node,
+ modifiers: modifiers,
+ evaluatedModifiers: {},
+ elementsByRecord: {},
+ };
+ if (!_.isEmpty(modifiers)) { // Register only if modifiers might change (TODO condition might be improved here)
+ this.allModifiersData.push(modifiersData);
+ }
+ }
+
+ // Evaluate if necessary
+ if (!modifiersData.evaluatedModifiers[record.id]) {
+ this._computeModifiers(modifiersData, record);
+ }
+
+ // Element might not be given yet (a second call to the function can
+ // update the registration with the element)
+ if (element) {
+ var newElement = {};
+ if (element instanceof jQuery) {
+ newElement.$el = element;
+ } else {
+ newElement.widget = element;
+ newElement.$el = element.$el;
+ }
+ if (options && options.callback) {
+ newElement.callback = options.callback;
+ }
+
+ if (!modifiersData.elementsByRecord[record.id]) {
+ modifiersData.elementsByRecord[record.id] = [];
+ }
+ modifiersData.elementsByRecord[record.id].push(newElement);
+
+ this._applyModifiers(modifiersData, record, newElement);
+ }
+
+ return modifiersData.evaluatedModifiers[record.id];
+ },
+ /**
* Render the view
*
* @override
* @returns {Deferred}
*/
_render: function () {
- var oldWidgets = this.widgets;
- this.widgets = [];
- return this._renderView().then(_.invoke.bind(_, oldWidgets, 'destroy'));
+ var oldAllFieldWidgets = this.allFieldWidgets;
+ this.allFieldWidgets = {}; // TODO maybe merging allFieldWidgets and allModifiersData into "nodesData" in some way could be great
+ this.allModifiersData = [];
+ return this._renderView().then(function () {
+ _.each(oldAllFieldWidgets, function (recordWidgets) {
+ _.each(recordWidgets, function (widget) {
+ widget.destroy();
+ });
+ });
+ });
+ },
+ /**
+ * Instantiates the appropriate AbstractField specialization for the given
+ * node and prepares its rendering and addition to the DOM. Indeed, the
+ * rendering of the widget will be started and the associated deferred will
+ * be added to the 'defs' attribute. This is supposed to be created and
+ * deleted by the calling code if necessary.
+ * Note: for this implementation to work, AbstractField willStart methods
+ * *must* be synchronous.
+ *
+ * @private
+ * @param {Object} node
+ * @param {Object} record
+ * @param {Object} [options]
+ * @param {Object} [modifiersOptions]
+ * @returns {AbstractField}
+ */
+ _renderFieldWidget: function (node, record, options, modifiersOptions) {
+ var fieldName = node.attrs.name;
+
+ // Register the node-associated modifiers
+ var modifiers = this._registerModifiers(node, record);
+
+ // Initialize and register the widget
+ // Readonly status is known as the modifiers have just been registered
+ var Widget = record.fieldsInfo[this.viewType][fieldName].Widget;
+ var widget = new Widget(this, fieldName, record, _.extend({
+ mode: modifiers.readonly ? 'readonly' : this.mode,
+ viewType: this.viewType,
+ }, options || {}));
+
+ // Register the widget so that it can easily be found again
+ if (this.allFieldWidgets[record.id] === undefined) {
+ this.allFieldWidgets[record.id] = [];
+ }
+ this.allFieldWidgets[record.id].push(widget);
+
+ widget.__node = node; // TODO get rid of this if possible one day
+
+ // Prepare widget rendering and save the related deferred
+ var def = widget.__widgetRenderAndInsert(function () {});
+ if (def.state() === 'pending') {
+ this.defs.push(def);
+ }
+
+ // Update the modifiers registration by associating the widget and by
+ // giving the modifiers options now (as the potential callback is
+ // associated to new widget)
+ this._registerModifiers(node, record, widget, modifiersOptions);
+
+ return widget;
},
/**
* Actual rendering. Supposed to be overridden by concrete renderers.
@@ -69,7 +471,7 @@ return AbstractRenderer.extend({
* - instantiate a widget from the registry for each field in the arch
*
* Note that the 'state' field should contains all necessary information
- * for the rendering. The field widgets should be as synchronous as
+ * for the rendering. The field widgets should be as synchronous as
* possible.
*
* @abstract
@@ -78,6 +480,98 @@ return AbstractRenderer.extend({
_renderView: function () {
return $.when();
},
+ /**
+ * Rerenders a given widget and make sure the associated data which
+ * referenced the old one is updated.
+ *
+ * @private
+ * @param {Widget} widget
+ * @param {Object} record
+ * @returns {AbstractField}
+ */
+ _rerenderFieldWidget: function (widget, record) {
+ // Render the new field widget
+ var newWidget = this._renderFieldWidget(widget.__node, record);
+ widget.$el.replaceWith(newWidget.$el);
+
+ // Destroy the old widget and position the new one at the old one's
+ var oldIndex = this._destroyFieldWidget(record, widget);
+ var recordWidgets = this.allFieldWidgets[record.id];
+ recordWidgets.splice(oldIndex, 0, newWidget);
+ recordWidgets.pop();
+
+ return newWidget;
+ },
+ /**
+ * Unregisters an element of the modifiers data associated to the given
+ * node and record.
+ *
+ * @param {Object} node
+ * @param {Object} record
+ * @param {jQuery|AbstractField} element
+ */
+ _unregisterModifiersElement: function (node, record, element) {
+ var modifiersData = this._getModifiersData(node);
+ if (modifiersData) {
+ var elements = modifiersData.elementsByRecord[record.id];
+ var index = _.findIndex(elements, function (oldElement) {
+ return oldElement.widget === element
+ || oldElement.$el[0] === element[0];
+ });
+ if (index >= 0) {
+ elements.splice(index, 1);
+ }
+ }
+ },
+ /**
+ * Does two actions, for each registered modifiers:
+ * 1) Recomputes the modifiers associated to the given record and saves them
+ * (as boolean values) in the appropriate modifiers data.
+ * 2) Updates the rendering of the view elements associated to the given
+ * record to match the new modifiers.
+ *
+ * @see _computeModifiers
+ * @see _applyModifiers
+ *
+ * @private
+ * @param {Object} record
+ * @returns {Deferred} resolved once finished
+ */
+ _updateAllModifiers: function (record) {
+ var self = this;
+
+ var defs = _.flatten(_.map(this.allModifiersData, function (modifiersData) {
+ self._computeModifiers(modifiersData, record);
+ return self._applyModifiers(modifiersData, record);
+ }));
+
+ return $.when.apply($, defs);
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * When someone presses TAB in a widget, it is nice to be able to go to the
+ * next widget, especially since the TAB key event is preventDefaulted
+ *
+ * @abstract
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onMoveNext: function (ev) {},
+ /**
+ * When someone presses SHIFT+TAB in a widget, it is nice to be able to go
+ * back to the previous widget, especially since the TAB key event is
+ * preventDefaulted
+ *
+ * @abstract
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onMovePrevious: function (ev) {},
});
+return BasicRenderer;
});
diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js
index d850b2b7a2f..2604dcf0aa1 100644
--- a/addons/web/static/src/js/views/basic/basic_view.js
+++ b/addons/web/static/src/js/views/basic/basic_view.js
@@ -19,6 +19,7 @@ var BasicView = AbstractView.extend({
Model: BasicModel,
Controller: BasicController,
}),
+ viewType: undefined,
/**
* process the fields_view to find all fields appearing in the views.
* list those fields' name in this.fields_name, which will be the list
@@ -34,6 +35,8 @@ var BasicView = AbstractView.extend({
init: function (viewInfo, params) {
this._super.apply(this, arguments);
+ this.rendererParams.viewType = this.viewType;
+
this.controllerParams.confirmOnDelete = true;
this.controllerParams.archiveEnabled = 'active' in viewInfo.fields;
this.controllerParams.hasButtons =
diff --git a/addons/web/static/src/js/views/field_manager_mixin.js b/addons/web/static/src/js/views/field_manager_mixin.js
index 9fcd34ffee2..06997c1c374 100644
--- a/addons/web/static/src/js/views/field_manager_mixin.js
+++ b/addons/web/static/src/js/views/field_manager_mixin.js
@@ -12,6 +12,7 @@ var BasicModel = require('web.BasicModel');
var FieldManagerMixin = {
custom_events: {
+ discard_changes: '_onDiscardChanges',
discard_line: '_onDiscardLine',
field_changed: '_onFieldChanged',
load: '_onLoad',
@@ -60,6 +61,20 @@ var FieldManagerMixin = {
//--------------------------------------------------------------------------
/**
+ * Controlled elements might want to ask for a discard of the changes made
+ * to a whole record (see editable list view). In that case, this handler
+ * is called and call the appropriate @see BasicModel.discardChanges
+ * function. The result is then notified thanks to a call to
+ * @see _confirmChange.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onDiscardChanges: function (ev) {
+ this.model.discardChanges(ev.data.id);
+ this._confirmChange(ev.data.id, [], ev); // TODO get real list of changes ?
+ },
+ /**
* In some case, we may need to remove an element from a list, without going
* through the notifyChanges machinery. The motivation for this is when the
* user click on 'Add an item' in a field one2many with a required field,
diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js
index f97a93e14e8..f83fdd78bcd 100644
--- a/addons/web/static/src/js/views/form/form_controller.js
+++ b/addons/web/static/src/js/views/form/form_controller.js
@@ -295,17 +295,6 @@ var FormController = BasicController.extend({
return this.alive(def);
},
/**
- * @override method from FieldManagerMixin
- * @private
- * @param {string} id it is ignored, because we already know it.
- * @param {string[]} fields
- * @param {OdooEvent} event the event that triggered the change
- */
- _confirmChange: function (id, fields, event) {
- var record = this.model.get(this.handle);
- this.renderer.updateWidgets(fields, record, event);
- },
- /**
* When a save operation has been confirmed from the model, this method is
* called.
*
@@ -326,7 +315,7 @@ var FormController = BasicController.extend({
return _.isObject(d) &&
(d.id === id || _.findWhere(d.data, {id: id}));
});
- this.renderer.updateWidgets([fieldsChanged], record);
+ this.renderer.confirmChange(record, record.id, [fieldsChanged]);
}
},
/**
@@ -588,7 +577,7 @@ var FormController = BasicController.extend({
this.model.setSort(event.data.id, event.data.name);
var field = event.data.field;
var state = this.model.get(this.handle);
- this.renderer.updateWidgets([field], state);
+ this.renderer.confirmChange(state, state.id, [field]);
},
});
diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js
index ff02fc85337..05d8dd2d5b2 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -4,7 +4,6 @@ odoo.define('web.FormRenderer', function (require) {
var BasicRenderer = require('web.BasicRenderer');
var config = require('web.config');
var core = require('web.core');
-var Domain = require('web.Domain');
var _t = core._t;
var qweb = core.qweb;
@@ -13,23 +12,14 @@ var FIELD_CLASSES = {
'one2many': 'o_field_one2many',
};
-return BasicRenderer.extend({
+var FormRenderer = BasicRenderer.extend({
className: "o_form_view",
- custom_events: {
- move_next: '_onMoveNext',
- move_previous: '_onMovePrevious',
- },
/**
- * @override method from BasicRenderer
- * @param {Object} params
- * @param {string} params.mode either 'readonly' on 'edit'
+ * @override
*/
init: function (parent, state, params) {
this._super.apply(this, arguments);
this.idsForLabels = {};
- this.widgets = [];
- this.mode = params.mode;
- this.field_values = this.state.getEvalContext();
},
//--------------------------------------------------------------------------
@@ -60,8 +50,8 @@ return BasicRenderer.extend({
$label.toggleClass('o_form_invalid', !isValid);
}
- _.each(this.widgets, function (widget) {
- var isValid = widget.isValid();
+ _.each(this.allFieldWidgets[this.state.id], function (widget) {
+ var isValid = self._canWidgetBeSaved(widget);
if (isValid instanceof $.Deferred) {
defs.push(isValid.then(function (isValid) {
markWidget(widget, isValid);
@@ -81,8 +71,10 @@ return BasicRenderer.extend({
* environment with their current value (useful for widgets that can't
* detect when their value changes, e.g. field 'html').
*/
- commitChanges: function () {
- _.invoke(this.widgets, 'commitChanges');
+ commitChanges: function () { // TODO add a test
+ _.each(this.allFieldWidgets, function (recordWidgets) {
+ _.invoke(recordWidgets, 'commitChanges');
+ });
},
/**
* returns the active tab pages for each notebook
@@ -139,38 +131,13 @@ return BasicRenderer.extend({
// if fieldNames are given, we update the corresponding field widget.
// I think this is wrong, and the caller could directly call the
- // updateWidgets method
+ // confirmChange method
if (params.fieldNames) {
// only update the given fields
- return this.updateWidgets(params.fieldNames, state);
+ return this.confirmChange(state, state.id, params.fieldNames);
}
- this.field_values = state.getEvalContext();
return this._super.apply(this, arguments);
},
- /**
- * Update some field widgets with a new state. We could rerender the form
- * view from scratch, but then it would not be as efficient, and we might
- * lose some local state, such as the input focus cursor, or the scrolling
- * position.
- *
- * @param {string[]} fields list of fields to be updated
- * @param {Object} state new state
- * @param {OdooEvent} [event] the event that triggered the change
- * @returns {Deferred}
- */
- updateWidgets: function (fields, state, event) {
- this.state = state;
- this.field_values = this.state.getEvalContext();
- var defs = [];
- _.each(this.widgets, function (widget) {
- if (_.contains(fields, widget.name) || widget.resetOnAnyFieldChange) {
- defs.push(widget.reset(state, event));
- }
- });
- // re-evalute the modifiers with the new state
- this._evaluateModifiers();
- return $.when.apply($, defs);
- },
//--------------------------------------------------------------------------
// Private
@@ -181,16 +148,9 @@ return BasicRenderer.extend({
*
* @private
* @param {Object} widget a field widget
- * @param {Object} field description of the field
- */
- _addFieldClassNames: function (widget, field) {
- widget.$el.addClass('o_form_field');
- // classname 'o_form_field_empty' hides the widget, so we apply it in
- // on existing records for fields with no value, in readonly mode or if the
- // field is readonly
- if (this.state.data.id && !widget.isSet() && (this.mode === 'readonly' || field.readonly)) {
- widget.$el.addClass('o_form_field_empty');
- }
+ */
+ _addFieldClassNames: function (widget) {
+ widget.$el.addClass('o_form_field'); // TODO will be removed in the CSS update
},
/**
* @private
@@ -209,21 +169,6 @@ return BasicRenderer.extend({
},
/**
* @private
- */
- _evaluateModifiers: function () {
- var self = this;
- _.each(this.nodeModifiers, function (d) {
- if ('invisible' in d.modifiers) {
- var is_invisible = new Domain(d.modifiers.invisible).compute(self.field_values);
- d.$el.toggleClass('o_form_invisible', is_invisible);
- if (d.onEvalCallback) {
- d.onEvalCallback({invisible: is_invisible});
- }
- }
- });
- },
- /**
- * @private
* @param {string} name
* @returns {string}
*/
@@ -239,16 +184,8 @@ return BasicRenderer.extend({
* @private
* @param {jQueryElement} $el
* @param {Object} node
- * @param {function} onEvalCallback
- */
- _handleAttributes: function ($el, node, onEvalCallback) {
- // register the modifiers with their corresponding $el so that they can
- // be re-evaluated at each field changed
- this.nodeModifiers.push({
- $el: $el,
- modifiers: JSON.parse(node.attrs.modifiers || "{}"),
- onEvalCallback: onEvalCallback,
- });
+ */
+ _handleAttributes: function ($el, node) {
if (node.attrs.class) {
$el.addClass(node.attrs.class);
}
@@ -287,7 +224,7 @@ return BasicRenderer.extend({
// Get the folded buttons
var folded_buttons = visible_buttons.slice(nb_buttons);
- if(folded_buttons.length === 1) {
+ if (folded_buttons.length === 1) {
unfolded_buttons = buttons;
folded_buttons = [];
}
@@ -302,7 +239,7 @@ return BasicRenderer.extend({
});
// Add the dropdown with folded buttons if any
- if(folded_buttons.length) {
+ if (folded_buttons.length) {
$result.append($("<button>", {
type: 'button',
'class': "btn btn-sm oe_stat_button o_button_more dropdown-toggle",
@@ -318,36 +255,35 @@ return BasicRenderer.extend({
}
this._handleAttributes($result, node);
+ this._registerModifiers(node, this.state, $result);
return $result;
},
/**
+ * @override
* @private
- * @param {Object} node
- * @returns {FieldWidget}
+ * @param {string} node
+ * @param {Object} record
+ * @param {Object} [options]
+ * @returns {AbstractField}
*/
- _renderFieldWidget: function (node) {
- var name = node.attrs.name;
- var field = this.state.fields[name];
+ _renderFieldWidget: function (node, record, options, modifiersOptions) {
+ var self = this;
+ modifiersOptions = _.extend({
+ callback: function (element, modifiers, record) {
+ element.$el.toggleClass('o_form_field_empty', !!( // FIXME condition is evaluated twice (label AND widget...)
+ record.data.id
+ && (modifiers.readonly || self.mode === 'readonly')
+ && !element.widget.isSet()
+ ));
+ },
+ }, modifiersOptions || {});
- var modifiers = JSON.parse(node.attrs.modifiers || "{}");
- var is_readonly = new Domain(modifiers.readonly).compute(this.field_values);
- var is_required = field.required || new Domain(modifiers.required).compute(this.field_values);
- var options = {
- mode: is_readonly ? 'readonly' : this.mode,
- required: is_required,
- idForLabel: this._getIDForLabel(name),
- viewType: 'form',
- };
- var attrs = this.state.fieldsInfo.form[name];
- var widget = new attrs.Widget(this, name, this.state, options);
- this.widgets.push(widget);
- var def = widget.__widgetRenderAndInsert(function () {});
- if (def.state() === 'pending') {
- this.defs.push(def);
- }
- widget.$el.addClass(FIELD_CLASSES[field.type]);
+ var widget = this._super(node, record, options, modifiersOptions);
+ widget.getFocusableElement().attr('id', this._getIDForLabel(node.attrs.name));
+
+ widget.$el.addClass(FIELD_CLASSES[record.fields[node.attrs.name].type]);
+ this._addFieldClassNames(widget);
this._handleAttributes(widget.$el, node);
- this._addFieldClassNames(widget, field);
return widget;
},
@@ -357,13 +293,9 @@ return BasicRenderer.extend({
* @returns {jQueryElement}
*/
_renderGenericTag: function (node) {
- var $result = $('<' + node.tag + '>');
- _.each(node.attrs, function (attr, name) {
- if (name !== 'class' && name !== 'modifiers') {
- $result.attr(name, attr);
- }
- });
+ var $result = $('<' + node.tag + '>', _.omit(node.attrs, 'modifiers'));
this._handleAttributes($result, node);
+ this._registerModifiers(node, this.state, $result);
$result.append(_.map(node.children, this._renderNode.bind(this)));
return $result;
},
@@ -378,6 +310,7 @@ return BasicRenderer.extend({
.addClass('btn btn-sm btn-default');
this._addOnClickAction($button, node);
this._handleAttributes($button, node);
+ this._registerModifiers(node, this.state, $button);
return $button;
},
/**
@@ -386,26 +319,35 @@ return BasicRenderer.extend({
* @returns {jQueryElement}
*/
_renderInnerField: function (node) {
- var field = this.state.fields[node.attrs.name];
+ var self = this;
+ var fieldName = node.attrs.name;
+ var field = this.state.fields[fieldName];
var fieldDescr = node.attrs.string || field.string;
- var hasLabel = !node.attrs.nolabel;
- var widget = this._renderFieldWidget(node);
- if (hasLabel) {
- var $label = $('<label>')
- .addClass('o_form_label')
- .attr('for', this._getIDForLabel(node.attrs.name))
- .text(fieldDescr);
- var isReadOnly = this.mode === 'readonly' || field.readonly;
- if (this.state.data.id && !widget.isSet() && isReadOnly) {
- $label.addClass('o_form_label_empty');
- }
- this._handleAttributes($label, node);
+ var widget = this._renderFieldWidget(node, this.state);
+
+ if (!node.attrs.nolabel) {
+ var $label = $('<label>', {
+ class: 'o_form_label',
+ for: this._getIDForLabel(node.attrs.name),
+ text: fieldDescr,
+ });
+ this._registerModifiers(node, this.state, $label, {
+ callback: function (element, modifiers, record) {
+ var recordWidgets = self.allFieldWidgets[record.id];
+ var widget = _.findWhere(recordWidgets, {name: fieldName});
+ element.$el.toggleClass('o_form_label_empty', !!(
+ record.data.id
+ && (modifiers.readonly || self.mode === 'readonly')
+ && !widget.isSet() // getting like this because it could have been re-rendered...
+ ));
+ },
+ });
return $('<tr>')
.append($('<td class="o_td_label">').append($label))
.append($('<td style="width: 100%">').append(widget.$el));
} else {
var style = {
- width: 100/2 + '%'
+ width: 100/2 + '%',
};
return $('<tr>')
.append($('<td colspan="1">').css(style).append(widget.$el));
@@ -419,6 +361,7 @@ return BasicRenderer.extend({
_renderInnerGroup: function (node) {
var $result = $('<table class="o_group o_inner_group"/>');
this._handleAttributes($result, node);
+ this._registerModifiers(node, this.state, $result);
if (node.attrs.string) {
var $sep = $('<tr><td colspan="2" style="width:100%;"><div class="o_horizontal_separator">' + node.attrs.string + '</div></td></tr>');
$result.append($sep);
@@ -522,6 +465,7 @@ return BasicRenderer.extend({
$button.append(_.map(node.children, this._renderNode.bind(this)));
this._addOnClickAction($button, node);
this._handleAttributes($button, node);
+ this._registerModifiers(node, this.state, $button);
return $button;
},
/**
@@ -568,6 +512,7 @@ return BasicRenderer.extend({
$button.append(_.map(node.children, this._renderNode.bind(this)));
this._addOnClickAction($button, node);
this._handleAttributes($button, node);
+ this._registerModifiers(node, this.state, $button);
return $button;
},
/**
@@ -576,7 +521,7 @@ return BasicRenderer.extend({
* @returns {jQueryElement}
*/
_renderTagField: function (node) {
- return this._renderFieldWidget(node).$el;
+ return this._renderFieldWidget(node, this.state).$el;
},
/**
* @private
@@ -617,6 +562,7 @@ return BasicRenderer.extend({
$child.appendTo($result);
});
this._handleAttributes($result, node);
+ this._registerModifiers(node, this.state, $result);
return $result;
},
/**
@@ -633,7 +579,7 @@ return BasicRenderer.extend({
if (child.tag === 'button') {
$buttons.append(self._renderHeaderButton(child));
} else if (child.tag === 'field') {
- var widget = self._renderFieldWidget(child);
+ var widget = self._renderFieldWidget(child, self.state);
$statusbar.append(widget.$el);
}
});
@@ -658,6 +604,7 @@ return BasicRenderer.extend({
.attr('for', this._getIDForLabel(node.attrs.for))
.text(text);
this._handleAttributes($result, node);
+ this._registerModifiers(node, this.state, $result);
return $result;
},
/**
@@ -677,15 +624,18 @@ return BasicRenderer.extend({
$header.addClass('active');
$page.addClass('active');
}
- self._handleAttributes($header, child, function onEvalCallback (attrs) {
- // if the active tab is invisible, activate the first visible tab instead
- if (attrs.invisible && $header.hasClass('active')) {
- $header.removeClass('active');
- $page.removeClass('active');
- var $firstVisibleTab = $headers.find('li:not(.o_form_invisible):first()');
- $firstVisibleTab.addClass('active');
- $pages.find($firstVisibleTab.find('a').attr('href')).addClass('active');
- }
+ self._handleAttributes($header, child);
+ self._registerModifiers(child, self.state, $header, {
+ callback: function (element, modifiers, record) {
+ // if the active tab is invisible, activate the first visible tab instead
+ if (modifiers.invisible && element.$el.hasClass('active')) {
+ element.$el.removeClass('active');
+ $page.removeClass('active');
+ var $firstVisibleTab = $headers.find('li:not(.o_form_invisible):first()');
+ $firstVisibleTab.addClass('active');
+ $pages.find($firstVisibleTab.find('a').attr('href')).addClass('active');
+ }
+ },
});
$headers.append($header);
$pages.append($page);
@@ -727,34 +677,31 @@ return BasicRenderer.extend({
*/
_renderView: function () {
var self = this;
- this.defs = [];
- this.nodeModifiers = [];
// render the form and evaluate the modifiers
+ var defs = [];
+ this.defs = defs;
var $form = this._renderNode(this.arch).addClass(this.className);
- this._evaluateModifiers();
-
- var defs = this.defs;
delete this.defs;
+
return $.when.apply($, defs).then(function () {
self.$el.html($form.contents());
- // necessary to allow all sub widgets to use their dimensions in layout
- // related activities, such as autoresize on fieldtexts
+ // Necessary to allow all sub widgets to use their dimensions in
+ // layout related activities, such as autoresize on fieldtexts
core.bus.trigger('DOM_updated');
self.$el.toggleClass('o_form_nosheet', !self.has_sheet);
self.$el.toggleClass('o_form_editable', self.mode === 'edit');
self.$el.toggleClass('o_form_readonly', self.mode === 'readonly');
// Attach the tooltips on the fields' label
- _.each(self.widgets, function (widget) {
+ _.each(self.allFieldWidgets[self.state.id], function (widget) {
if (core.debug || widget.attrs.help || widget.field.help) {
var idForLabel = self.idsForLabels[widget.name];
var $label = idForLabel ? self.$('label[for=' + idForLabel + ']') : $();
self._addFieldTooltip(widget, $label);
}
});
-
});
},
@@ -763,34 +710,26 @@ return BasicRenderer.extend({
//--------------------------------------------------------------------------
/**
- * When someone presses TAB in a widget, it is nice to be able to go to the
- * next widget, especially since the TAB key event is preventDefaulted
- *
+ * @override
* @private
- * @param {OdooEvent} event
+ * @param {OdooEvent} ev
*/
- _onMoveNext: function (event) {
- event.stopPropagation();
- var index = this.widgets.indexOf(event.target);
- if (index + 1 < this.widgets.length) {
- this.widgets[index + 1].activate();
- }
+ _onMoveNext: function (ev) {
+ ev.stopPropagation();
+ var index = this.allFieldWidgets[this.state.id].indexOf(ev.target);
+ this._activateNextFieldWidget(this.state, index);
},
/**
- * When someone presses SHIFT+TAB in a widget, it is nice to be able to go
- * back to the previous widget, especially since the TAB key event is
- * preventDefaulted
- *
+ * @override
* @private
* @param {OdooEvent} event
*/
- _onMovePrevious: function (event) {
- event.stopPropagation();
- var index = this.widgets.indexOf(event.target);
- if (index >= 1) {
- this.widgets[index - 1].activate();
- }
+ _onMovePrevious: function (ev) {
+ ev.stopPropagation();
+ var index = this.allFieldWidgets[this.state.id].indexOf(ev.target);
+ this._activatePreviousFieldWidget(this.state, index);
},
});
+return FormRenderer;
});
diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer.js b/addons/web/static/src/js/views/kanban/kanban_renderer.js
index 9e4e6865c0f..5c976bba903 100644
--- a/addons/web/static/src/js/views/kanban/kanban_renderer.js
+++ b/addons/web/static/src/js/views/kanban/kanban_renderer.js
@@ -89,6 +89,7 @@ var KanbanRenderer = BasicRenderer.extend({
init: function (parent, state, params) {
this._super.apply(this, arguments);
+ this.widgets = [];
this.qweb = new QWeb(session.debug, {_s: session.origin});
var templates = findInNode(this.arch, function (n) { return n.tag === 'templates';});
transformQwebTemplate(templates, state.fields);
diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js
index 50bb74334c4..5dcc2300eac 100644
--- a/addons/web/static/src/js/views/list/list_controller.js
+++ b/addons/web/static/src/js/views/list/list_controller.js
@@ -159,19 +159,6 @@ var ListController = BasicController.extend({
},
/**
* This function is the hook called by the field manager mixin to confirm
- * that a change just occured (after the eventual onchange have been
- * applied).
- *
- * @param {string} id a basicmodel id, which is a record in the list view
- * @param {string[]} fields the changed fields
- * @param {OdooEvent} event the event that triggered the change
- */
- _confirmChange: function (id, fields, event) {
- var state = this.model.get(this.handle);
- this.renderer.confirmChange(state, id, fields, event);
- },
- /**
- * This function is the hook called by the field manager mixin to confirm
* that a record has been saved.
*
* @override
diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js
index 1e24aaeb879..df7a953c632 100644
--- a/addons/web/static/src/js/views/list/list_editable_renderer.js
+++ b/addons/web/static/src/js/views/list/list_editable_renderer.js
@@ -4,31 +4,27 @@ odoo.define('web.EditableListRenderer', function (require) {
/**
* Editable List renderer
*
- * The list renderer is reasonably complex, so we split it in two files. This
+ * The list renderer is reasonably complex, so we split it in two files. This
* file simply 'includes' the basic ListRenderer to add all the necessary
- * behavior to enable editing records.
+ * behaviors to enable editing records.
*
* Unlike Odoo v10 and before, this list renderer is independant from the form
- * view. It uses the same widgets, but the code is totally stand alone.
+ * view. It uses the same widgets, but the code is totally stand alone.
*/
-
var core = require('web.core');
var Dialog = require('web.Dialog');
-var Domain = require('web.Domain');
var ListRenderer = require('web.ListRenderer');
var _t = core._t;
ListRenderer.include({
- custom_events: {
- field_changed: '_onFieldChanged',
+ custom_events: _.extend({}, ListRenderer.prototype.custom_events, {
move_down: '_onMoveDown',
move_up: '_onMoveUp',
move_left: '_onMoveLeft',
move_right: '_onMoveRight',
- move_next: '_onMoveNext',
move_next_line: '_onMoveNextLine',
- },
+ }),
events: _.extend({}, ListRenderer.prototype.events, {
'click tbody td': '_onCellClick',
'click tbody tr:not(.o_data_row)': '_onEmptyRowClick',
@@ -39,15 +35,12 @@ ListRenderer.include({
/**
* @override
* @param {Object} params
- * @param {string} params.mode either 'readonly' or 'edit'
* @param {boolean} params.addCreateLine
* @param {boolean} params.addTrashIcon
*/
init: function (parent, state, params) {
this._super.apply(this, arguments);
- this.mode = params.mode;
-
// if addCreateLine is true, the renderer will add a 'Add an item' link
// at the bottom of the list view
this.addCreateLine = params.addCreateLine;
@@ -88,19 +81,22 @@ ListRenderer.include({
var self = this;
var invalidDirtyWidgets = [];
var invalidCleanWidgets = [];
- _.each(this.widgets, function (widget) {
- // Note: the isValid method may return a deferred in general, but
- // not in this case. Here, this method is called by a fieldone2many
- // when it need to make sure the state is valid. So, the widgets
- // that we consider here are widgets that can be in a one2many, and
- // we don't support a list inside a list (for now at least).
- if (!widget.isValid()) {
- if (self.dirtyRows[widget.dataPointID]) {
- invalidDirtyWidgets.push(widget);
- } else {
- invalidCleanWidgets.push(widget);
+ _.each(this.allFieldWidgets, function (recordWidgets, recordID) {
+ _.each(recordWidgets, function (widget) {
+ // Note: the _canWidgetBeSaved method may return a deferred in
+ // general, but not in this case. Here, this method is called by
+ // a fieldone2many when it needs to make sure the state is
+ // valid. So, the widgets that we consider here are widgets that
+ // can be in a one2many, and we don't support a list inside a
+ // list (for now at least).
+ if (!self._canWidgetBeSaved(widget)) {
+ if (self.dirtyRows[recordID]) {
+ invalidDirtyWidgets.push(widget);
+ } else {
+ invalidCleanWidgets.push(widget);
+ }
}
- }
+ });
});
if (invalidDirtyWidgets.length) {
// TODO: ask for confirmation, open dialog, and depending on user
@@ -112,29 +108,29 @@ ListRenderer.include({
return $.when();
},
/**
- * This method is called by the controller, when a change has been confirmed
- * by the model. In that situation, we need to properly update the cell
- * widgets. (it is necessary, because the value could have been changed by
- * an onchange).
+ * @see BasicRenderer.confirmChange
*
- * Note that it could happen that the changedRecordID is not found in the
- * current state. This is possible if a change in a record caused an
- * onchange which erased the current record. In that case, we simply
- * rerender the list.
+ * We also react to changes by adding the 'o_field_dirty' css class,
+ * which changes its style to a visible orange outline. It allows the user
+ * to have a visual feedback on which fields changed, and on which fields
+ * have been saved.
*
- * @param {Object} state a record resource (the new full state)
- * @param {string} changedRecordID the local id for the changed record
- * @param {string[]} changedFields list of modified fields
- * @param {OdooEvent} event the event that triggered the change
+ * @override
+ * @param {Object} state - a record resource (the new full state)
+ * @param {string} id - the local id for the changed record
+ * @param {string[]} fields - list of modified fields
+ * @param {OdooEvent} e - the event that triggered the change
+ * @returns {Deferred}
*/
- confirmChange: function (state, changedRecordID, changedFields, event) {
- this.state = state;
- var rowIndex = _.findIndex(this.state.data, {id: changedRecordID});
- if (rowIndex > -1) {
- this._updateRow(rowIndex, changedFields, event);
- } else {
- this._render();
- }
+ confirmChange: function (state, id, fields, e) {
+ this.dirtyRows[id] = true;
+
+ return this._super.apply(this, arguments).then(function (resetWidgets) {
+ _.each(resetWidgets, function (widget) {
+ var $td = widget.$el.parent();
+ $td.addClass('o_field_dirty');
+ });
+ });
},
/**
* This method is called by the controller when the user has clicked 'save',
@@ -179,7 +175,7 @@ ListRenderer.include({
* @returns {jQueryElement} the $td for the index-th field
*/
_findTd: function ($row, index) {
- return $row.find('td').eq(index + (this.hasSelectors ? 1 : 0));
+ return $row.children(':nth(' + (index + (this.hasSelectors ? 1 : 0)) + ')');
},
/**
* Returns the current number of columns. The editable renderer may add a
@@ -196,42 +192,6 @@ ListRenderer.include({
return n;
},
/**
- * Determine if a given cell is readonly. A cell is considered readonly if
- * the field is readonly, or the widget is statically readonly (like the
- * sequence widget) of if there are a 'readonly' attr, with a matching
- * domain.
- *
- * @param {Object} record a basic model record
- * @param {Object} field a field description
- * @param {Object} node a node object (from the arch)
- * @returns {boolean}
- */
- _isReadonly: function (record, field, node) {
- if (field.readonly) {
- return true;
- }
- var Widget = this.state.fieldsInfo.list[node.attrs.name].Widget;
- if (Widget && Widget.prototype.readonly) {
- return true;
- }
- if ('readonly' in node.modifiers) {
- var fieldValues = record.getEvalContext();
- return new Domain(node.modifiers.readonly).compute(fieldValues);
- }
- return false;
- },
- /**
- * A cell is editable if its field isn't invisible and if it isn't readonly.
- *
- * @param {Object} record a basic model record
- * @param {Object} field a field description
- * @param {Object} node a node object (from the arch)
- * @returns {boolean}
- */
- _isEditable: function (record, field, node) {
- return !this._isInvisible(record, node) && !this._isReadonly(record, field, node);
- },
- /**
* The renderer needs to support reordering lines. This is only active in
* edit mode. The hasHandle attribute is used when there is a sequence
* widget.
@@ -260,65 +220,17 @@ ListRenderer.include({
* @param {Object} record
* @param {Object} node
* @param {integer} index
+ * @param {Object} [options]
* @returns {jQueryElement}
*/
- _renderBodyCell: function (record, node, index) {
+ _renderBodyCell: function (record, node, index, options) {
+ var $cell = this._super.apply(this, arguments);
if (this.mode === 'readonly') {
- return this._super.apply(this, arguments);
- }
- var $cell = this._super(record, node);
- if (node.tag === 'field') {
- var field = this.state.fields[node.attrs.name];
- if (field.required) {
- $cell.addClass('o_required_field');
- }
- if (this._isReadonly(record, field, node)) {
- $cell.addClass('o_readonly');
- }
+ return $cell;
}
return $cell.data('index', index);
},
/**
- * Helper method, to instantiate and render a field widget. It is only
- * called by _selectCell, when editing a row. It kind of looks like the
- * _renderBodyCell method of ListRenderer, but it has to properly tag the
- * widget and to do other subtly different tasks, so it is awkward to try to
- * merge them.
- *
- * @private
- * @param {jQueryElement} $row
- * @param {Object} node
- * @param {integer} rowIndex
- * @param {integer} colIndex
- * @returns {AbstractField} the instance of the desired widget
- */
- _renderFieldWidget: function ($row, node, rowIndex, colIndex) {
- if (node.tag === 'button') {
- return;
- }
- var name = node.attrs.name;
- var record = this.state.data[rowIndex];
- var field = this.state.fields[name];
- if (!this._isEditable(record, field, node)) {
- return;
- }
- var $td = this._findTd($row, colIndex);
- var Widget = this.state.fieldsInfo.list[name].Widget;
- var widget = new Widget(this, name, record, {
- mode: 'edit',
- viewType: 'list',
- });
- if (widget.replace_element) {
- $td.empty();
- }
- $td.addClass('o_edit_mode');
- widget.appendTo($td);
- widget.__rowIndex = rowIndex;
- widget.__colIndex = colIndex;
- this.widgets.push(widget);
- return widget;
- },
- /**
* Editable rows are extended with their index, and possibly a trash icon on
* their right, to allow deleting the corresponding record.
*
@@ -362,7 +274,7 @@ ListRenderer.include({
/**
* This is one of the trickiest method in the editable renderer. It has to
* do a lot of stuff: it has to determine which cell should be selected (if
- * the terget cell is readonly, we need to find another suitable cell), then
+ * the target cell is readonly, we need to find another suitable cell), then
* unselect the current row, and activate the line where the selected cell
* is, if necessary.
*
@@ -370,118 +282,97 @@ ListRenderer.include({
* @param {integer} colIndex
*/
_selectCell: function (rowIndex, colIndex) {
- var self = this;
-
- // do nothing if user tries to select current cell
+ // Do nothing if the user tries to select current cell
if (rowIndex === this.currentRow && colIndex === this.currentCol) {
return;
}
- // make sure the colIndex is on an editable field. otherwise, find
- // next editable field, or do nothing if no such field exists
- var data = this.state.data;
- var fields = this.state.fields;
- var col, field, isEditable, record;
- for (var i = 0; i < this.columns.length; i++) {
- col = this.columns[colIndex];
- record = data[rowIndex];
- field = fields[col.attrs.name];
- isEditable = this._isEditable(record, field, col);
- if (isEditable) {
- break;
- }
- colIndex = (colIndex + 1) % this.columns.length;
- }
- if (!isEditable) {
- return;
- }
-
- // if we are just changing active cell in the same row, activate the
- // corresponding widget and return
- if (rowIndex === this.currentRow) {
- var w = _.findWhere(this.widgets, {
- __rowIndex: rowIndex,
- __colIndex: colIndex
- });
- this.currentCol = colIndex;
- w.activate();
- return;
- }
+ var record = this.state.data[rowIndex];
- // if this is a different column, then we need to prepare for it by
- // unselecting the current row. This will trigger the save.
+ // If this is a different row, then we need to prepare for it by
+ // unselecting the current row and selecting the new one.
+ var def = $.when();
if (rowIndex !== this.currentRow) {
- this._unselectRow();
+ def = this._unselectRow(); // This will trigger the save
+ def.then(this._selectRow.bind(this, rowIndex));
}
- // instantiate column widgets
- var $row = this.$('tbody tr').eq(rowIndex);
- $row.addClass('o_selected_row');
- // force the width of each cell to its current value so that it won't
+ // In all cases (new row or not), activate the proper column widget
+ var self = this;
+ def.then(function () {
+ var fieldIndex = self._activateFieldWidget(record, colIndex - getNbButtonBefore(colIndex));
+ if (fieldIndex < 0) {
+ return self._unselectRow();
+ }
+
+ self.currentCol = fieldIndex + getNbButtonBefore(fieldIndex);
+
+ function getNbButtonBefore(index) {
+ var nbButtons = 0;
+ for (var i = 0 ; i < index ; i++) {
+ if (self.columns[i].tag === 'button') {
+ nbButtons++;
+ }
+ }
+ return nbButtons;
+ }
+ });
+ },
+ /**
+ * Activates the row at the given row index.
+ *
+ * @param {integer} rowIndex
+ */
+ _selectRow: function (rowIndex) {
+ this.currentRow = rowIndex;
+
+ var self = this;
+ var record = this.state.data[rowIndex];
+
+ // Force the width of each cell to its current value so that it won't
// change if its content changes, to prevent the view from flickering
- $row.find('td').each(function () {
+ var $row = this.$('tbody > tr:nth(' + rowIndex + ')');
+ $row.addClass('o_selected_row');
+ $row.children().each(function () {
var $td = $(this);
$td.width($td.width());
});
+ // Instantiate column widgets and destroy potential readonly ones
+ var oldWidgets = _.clone(this.allFieldWidgets[record.id]);
_.each(this.columns, function (node, index) {
- var widget = self._renderFieldWidget($row, node, rowIndex, index);
- if (widget && index === colIndex) {
- widget.activate();
+ var $newTd = self._renderBodyCell(record, node, index, {
+ renderInvisible: true,
+ renderWidgets: true,
+ });
+ var $td = self._findTd($row, index);
+ if ($td.hasClass('o_list_button')) {
+ self._unregisterModifiersElement(node, record, $td.children()); // FIXME this is ugly...
}
+ $td.empty().append($newTd.contents());
});
+ _.each(oldWidgets, this._destroyFieldWidget.bind(this, record));
- this.currentRow = rowIndex;
- this.currentCol = colIndex;
this.trigger_up('change_mode', {mode: 'edit'});
},
/**
* Set the value of a cell. This method can be called when the value of a
- * cell was modified, for example afte an onchange.
+ * cell was modified, for example after an onchange.
*
* @param {integer} rowIndex
* @param {integer} colIndex
* @param {boolean} keepDirtyFlag
- * @param {boolean} forceDirtyFlag
*/
- _setCellValue: function (rowIndex, colIndex, keepDirtyFlag, forceDirtyFlag) {
+ _setCellValue: function (rowIndex, colIndex, keepDirtyFlag) {
var record = this.state.data[rowIndex];
var node = this.columns[colIndex];
- var $td = this._findTd(this.$('tbody tr').eq(rowIndex), colIndex);
- var $new_td = this._renderBodyCell(record, node, colIndex);
- if (keepDirtyFlag && $td.hasClass('o_field_dirty') || forceDirtyFlag) {
- $new_td.addClass('o_field_dirty');
+ var $oldTd = this._findTd(this.$('tbody > tr:nth(' + rowIndex + ')'), colIndex);
+ var $newTd = this._renderBodyCell(record, node, colIndex, {mode: 'readonly'});
+ if (keepDirtyFlag && $oldTd.hasClass('o_field_dirty')) {
+ $newTd.addClass('o_field_dirty');
}
- $td.replaceWith($new_td);
- },
- /**
- * Updates the row of a particular record of the editable list view.
- *
- * @param {index} index the index of the record whose row needs updating
- * @param {string[]} changedFields names of the fields which changed
- * @param {OdooEvent} event the event that triggered the change
- */
- _updateRow: function (index, changedFields, event) {
- var self = this;
- var record = this.state.data[index];
-
- // filter out fields that are not present in the view. They may come
- // from an onchange, but we need to ignore them.
- changedFields = _.filter(changedFields, function (fieldName) {
- return _.findWhere(self.columns, {name: fieldName});
- });
- _.each(changedFields, function (field_name) {
- var widget = _.findWhere(self.widgets, {name: field_name});
- if (widget && widget.__rowIndex === index) {
- widget.reset(record, event);
- widget.$el.parent().addClass('o_field_dirty');
- } else {
- var column_index = _.findIndex(self.columns, function (column) {
- return column.attrs.name === field_name;
- });
- self._setCellValue(index, column_index, true, true);
- }
- });
+ this._unregisterModifiersElement(node, record, $oldTd);
+ $oldTd.replaceWith($newTd);
},
/**
* This method is called whenever we click/move outside of a row that was
@@ -491,79 +382,76 @@ ListRenderer.include({
* If the current row is valid, then it can be unselected. If it is not
* valid, we need to check if it is dirty. If it is not dirty, it can be
* unselected (and will be removed). If it is dirty, we need to ask the user
- * for confirmation. If the user agrees, the row will be removed, otherwise
+ * for confirmation. If the user agrees, the row will be removed, otherwise
* nothing happens (it is still selected)
*
* @returns {Deferred} The deferred resolves if the row was unselected (and
- * possibly removed). If may be rejected, when the row is dirty and the
+ * possibly removed). If may be rejected, when the row is dirty and the
* user refuses to discard its changes.
*/
_unselectRow: function () {
- var self = this;
-
- // We need to protect against calling this method when no row is
- // selected.
+ // Protect against calling this method when no row is selected
if (this.currentRow === null) {
return $.when();
}
- var discarded = false;
-
- function discardLine() {
- self.trigger_up('discard_line', {id: currentLineID});
- discarded = true;
- def.resolve();
+ var self = this;
+ var record = this.state.data[this.currentRow];
+ var rowWidgets = _.clone(this.allFieldWidgets[record.id]);
+ var isRowValid = _.every(rowWidgets, this._canWidgetBeSaved.bind(this));
+ var $row = this.$('tbody > tr.o_selected_row');
+
+ // If the row is not valid and is dirty, we have to ask the user
+ // confirmation before going further
+ var def = $.when();
+ if (!isRowValid && this.dirtyRows[record.id]) {
+ def = $.Deferred();
+ var message = _t("The line has been modified, your changes will be discarded. Are you sure you want to discard the changes ?");
+ var dialog = Dialog.confirm(this, message, {
+ title: _t("Warning"),
+ confirm_callback: def.resolve.bind(def),
+ cancel_callback: def.reject.bind(def),
+ });
+ dialog.$modal.on('hidden.bs.modal', def.reject.bind(def));
}
- var rowWidgets = _.where(this.widgets, {__rowIndex: this.currentRow});
- var isRowValid = _.every(rowWidgets, function (w) { return w.isValid();});
- var $row = this.$('tbody tr.o_selected_row');
-
- var def;
-
- if (isRowValid) {
- // trigger the save (if the record isn't dirty, the datamodel won't save
- // anything)
- self.trigger_up('field_changed', {
- dataPointID: self.state.data[self.currentRow].id,
- changes: {},
- force_save: true,
- });
- self.trigger_up('change_mode', {mode: 'readonly'});
- $row.removeClass('o_selected_row');
- $row.find('td').removeClass('o_edit_mode');
- def = $.when();
- } else {
- def = $.Deferred();
- var currentLineID = this.state.data[this.currentRow].id;
- var isDirty = this.dirtyRows[currentLineID];
- if (isDirty) {
- // if the currently selected record is dirty, we want to ask for
- // confirmation before discarding it
- var message = _t("The line has been modified, your changes will be discarded. Are you sure you want to discard the changes ?");
- var options = {
- title: _t("Warning"),
- confirm_callback: discardLine,
- cancel_callback: def.reject.bind(def)
- };
- var dialog = Dialog.confirm(this, message, options);
- dialog.$modal.on('hidden.bs.modal', def.reject.bind(def));
+ return def.then(function finalizeUnselect() {
+ var rowRemoved = false;
+ var keepDirtyFlag = true;
+ if (isRowValid) {
+ // Trigger the save (if the record isn't dirty, the datamodel
+ // won't save anything)
+ self.trigger_up('field_changed', {
+ dataPointID: record.id,
+ changes: {},
+ force_save: true,
+ });
+ } else if (record.res_id) {
+ // Not a creation, tell the model to discard the changes
+ self.trigger_up('discard_changes', {id: record.id});
+ keepDirtyFlag = false;
} else {
- discardLine();
+ // Creation, tell the model to discard the row and remove the
+ // associated row DOM element
+ self.trigger_up('discard_line', {id: record.id});
+ $row.remove(); // FIXME row indexes are not updated... let's remove data('index') and check index dynamically ?
+ rowRemoved = true;
}
- }
- return def.then(function finalizeUnselect() {
- var widgets = _.where(self.widgets, {__rowIndex: self.currentRow});
- _.each(widgets, function (widget) {
- self._setCellValue(self.currentRow, widget.__colIndex, true);
- widget.destroy();
- });
- if (discarded) {
- $row.remove();
+ // Update row if it was not removed
+ if (!rowRemoved) {
+ $row.removeClass('o_selected_row');
+ _.each(self.columns, function (col, colIndex) {
+ self._setCellValue(self.currentRow, colIndex, keepDirtyFlag);
+ });
}
- self.widgets = _.without(self.widgets, widgets);
+
+ // Destroy the edition widgets
+ _.each(rowWidgets, self._destroyFieldWidget.bind(self, record));
+
+ delete self.dirtyRows[record.id];
self.currentRow = null;
+ self.trigger_up('change_mode', {mode: 'readonly'});
});
},
@@ -628,21 +516,8 @@ ListRenderer.include({
this._unselectRow();
},
/**
- * We react to field_changed events by adding the .o_field_dirty css class,
- * which changes its style to a visible orange outline. It allows the user
- * to have a visual feedback on which fields changed, and on which fields
- * have been saved.
- *
- * @param {OdooEvent} event
- */
- _onFieldChanged: function (event) {
- this.dirtyRows[event.data.dataPointID] = true;
- var $td = event.target.$el.parent();
- $td.addClass('o_field_dirty');
- },
- /**
* Clicking on a footer should unselect (and save) the currently selected
- * row. It has to be done this way, because this is a click inside this.el,
+ * row. It has to be done this way, because this is a click inside this.el,
* and _onWindowClicked ignore those clicks.
*/
_onFooterClick: function () {
@@ -669,13 +544,13 @@ ListRenderer.include({
* cell on the right, or the first on the next line. This method is called
* when the user press the TAB key.
*
- * @param {OdooEvent} event
+ * @param {OdooEvent} ev
*/
- _onMoveNext: function (event) {
+ _onMoveNext: function (ev) {
// we need to stop the event, to prevent interference with some other
// component up there, such as a form renderer.
- event.stopPropagation();
- if (this.currentCol < this.columns.length - 1) {
+ ev.stopPropagation();
+ if (this.currentCol + 1 < this.columns.length) {
this._selectCell(this.currentRow, this.currentCol + 1);
} else if (this.currentRow < this.state.data.length - 1) {
this._selectCell(this.currentRow + 1, 0);
@@ -691,10 +566,27 @@ ListRenderer.include({
}
},
/**
+ * Move the cursor on the previous available cell, so either the previous
+ * available cell on the left, or the last on the previous line. This method
+ * is called when the user press the TAB key while holding the shift key.
+ *
+ * @param {OdooEvent} ev
+ */
+ _onMovePrevious: function (ev) {
+ // we need to stop the event, to prevent interference with some other
+ // component up there, such as a form renderer.
+ ev.stopPropagation();
+ if (this.currentCol > 0) {
+ this._selectCell(this.currentRow, this.currentCol - 1);
+ } else if (this.currentRow > 0) {
+ this._selectCell(this.currentRow - 1, this.columns.length - 1);
+ }
+ },
+ /**
* Move the cursor one cell right, if possible
*/
_onMoveRight: function () {
- if (this.currentCol < this.columns.length - 1) {
+ if (this.currentCol + 1 < this.columns.length) {
this._selectCell(this.currentRow, this.currentCol + 1);
}
},
@@ -770,7 +662,6 @@ ListRenderer.include({
this._unselectRow();
},
-
});
});
diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js
index c51cae980da..b987929bce0 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -4,7 +4,6 @@ odoo.define('web.ListRenderer', function (require) {
var BasicRenderer = require('web.BasicRenderer');
var config = require('web.config');
var core = require('web.core');
-var Domain = require('web.Domain');
var field_utils = require('web.field_utils');
var Pager = require('web.Pager');
var session = require('web.session');
@@ -51,19 +50,11 @@ var ListRenderer = BasicRenderer.extend({
this._super.apply(this, arguments);
var self = this;
this.hasHandle = false;
- this._processModifiers();
this.columns = _.reject(this.arch.children, function (c) {
- if (c.attrs.invisible === '1') {
- return true;
- }
- if (c.modifiers.tree_invisible) {
- return true;
- }
if (c.attrs.widget === 'handle') {
self.hasHandle = true;
}
- c.name = c.attrs.name;
- return false;
+ return !!JSON.parse(c.attrs.modifiers || "{}").tree_invisible;
});
this.rowDecorations = _.chain(this.arch.attrs)
.pick(function (value, key) {
@@ -213,22 +204,6 @@ var ListRenderer = BasicRenderer.extend({
return this.hasSelectors ? n+1 : n;
},
/**
- * Determine if a given cell is invisible. A cell is considered invisible
- * if there is an 'invisible' attr, with a matching domain.
- *
- * @private
- * @param {Object} record a basic model record
- * @param {Object} node a node object (from the arch)
- * @returns {boolean}
- */
- _isInvisible: function (record, node) {
- if ('invisible' in node.modifiers) {
- var fieldValues = record.getEvalContext();
- return new Domain(node.modifiers.invisible).compute(fieldValues);
- }
- return false;
- },
- /**
* Render a list of <td>, with aggregates if available. It can be displayed
* in the footer, or for each open groups.
*
@@ -267,56 +242,88 @@ var ListRenderer = BasicRenderer.extend({
return $('<tbody>').append($rows);
},
/**
- * Render a cell for the table. For most cells, we only want to display the
- * formatted value, with some appropriate css class. However, when the
+ * Render a cell for the table. For most cells, we only want to display the
+ * formatted value, with some appropriate css class. However, when the
* node was explicitely defined with a 'widget' attribute, then we
* instantiate the corresponding widget.
*
* @private
* @param {Object} record
* @param {Object} node
+ * @param {integer} colIndex
+ * @param {Object} [options]
+ * @param {Object} [options.mode]
+ * @param {Object} [options.renderInvisible=false]
+ * force the rendering of invisible cell content
+ * @param {Object} [options.renderWidgets=false]
+ * force the rendering of the cell value thanks to a widget
* @returns {jQueryElement} a <td> element
*/
- _renderBodyCell: function (record, node) {
- var self = this;
- var $td = $('<td>');
- if (this._isInvisible(record, node)) {
- return $td;
+ _renderBodyCell: function (record, node, colIndex, options) {
+ var tdClassName;
+ if (node.tag === 'button') {
+ tdClassName = 'o_list_button';
+ } else if (node.attrs.widget){
+ tdClassName = ('o_' + node.attrs.widget + '_cell');
+ } else {
+ tdClassName = FIELD_CLASSES[this.state.fields[node.attrs.name].type];
}
- var name = node.attrs.name;
- if (node.attrs.widget) {
- var Widget = this.state.fieldsInfo.list[name].Widget;
- var widget = new Widget(this, name, record, {
- mode: 'readonly',
- viewType: 'list',
- });
- widget.appendTo($td);
- $td.addClass('o_' + node.attrs.widget + '_cell');
+ var $td = $('<td>', { class: tdClassName });
+
+ // We register modifiers on the <td> element so that it gets the correct
+ // modifiers classes (for styling)
+ var modifiers = this._registerModifiers(node, record, $td);
+ // If the invisible modifiers is true, the <td> element is left empty.
+ // Indeed, if the modifiers was to change the whole cell would be
+ // rerendered anyway.
+ if (modifiers.invisible && !(options && options.renderInvisible)) {
return $td;
}
+
if (node.tag === 'button') {
- var $button = $('<button type="button">').addClass('o_icon_button');
- $button.append($('<i>').addClass('fa').addClass(node.attrs.icon))
- .prop('title', node.attrs.string)
- .click(function (e) {
- e.stopPropagation();
- self.trigger_up('button_clicked', {
- attrs: node.attrs,
- record: record,
- });
- });
- $td.append($button);
- $td.addClass('o_list_button').click(function (e) {
- // prevent opening or editing the record on cell click
- e.stopPropagation();
- });
- return $td;
+ return $td.append(this._renderButton(record, node));
}
+ if (node.attrs.widget || (options && options.renderWidgets)) {
+ this.defs = []; // TODO maybe wait for those somewhere ?
+ var widget = this._renderFieldWidget(node, record, _.pick(options, 'mode'));
+ delete this.defs;
+ return $td.append(widget.$el);
+ }
+ var name = node.attrs.name;
var field = this.state.fields[name];
var value = record.data[name];
- $td.addClass(FIELD_CLASSES[field.type]);
- var formatted_value = field_utils.format[field.type](value, field, { data: record.data });
- return $td.html(formatted_value);
+ var formattedValue = field_utils.format[field.type](value, field, { data: record.data });
+ return $td.html(formattedValue);
+ },
+ /**
+ * Renders the button element associated to the given node and record.
+ *
+ * @private
+ * @param {Object} record
+ * @param {Object} node
+ * @returns {jQuery} a <button> element
+ */
+ _renderButton: function (record, node) {
+ var $button = $('<button>', {
+ type: 'button',
+ class: 'o_icon_button',
+ title: node.attrs.string,
+ });
+ $button.append($('<i>', {class: 'fa ' + node.attrs.icon}));
+
+ this._registerModifiers(node, record, $button);
+
+ // TODO this should be moved to a handler
+ var self = this;
+ $button.on("click", function (e) {
+ e.stopPropagation();
+ self.trigger_up('button_clicked', {
+ attrs: node.attrs,
+ record: record,
+ });
+ });
+
+ return $button;
},
/**
* Render a complete empty row. This is used to fill in the blanks when we
@@ -544,7 +551,10 @@ var ListRenderer = BasicRenderer.extend({
*/
_renderRow: function (record) {
var decorations = this._computeDecorationClassNames(record);
- var $cells = _.map(this.columns, this._renderBodyCell.bind(this, record));
+ var self = this;
+ var $cells = _.map(this.columns, function (node, index) {
+ return self._renderBodyCell(record, node, index, {mode: 'readonly'});
+ });
var $tr = $('<tr class="o_data_row">')
.data('id', record.id)
.addClass(decorations.length && decorations.join(' '))
@@ -623,17 +633,6 @@ var ListRenderer = BasicRenderer.extend({
return this._super();
},
/**
- * Process the modifiers by setting their parsed version on the
- * corresponding nodes.
- *
- * @private
- */
- _processModifiers: function () {
- _.each(this.arch.children, function (c) {
- c.modifiers = JSON.parse(c.attrs.modifiers || '{}');
- });
- },
- /**
* Whenever we change the state of the selected rows, we need to call this
* method to keep the this.selection variable in sync, and also to recompute
* the aggregates.
@@ -713,5 +712,4 @@ var ListRenderer = BasicRenderer.extend({
});
return ListRenderer;
-
});
diff --git a/addons/web/static/src/less/new_views.less b/addons/web/static/src/less/new_views.less
index cb43686fbeb..f8c0ca2e7b6 100644
--- a/addons/web/static/src/less/new_views.less
+++ b/addons/web/static/src/less/new_views.less
@@ -5,63 +5,44 @@
.o_list_view {
tr.o_selected_row {
outline: 1px solid lightgray;
- }
- td.o_edit_mode {
- position: relative;
- outline: 1px solid gray;
- &.o_required_field {
- outline-width: 2px;
- z-index: 1;
- }
- background-color: white;
+ > td:not(.o_readonly):not(.o_list_button) {
+ position: relative;
+ background-color: white;
- .o_form_input {
- border: none;
- padding: 0 5px;
- }
+ > .o_field_widget, > .o_form_field_many2one {
+ .o-position-absolute(0, 0, 0, 0);
+ }
- > .o_field_widget, > .o_form_field_many2one {
- .o-position-absolute(0, 0, 0, 0);
- }
+ .o_form_field_many2one .o_form_input_dropdown {
+ .o-flex(1, 1, auto);
- .o_form_field_many2one .o_form_input_dropdown {
- .o-flex(1, 1, auto);
+ .o_form_input {
+ height: 100%;
+ }
- .o_form_input {
- height: 100%;
+ .o_dropdown_button {
+ display: none;
+ }
}
- .o_dropdown_button {
- display: none;
+ .o_form_field_many2manytags {
+ .o_form_field_many2one {
+ height: 100%;
+ display: inline-block;
+ }
}
- }
- .o_form_field_many2manytags {
- .o_form_field_many2one {
- height: 100%;
- display: inline-block;
+ .o_field_boolean {
+ .o-flex-display();
+ .o-align-items(center);
}
}
-
- .o_field_boolean {
- .o-flex-display();
- .o-align-items(center);
- }
}
td.o_field_dirty {
outline: 1px solid orange;
}
- //
- // &.o_readonly {
- // outline: 2px solid lightgray;
- // }
- // div.o_field_dirty {
- // outline: 1px solid orange;
- // }
- // }
-
}
////////////////////////////////////////////////////////////////////////////////
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index f32f0b6ac61..7d7aabb5617 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -182,17 +182,17 @@ QUnit.module('basic_fields', {
// Edit a line
var $cell = list.$('tr.o_data_row:has(.o_checkbox input:checked) td:not(.o_list_record_selector)').first();
assert.ok($cell.find('.o_checkbox input:checked').prop('disabled'),
- "input should be disabled in readonly mode")
+ "input should be disabled in readonly mode");
$cell.click();
assert.ok(!$cell.find('.o_checkbox input:checked').prop('disabled'),
- "input should not have the disabled property in edit mode")
+ "input should not have the disabled property in edit mode");
$cell.find('.o_checkbox input:checked').click();
// save
list.$buttons.find('.o_list_button_save').click();
$cell = list.$('tr.o_data_row:has(.o_checkbox input:not(:checked)) td:not(.o_list_record_selector)').first();
assert.ok($cell.find('.o_checkbox input:not(:checked)').prop('disabled'),
- "input should be disabled again")
+ "input should be disabled again");
assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .o_checkbox input').length, 5,
"should still have 5 checkboxes");
assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .o_checkbox input:checked').length, 3,
@@ -200,9 +200,8 @@ QUnit.module('basic_fields', {
// Re-Edit the line and fake-check the checkbox
$cell.click();
- var $checkbox = $cell.find('.o_checkbox input:not(:checked)');
- $checkbox.click(); // Change the checkbox
- $checkbox.click(); // Undo the change
+ $cell.find('.o_checkbox input').click(); // Change the checkbox
+ $cell.find('.o_checkbox input').click(); // Undo the change
// Save
list.$buttons.find('.o_list_button_save').click();
@@ -304,7 +303,7 @@ QUnit.module('basic_fields', {
form.destroy();
});
- QUnit.test('float field in form view', function(assert) {
+ QUnit.test('float field in form view', function (assert) {
assert.expect(5);
var form = createView({
@@ -322,20 +321,20 @@ QUnit.module('basic_fields', {
assert.ok(!form.$('.o_form_field').hasClass('o_form_field_empty'),
'Float field should be considered set for value 0.');
assert.strictEqual(form.$('.o_form_field.o_form_field_number').first().text(), '0.000',
- 'The value should be displayed properly.')
+ 'The value should be displayed properly.');
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('input.o_form_input').val(), '0.000',
- 'The value should be rendered with correct precision.')
+ 'The value should be rendered with correct precision.');
form.$('input.o_form_input').val('108.2458938598598').trigger('input');
assert.strictEqual(form.$('input.o_form_input').val(), '108.2458938598598',
- 'The value should not be formated yet.')
+ 'The value should not be formated yet.');
form.$('input.o_form_input').val('18.8958938598598').trigger('input');
form.$buttons.find('.o_form_button_save').click();
assert.strictEqual(form.$('.o_form_field.o_form_field_number').first().text(), '18.896',
- 'The new value should be rounded properly.')
+ 'The new value should be rounded properly.');
form.destroy();
});
@@ -352,7 +351,7 @@ QUnit.module('basic_fields', {
'</tree>',
});
- var zeroValues = list.$('td').filter(function() {return $(this).text() === '0.000'});
+ var zeroValues = list.$('td').filter(function () {return $(this).text() === '0.000';});
assert.strictEqual(zeroValues.length, 2,
'Unset float values should be rendered as zeros.');
@@ -361,16 +360,16 @@ QUnit.module('basic_fields', {
$cell.click();
assert.strictEqual(list.$('input.o_form_input').length, 1,
- 'The view should have 1 input for editable float.')
+ 'The view should have 1 input for editable float.');
list.$('input.o_form_input').val('108.2458938598598').trigger('input');
assert.strictEqual(list.$('input.o_form_input').val(), '108.2458938598598',
- 'The value should not be formated yet.')
+ 'The value should not be formated yet.');
list.$('input.o_form_input').val('18.8958938598598').trigger('input');
list.$buttons.find('.o_list_button_save').click();
assert.strictEqual(list.$('.o_form_field.o_form_field_number').first().text(), '18.896',
- 'The new value should be rounded properly.')
+ 'The new value should be rounded properly.');
list.destroy();
});
@@ -424,7 +423,7 @@ QUnit.module('basic_fields', {
form.destroy();
});
- QUnit.test('email field in editable list view', function(assert) {
+ QUnit.test('email field in editable list view', function (assert) {
assert.expect(10);
var list = createView({
@@ -448,15 +447,15 @@ QUnit.module('basic_fields', {
// Edit a line and check the result
var $cell = list.$('tbody td:not(.o_list_record_selector)').first();
$cell.click();
- assert.ok($cell.hasClass('o_edit_mode'), 'should be set as edit mode');
+ assert.ok($cell.parent().hasClass('o_selected_row'), 'should be set as edit mode');
assert.strictEqual($cell.find('input').val(), 'yop',
'should have the corect value in internal input');
- $cell.find('input').val('new').trigger('input');;
+ $cell.find('input').val('new').trigger('input');
// save
list.$buttons.find('.o_list_button_save').click();
$cell = list.$('tbody td:not(.o_list_record_selector)').first();
- assert.ok(!$cell.hasClass('o_edit_mode'), 'should not be in edit mode anymore');
+ assert.ok(!$cell.parent().hasClass('o_selected_row'), 'should not be in edit mode anymore');
assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'new',
"value should be properly updated");
$mailtoLink = list.$('a.o_form_uri.o_field_widget.o_text_overflow');
@@ -485,7 +484,7 @@ QUnit.module('basic_fields', {
res_id: 1,
});
- var charField = form.renderer.widgets[0];
+ var charField = _.find(form.renderer.allFieldWidgets)[0];
assert.strictEqual(charField.isValid(), true);
form.destroy();
});
@@ -527,7 +526,7 @@ QUnit.module('basic_fields', {
form.destroy();
});
- QUnit.test('char field in editable list view', function(assert) {
+ QUnit.test('char field in editable list view', function (assert) {
assert.expect(6);
var list = createView({
@@ -545,15 +544,15 @@ QUnit.module('basic_fields', {
// Edit a line and check the result
var $cell = list.$('tbody td:not(.o_list_record_selector)').first();
$cell.click();
- assert.ok($cell.hasClass('o_edit_mode'), 'should be set as edit mode');
+ assert.ok($cell.parent().hasClass('o_selected_row'), 'should be set as edit mode');
assert.strictEqual($cell.find('input').val(), 'yop',
'should have the corect value in internal input');
- $cell.find('input').val('brolo').trigger('input');;
+ $cell.find('input').val('brolo').trigger('input');
// save
list.$buttons.find('.o_list_button_save').click();
$cell = list.$('tbody td:not(.o_list_record_selector)').first();
- assert.ok(!$cell.hasClass('o_edit_mode'), 'should not be in edit mode anymore');
+ assert.ok(!$cell.parent().hasClass('o_selected_row'), 'should not be in edit mode anymore');
assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'brolo',
"value should be properly updated");
list.destroy();
@@ -608,7 +607,7 @@ QUnit.module('basic_fields', {
form.destroy();
});
- QUnit.test('char field in editable list view', function(assert) {
+ QUnit.test('char field in editable list view', function (assert) {
assert.expect(10);
var list = createView({
@@ -630,15 +629,15 @@ QUnit.module('basic_fields', {
// Edit a line and check the result
var $cell = list.$('tbody td:not(.o_list_record_selector)').first();
$cell.click();
- assert.ok($cell.hasClass('o_edit_mode'), 'should be set as edit mode');
+ assert.ok($cell.parent().hasClass('o_selected_row'), 'should be set as edit mode');
assert.strictEqual($cell.find('input').val(), 'yop',
'should have the corect value in internal input');
- $cell.find('input').val('brolo').trigger('input');;
+ $cell.find('input').val('brolo').trigger('input');
// save
list.$buttons.find('.o_list_button_save').click();
$cell = list.$('tbody td:not(.o_list_record_selector)').first();
- assert.ok(!$cell.hasClass('o_edit_mode'), 'should not be in edit mode anymore');
+ assert.ok(!$cell.parent().hasClass('o_selected_row'), 'should not be in edit mode anymore');
assert.strictEqual(list.$('a.o_form_uri.o_field_widget.o_text_overflow').length, 5,
"should still have 5 anchors with correct classes");
assert.strictEqual(list.$('a.o_form_uri.o_field_widget.o_text_overflow').first().attr('href'), 'brolo',
@@ -921,7 +920,7 @@ QUnit.module('basic_fields', {
QUnit.module('HandleWidget');
QUnit.test('handle widget', function (assert) {
- assert.expect(7);
+ assert.expect(6);
this.data.partner.records[0].p = [2, 4];
var form = createView({
@@ -949,9 +948,6 @@ QUnit.module('basic_fields', {
form.$buttons.find('.o_form_button_edit').click();
- assert.ok(form.$('td:first').hasClass('o_readonly'),
- "column should be displayed as readonly");
-
assert.ok(form.$('td:first').hasClass('o_handle_cell'),
"column widget should be displayed in css class");
@@ -1677,7 +1673,7 @@ QUnit.module('basic_fields', {
// Edit a line and check the result
var $cell = list.$('tbody td:not(.o_list_record_selector)').first();
$cell.click();
- assert.ok($cell.hasClass('o_edit_mode'), 'should be set as edit mode');
+ assert.ok($cell.parent().hasClass('o_selected_row'), 'should be set as edit mode');
assert.strictEqual($cell.find('input').val(), 'yop',
'should have the corect value in internal input');
$cell.find('input').val('new').trigger('input');
@@ -1685,7 +1681,7 @@ QUnit.module('basic_fields', {
// save
list.$buttons.find('.o_list_button_save').click();
$cell = list.$('tbody td:not(.o_list_record_selector)').first();
- assert.ok(!$cell.hasClass('o_edit_mode'), 'should not be in edit mode anymore');
+ assert.ok(!$cell.parent().hasClass('o_selected_row'), 'should not be in edit mode anymore');
assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'n\u00ADew',
"value should be properly updated");
$phoneLink = list.$('a.o_form_uri.o_field_widget.o_text_overflow');
@@ -1787,15 +1783,15 @@ QUnit.module('basic_fields', {
// Edit a line and check the result
var $cell = list.$('tbody td:not(.o_list_record_selector)').first();
$cell.click();
- assert.ok($cell.hasClass('o_edit_mode'), 'should be set as edit mode');
+ assert.ok($cell.parent().hasClass('o_selected_row'), 'should be set as edit mode');
assert.strictEqual($cell.find('input').val(), 'yop',
'should have the corect value in internal input');
- $cell.find('input').val('new').trigger('input');;
+ $cell.find('input').val('new').trigger('input');
// save
list.$buttons.find('.o_list_button_save').click();
$cell = list.$('tbody td:not(.o_list_record_selector)').first();
- assert.ok(!$cell.hasClass('o_edit_mode'), 'should not be in edit mode anymore');
+ assert.ok(!$cell.parent().hasClass('o_selected_row'), 'should not be in edit mode anymore');
assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'new',
"value should be properly updated");
assert.strictEqual(list.$('span.o_field_widget.o_text_overflow:not(.o_form_uri)').length, 5,
@@ -2100,8 +2096,8 @@ QUnit.module('basic_fields', {
form.destroy();
});
- QUnit.test('state_selection widget in editable list view', function(assert) {
- assert.expect(34);
+ QUnit.test('state_selection widget in editable list view', function (assert) {
+ assert.expect(32);
var list = createView({
View: ListView,
@@ -2125,10 +2121,8 @@ QUnit.module('basic_fields', {
// Click on the status button to make the dropdown appear
var $cell = list.$('tbody td.o_state_selection_cell').first();
list.$('.o_state_selection_cell .o_selection > a span.o_status').first().click();
- assert.ok(!$cell.hasClass('o_edit_mode'),
- 'should not be in edit mode since we clicked on the widget and not the cell itself');
- assert.ok(!$cell.siblings().hasClass('o_edit_mode'),
- 'siblings should not be in edit mode since we clicked on the widget and not the cell itself');
+ assert.ok(!$cell.parent().hasClass('o_selected_row'),
+ 'should not be in edit mode since we clicked on the state selection widget');
assert.strictEqual(list.$('ul.dropdown-menu.state:visible').length, 1,
"there should be a dropdown");
assert.strictEqual(list.$('ul.dropdown-menu.state:visible li').length, 2,
@@ -2148,10 +2142,8 @@ QUnit.module('basic_fields', {
// switch to edit mode and check the result
$cell = list.$('tbody td.o_state_selection_cell').first();
$cell.click();
- assert.ok(!$cell.hasClass('o_edit_mode'),
- 'should still not be in edit mode since widget is readonly');
- assert.ok($cell.siblings().hasClass('o_edit_mode'),
- 'siblings should be in edit mode since we clicked on the cell and not the widget itself');
+ assert.ok($cell.parent().hasClass('o_selected_row'),
+ 'should now be in edit mode');
assert.strictEqual(list.$('.o_state_selection_cell .o_selection > a span.o_status').length, 5,
"should still have five status selection widgets");
assert.strictEqual(list.$('.o_state_selection_cell .o_selection > a span.o_status.o_status_red').length, 0,
@@ -2174,9 +2166,9 @@ QUnit.module('basic_fields', {
$lastCell.click();
assert.strictEqual(list.$('ul.dropdown-menu.state:visible').length, 0,
"there should not be a dropdown anymore");
- assert.ok(!$firstCell.siblings().hasClass('o_edit_mode'),
+ assert.ok(!$firstCell.parent().hasClass('o_selected_row'),
'first row should not be in edit mode anymore');
- assert.ok($lastCell.siblings().hasClass('o_edit_mode'),
+ assert.ok($lastCell.parent().hasClass('o_selected_row'),
'last row should be in edit mode');
// Click on the last status button to make the dropdown appear
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 6b23940a9db..93a19de7b81 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -835,15 +835,15 @@ QUnit.module('relational_fields', {
"display name of first record in o2m list should be 'relational record 1'");
form.$buttons.find('.o_form_button_edit').click();
form.$('.o_field_one2many tbody td').first().click();
- assert.ok(form.$('.o_field_one2many tbody td').first().hasClass('o_edit_mode'),
+ assert.ok(form.$('.o_field_one2many tbody td').first().parent().hasClass('o_selected_row'),
"first row of o2m should be in edition");
form.$('.o_field_one2many tbody td').first().find('input').val("new value").trigger('input');
- assert.ok(form.$('.o_field_one2many tbody td').first().hasClass('o_edit_mode'),
+ assert.ok(form.$('.o_field_one2many tbody td').first().parent().hasClass('o_selected_row'),
"first row of o2m should still be in edition");
// // leave o2m edition
form.$el.click();
- assert.ok(!form.$('.o_field_one2many tbody td').first().hasClass('o_edit_mode'),
+ assert.ok(!form.$('.o_field_one2many tbody td').first().parent().hasClass('o_selected_row'),
"first row of o2m should be readonly again");
// discard changes
@@ -1224,18 +1224,18 @@ QUnit.module('relational_fields', {
// edit mode, then click on Add an item and enter a value
form.$buttons.find('.o_form_button_edit').click();
form.$('.o_form_field_x2many_list_row_add a').click();
- form.$('td.o_edit_mode input').val('kartoffel').trigger('input');
+ form.$('.o_selected_row > td input').val('kartoffel').trigger('input');
// click again on Add an item
form.$('.o_form_field_x2many_list_row_add a').click();
assert.strictEqual(form.$('td:contains(kartoffel)').length, 1,
"should have one td with the new value");
- assert.strictEqual(form.$('td.o_edit_mode input').length, 1,
+ assert.strictEqual(form.$('.o_selected_row > td input').length, 1,
"should have one other new td");
assert.strictEqual(form.$('tr.o_data_row').length, 2, "should have 2 data rows");
// enter another value and save
- form.$('td.o_edit_mode input').val('gemuse').trigger('input');
+ form.$('.o_selected_row > td input').val('gemuse').trigger('input');
form.$buttons.find('.o_form_button_save').click();
assert.strictEqual(form.$('tr.o_data_row').length, 2, "should have 2 data rows");
assert.strictEqual(form.$('td:contains(kartoffel)').length, 1,
@@ -2682,7 +2682,7 @@ QUnit.module('relational_fields', {
assert.strictEqual(form.$('.o_data_row').length, 2,
"should have 2 data rows");
- assert.ok(form.$('tr.o_data_row:first:contains(My little Foo Value)').length, 1,
+ assert.strictEqual(form.$('tr.o_data_row:first input').val(), 'My little Foo Value',
"first row should be the new value");
assert.ok(form.$('tr.o_data_row:first').hasClass('o_selected_row'),
"first row should be selected");
@@ -2728,7 +2728,7 @@ QUnit.module('relational_fields', {
assert.strictEqual(form.$('.o_data_row').length, 2,
"should have 2 data rows");
- assert.ok(form.$('tr.o_data_row:eq(1):contains(My little Foo Value)').length, 1,
+ assert.strictEqual(form.$('tr.o_data_row:eq(1) input').val(), 'My little Foo Value',
"second row should be the new value");
assert.ok(form.$('tr.o_data_row:eq(1)').hasClass('o_selected_row'),
"second row should be selected");
@@ -3190,13 +3190,15 @@ QUnit.module('relational_fields', {
res_id: 1,
});
- assert.ok(form.$('.o_statusbar_status button[data-value="4"]').hasClass('btn-primary disabled'),
+ var $selectedStatus = form.$('.o_statusbar_status button[data-value="4"]');
+ assert.ok($selectedStatus.hasClass('btn-primary') && $selectedStatus.hasClass('disabled'),
"selected status should be btn-primary and disabled");
var $clickable = form.$('.o_statusbar_status button.btn-default:not(.dropdown-toggle):not(:disabled)');
assert.strictEqual($clickable.length, 2,
"other status should be btn-default and not disabled");
$clickable.last().click(); // (last is visually the first here (css))
- assert.ok(form.$('.o_statusbar_status button[data-value="1"]').hasClass("btn-primary disabled"),
+ var $status = form.$('.o_statusbar_status button[data-value="1"]');
+ assert.ok($status.hasClass("btn-primary") && $status.hasClass("disabled"),
"value should have been updated");
form.destroy();
diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js
index de5e02934f0..d7778d0923d 100644
--- a/addons/web/static/tests/helpers/mock_server.js
+++ b/addons/web/static/tests/helpers/mock_server.js
@@ -154,7 +154,7 @@ var MockServer = Class.extend({
},
/**
* helper: read a string describing an arch, and returns a simulated
- * 'field_view_get' call to the server.
+ * 'fields_view_get' call to the server.
*
* @private
* @param {string|Object} arch a string OR a parsed xml document
@@ -165,6 +165,7 @@ var MockServer = Class.extend({
*/
_fieldsViewGet: function (arch, model, fields) {
var self = this;
+ var modifiersNames = ['invisible', 'readonly', 'required'];
var onchanges = this.data[model].onchanges || {};
var fieldNodes = {};
@@ -173,11 +174,45 @@ var MockServer = Class.extend({
arch = utils.xml_to_json(doc, true);
}
+ var inTreeView = (arch.tag === 'tree');
+
this._traverse(arch, function (node) {
if (typeof node === "string") {
return false;
}
var modifiers = {};
+
+ var isField = (node.tag === 'field');
+
+ if (isField) {
+ fieldNodes[node.attrs.name] = node;
+
+ // 'transfer_field_to_modifiers' simulation
+ var field = fields[node.attrs.name];
+
+ var defaultValues = {};
+ var stateExceptions = {};
+ _.each(modifiersNames, function (attr) {
+ stateExceptions[attr] = [];
+ defaultValues[attr] = !!field[attr];
+ });
+ _.each(field['states'] || {}, function (modifs, state) {
+ _.each(modifs, function (modif) {
+ if (defaultValues[modif[0]] !== modif[1]) {
+ stateExceptions[modif[0]].append(state);
+ }
+ });
+ });
+ _.each(defaultValues, function (defaultValue, attr) {
+ if (stateExceptions[attr].length) {
+ modifiers[attr] = [("state", defaultValue ? "not in" : "in", stateExceptions[attr])];
+ } else {
+ modifiers[attr] = defaultValue;
+ }
+ });
+ }
+
+ // 'transfer_node_to_modifiers' simulation
if (node.attrs.attrs) {
var attrs = pyeval.py_eval(node.attrs.attrs);
_.extend(modifiers, attrs);
@@ -189,20 +224,26 @@ var MockServer = Class.extend({
}
modifiers.invisible.push(["state", "not in", node.attrs.states.split(",")]);
}
- _.each(["invisible", "readonly", "required"], function (a) {
+ _.each(modifiersNames, function (a) {
if (node.attrs[a]) {
var v = pyeval.py_eval(node.attrs[a]);
- if (v || modifiers[a] === undefined) {
+ if (inTreeView && a === 'invisible') {
+ modifiers['tree_invisible'] = v;
+ } else if (v || !(a in modifiers) || !_.isArray(modifiers[a])) {
modifiers[a] = v;
}
}
});
+
+ // 'transfer_modifiers_to_node' simulation
+ _.each(modifiersNames, function (a) {
+ if (a in modifiers && (!!modifiers[a] === false || (_.isArray(modifiers[a]) && !modifiers[a].length))) {
+ delete modifiers[a];
+ }
+ });
node.attrs.modifiers = JSON.stringify(modifiers);
- if (node.tag === 'field') {
- fieldNodes[node.attrs.name] = node;
- return false;
- }
- return true;
+
+ return !isField;
});
var relModel, relFields;
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index 2a81286b257..b777fde31c6 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -205,7 +205,7 @@ QUnit.module('Views', {
form.destroy();
});
- QUnit.test('invisible fields are not rendered', function (assert) {
+ QUnit.test('invisible fields are properly hidden', function (assert) {
assert.expect(3);
var form = createView({
@@ -248,16 +248,13 @@ QUnit.module('Views', {
'</group>' +
'</group>' +
'<notebook>' +
- '<page string="visible">' +
- '</page>' +
- '<page string="invisible" invisible="1">' +
- '</page>' +
+ '<page string="visible"/>' +
+ '<page string="invisible" invisible="1"/>' +
'</notebook>' +
'</sheet>' +
'</form>',
res_id: 1,
});
-
assert.strictEqual(form.$('.o_notebook li.o_form_invisible a:contains(invisible)').length, 1,
"should not display tab invisible");
assert.strictEqual(form.$('table.o_inner_group.o_form_invisible td:contains(invgroup)').length, 1,
@@ -265,7 +262,7 @@ QUnit.module('Views', {
form.destroy();
});
- QUnit.test('invisible attrs are re-evaluated on field changed', function (assert) {
+ QUnit.test('invisible attrs on fields are re-evaluated on field change', function (assert) {
assert.expect(3);
// we set the value bar to simulate a falsy boolean value.
@@ -277,7 +274,7 @@ QUnit.module('Views', {
data: this.data,
arch: '<form string="Partners">' +
'<sheet><group>' +
- '<field name="product_id" invisible="1"/>' +
+ '<field name="product_id"/>' +
'<field name="timmy" invisible="1"/>' +
'<field name="foo" class="foo_field" attrs=\'{"invisible": [["product_id", "=", false]]}\'/>' +
'<field name="bar" class="bar_field" attrs=\'{"invisible":[("bar","=",False),("timmy","=",[])]}\'/>' +
@@ -295,7 +292,7 @@ QUnit.module('Views', {
var $dropdown = form.$('.o_form_field_many2one input').autocomplete('widget');
form.$('.o_form_field_many2one input').click();
$dropdown.find('li:first()').click();
- assert.ok(!form.$('.foo_field').hasClass('o_form_invisible'), 'should not display foo field');
+ assert.ok(!form.$('.foo_field').hasClass('o_form_invisible'), 'should display foo field');
form.destroy();
});
@@ -343,7 +340,7 @@ QUnit.module('Views', {
form.destroy();
});
- QUnit.test('invisible attrs on group', function (assert) {
+ QUnit.test('invisible attrs on group are re-evaluated on field change', function (assert) {
assert.expect(2);
var form = createView({
@@ -434,7 +431,6 @@ QUnit.module('Views', {
form.destroy();
});
-
QUnit.test('label uses the string attribute', function (assert) {
assert.expect(1);
@@ -458,15 +454,50 @@ QUnit.module('Views', {
form.destroy();
});
+ QUnit.test('readonly attrs on fields are re-evaluated on field change', function (assert) {
+ assert.expect(3);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo" attrs="{\'readonly\': [[\'bar\', \'=\', True]]}"/>' +
+ '<field name="bar"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('span[name="foo"]').length, 1,
+ "the foo field widget should be readonly");
+ form.$('.o_field_boolean input').click();
+ assert.strictEqual(form.$('input[name="foo"]').length, 1,
+ "the foo field widget should have been rerendered to now be editable");
+ form.$('.o_field_boolean input').click();
+ assert.strictEqual(form.$('span[name="foo"]').length, 1,
+ "the foo field widget should have been rerendered to now be readonly again");
+
+ form.destroy();
+ });
QUnit.test('empty fields have o_form_empty class in readonly mode', function (assert) {
- assert.expect(4);
+ assert.expect(8);
this.data.partner.fields.foo.default = false; // no default value for this test
this.data.partner.records[1].foo = false; // 1 is record with id=2
this.data.partner.records[1].trululu = false; // 1 is record with id=2
- this.data.partner.fields.trululu.readonly = true;
this.data.partner.fields.int_field.readonly = true;
+ this.data.partner.onchanges.foo = function (obj) {
+ if (obj.foo === "hello") {
+ obj.int_field = false;
+ }
+ };
+
var form = createView({
View: FormView,
model: 'partner',
@@ -475,7 +506,7 @@ QUnit.module('Views', {
'<sheet>' +
'<group>' +
'<field name="foo"/>' +
- '<field name="trululu"/>' +
+ '<field name="trululu" attrs="{\'readonly\': [[\'foo\', \'=\', False]]}"/>' +
'<field name="int_field"/>' +
'</group>' +
'</sheet>' +
@@ -484,16 +515,31 @@ QUnit.module('Views', {
});
assert.strictEqual(form.$('.o_form_field.o_form_field_empty').length, 2,
- "should have 2 empty fields with correct class");
+ "should have 2 empty fields with correct class");
assert.strictEqual(form.$('.o_form_label_empty').length, 2,
- "should have 2 muted labels (for the empty fieds) in readonly");
+ "should have 2 muted labels (for the empty fieds) in readonly");
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('.o_form_field_empty').length, 1,
- "in edit mode, only readonly fields should have .o_form_field_empty class");
+ "in edit mode, only empty readonly fields should have the o_form_field_empty class");
assert.strictEqual(form.$('.o_form_label_empty').length, 1,
- "in edit mode, only readonly fields should have .o_form_label_empty class");
+ "in edit mode, only labels associated to empty readonly fields should have the o_form_label_empty class");
+
+ form.$('input[name="foo"]').val("test").trigger("input");
+
+ assert.strictEqual(form.$('.o_form_field_empty').length, 0,
+ "after readonly modifier change, the o_form_field_empty class should have been removed");
+ assert.strictEqual(form.$('.o_form_label_empty').length, 0,
+ "after readonly modifier change, the o_form_label_empty class should have been removed");
+
+ form.$('input[name="foo"]').val("hello").trigger("input");
+
+ assert.strictEqual(form.$('.o_form_field_empty').length, 1,
+ "after value changed to false for a readonly field, the o_form_field_empty class should have been added");
+ assert.strictEqual(form.$('.o_form_label_empty').length, 1,
+ "after value changed to false for a readonly field, the o_form_label_empty class should have been added");
+
form.destroy();
});
@@ -559,6 +605,37 @@ QUnit.module('Views', {
form.destroy();
});
+ QUnit.test('required attrs on fields are re-evaluated on field change', function (assert) {
+ assert.expect(3);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo" attrs="{\'required\': [[\'bar\', \'=\', True]]}"/>' +
+ '<field name="bar"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('input[name="foo"].o_form_required').length, 1,
+ "the foo field widget should be required");
+ form.$('.o_field_boolean input').click();
+ assert.strictEqual(form.$('input[name="foo"]:not(.o_form_required)').length, 1,
+ "the foo field widget should now have been marked as non-required");
+ form.$('.o_field_boolean input').click();
+ assert.strictEqual(form.$('input[name="foo"].o_form_required').length, 1,
+ "the foo field widget should now have been marked as required again");
+
+ form.destroy();
+ });
+
QUnit.test('required fields should have o_form_required in readonly mode', function (assert) {
assert.expect(2);
@@ -947,7 +1024,6 @@ QUnit.module('Views', {
form.destroy();
});
-
QUnit.test('make default record with non empty one2many', function (assert) {
assert.expect(4);
@@ -1066,7 +1142,7 @@ QUnit.module('Views', {
form.destroy();
});
- QUnit.test('buttons in footer are moved to $buttons if required', function (assert) {
+ QUnit.test('buttons in footer are moved to $buttons if necessary', function (assert) {
// not sure about this test...
assert.expect(2);
@@ -1139,7 +1215,6 @@ QUnit.module('Views', {
form.destroy();
});
-
QUnit.test('buttons with attr "special" do not trigger a save', function (assert) {
assert.expect(4);
@@ -1532,7 +1607,6 @@ QUnit.module('Views', {
form.destroy();
});
-
QUnit.test('restore local state when switching to another record', function (assert) {
assert.expect(4);
@@ -1703,7 +1777,6 @@ QUnit.module('Views', {
form.destroy();
});
-
QUnit.test('all group children have correct layout classname', function (assert) {
assert.expect(2);
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index cd04cecbc39..20a92efbcb2 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -105,7 +105,6 @@ QUnit.module('Views', {
list.destroy();
});
-
QUnit.test('simple editable rendering', function (assert) {
assert.expect(9);
@@ -249,7 +248,7 @@ QUnit.module('Views', {
});
QUnit.test('editable list view: readonly fields cannot be edited', function (assert) {
- assert.expect(3);
+ assert.expect(4);
this.data.foo.fields.foo.readonly = true;
@@ -267,9 +266,14 @@ QUnit.module('Views', {
var $second_td = list.$('td:not(.o_list_record_selector)').eq(1);
var $third_td = list.$('td:not(.o_list_record_selector)').eq(2);
$td.click();
- assert.ok(!$td.hasClass('o_edit_mode'), "foo cells should not be editable");
- assert.ok($second_td.hasClass('o_edit_mode'), "bar cells should be editable");
- assert.ok(!$third_td.hasClass('o_edit_mode'), "int_field cells should not be editable");
+ assert.ok($td.parent().hasClass('o_selected_row'),
+ "row should be in edit mode");
+ assert.ok($td.hasClass('o_readonly'),
+ "foo cell should be readonly in edit mode");
+ assert.ok(!$second_td.hasClass('o_readonly'),
+ "bar cell should be editable");
+ assert.ok($third_td.hasClass('o_readonly'),
+ "int_field cell should be readonly in edit mode");
list.destroy();
});
@@ -284,12 +288,12 @@ QUnit.module('Views', {
});
var $td = list.$('td:not(.o_list_record_selector)').first();
- assert.strictEqual($td.hasClass('o_edit_mode'), false, "td should not be in edit mode");
+ assert.ok(!$td.parent().hasClass('o_selected_row'), "td should not be in edit mode");
$td.click();
- assert.strictEqual($td.hasClass('o_edit_mode'), true, "td should be in edit mode");
- assert.strictEqual($td.hasClass('o_field_dirty'), false, "td should not be dirty");
+ assert.ok($td.parent().hasClass('o_selected_row'), "td should be in edit mode");
+ assert.ok(!$td.hasClass('o_field_dirty'), "td should not be dirty");
$td.find('input').val('abc').trigger('input');
- assert.strictEqual($td.hasClass('o_field_dirty'), true, "td should be dirty");
+ assert.ok($td.hasClass('o_field_dirty'), "td should be dirty");
list.destroy();
});
@@ -629,7 +633,6 @@ QUnit.module('Views', {
list.destroy();
});
-
QUnit.test('list without import button', function (assert) {
assert.expect(1);
@@ -825,7 +828,7 @@ QUnit.module('Views', {
"the div for the table should take the full height");
- assert.ok(list.$('tbody tr:eq(0) td:eq(1)').hasClass('o_edit_mode'),
+ assert.ok(list.$('tbody tr:eq(0)').hasClass('o_selected_row'),
"the date field td should be in edit mode");
assert.strictEqual(list.$('tbody tr:eq(0) td:eq(1)').text().trim(), "",
"the date field td should not have any content");
@@ -982,10 +985,10 @@ QUnit.module('Views', {
// edit first row
list.$('tbody tr:nth(0) td:nth(2)').click(); // click on first row to edit it
- assert.equal(list.$('tbody tr:nth(0) td:nth(4)').html(), "",
- "td that contains an invisible field should be empty");
- assert.equal(list.$('tbody tr:nth(0) td:nth(1)').html(), "",
- "td that contains an invisible button should be empty");
+ assert.strictEqual(list.$('tbody tr:nth(0) td:nth(4) input.o_form_invisible').length, 1,
+ "td that contains an invisible field should not be empty in edition");
+ assert.strictEqual(list.$('tbody tr:nth(0) td:nth(1) > button.o_form_invisible').length, 1,
+ "td that contains an invisible button should not be empty in edition");
list.$buttons.find('.o_list_button_discard').click(); // leave edition
// click on the invisible field's cell to edit first row
@@ -1065,11 +1068,12 @@ QUnit.module('Views', {
list.$('.o_data_row:first td:nth(1)').click();
assert.ok(list.$('.o_data_row:first').hasClass('o_selected_row'),
"first row should be selected");
- assert.ok(list.$('.o_data_row:first td:nth(1)').hasClass('o_readonly'),
- "foo field cells should have class 'o_readonly'");
- assert.strictEqual(list.$('.o_data_row:first td:nth(1)').text(), 'yop',
- "no widget should be rendered for readonly fields");
- assert.ok(list.$('.o_data_row:first td:nth(2)').hasClass('o_edit_mode'),
+ var $cell = list.$('.o_data_row:first td:nth(1)');
+ assert.ok($cell.hasClass('o_readonly') && $cell.parent().hasClass('o_selected_row'),
+ "foo field cells should have class 'o_readonly' and the row should be in edition");
+ assert.strictEqual(list.$('.o_data_row:first td:nth(1) span').text(), 'yop',
+ "a widget should have been rendered for readonly fields");
+ assert.ok(list.$('.o_data_row:first td:nth(2)').parent().hasClass('o_selected_row'),
"field 'int_field' should be in edition");
assert.strictEqual(list.$('.o_data_row:first td:nth(2) input').length, 1,
"a widget for field 'int_field should have been rendered'");
@@ -1260,7 +1264,198 @@ QUnit.module('Views', {
"should still have 4 records");
list.destroy();
});
-});
+ QUnit.test('invisible attrs on fields are re-evaluated on field change', function (assert) {
+ assert.expect(7);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch:
+ '<tree editable="top">' +
+ '<field name="foo" attrs="{\'invisible\': [[\'bar\', \'=\', True]]}"/>' +
+ '<field name="bar"/>' +
+ '</tree>',
+ });
+
+ assert.strictEqual(list.$('tbody td.o_form_invisible').length, 3,
+ "there should be 3 invisible foo cells in readonly mode");
+
+ // Make first line editable
+ list.$('tbody tr:nth(0) td:nth(1)').click();
+
+ assert.strictEqual(list.$('tbody tr:nth(0) td:nth(1) > input[name="foo"].o_form_invisible').length, 1,
+ "the foo field widget should have been rendered as invisible");
+
+ list.$('tbody tr:nth(0) td:nth(2) input').click();
+ assert.strictEqual(list.$('tbody tr:nth(0) td:nth(1) > input[name="foo"]:not(.o_form_invisible)').length, 1,
+ "the foo field widget should have been marked as non-invisible");
+ assert.strictEqual(list.$('tbody td.o_form_invisible').length, 2,
+ "the foo field widget parent cell should not be invisible anymore");
+
+ list.$('tbody tr:nth(0) td:nth(2) input').click();
+ assert.strictEqual(list.$('tbody tr:nth(0) td:nth(1) > input[name="foo"].o_form_invisible').length, 1,
+ "the foo field widget should have been marked as invisible again");
+ assert.strictEqual(list.$('tbody td.o_form_invisible').length, 3,
+ "the foo field widget parent cell should now be invisible again");
+
+ // Reswitch the cell to editable and save the row
+ list.$('tbody tr:nth(0) td:nth(2) input').click();
+ list.$('thead').click();
+
+ assert.strictEqual(list.$('tbody td.o_form_invisible').length, 2,
+ "there should be 2 invisible foo cells in readonly mode");
+
+ list.destroy();
+ });
+
+ QUnit.test('readonly attrs on fields are re-evaluated on field change', function (assert) {
+ assert.expect(7);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch:
+ '<tree editable="top">' +
+ '<field name="foo" attrs="{\'readonly\': [[\'bar\', \'=\', True]]}"/>' +
+ '<field name="bar"/>' +
+ '</tree>',
+ });
+
+ assert.strictEqual(list.$('tbody td.o_readonly').length, 3,
+ "there should be 3 readonly foo cells in readonly mode");
+
+ // Make first line editable
+ list.$('tbody tr:nth(0) td:nth(1)').click();
+
+ assert.strictEqual(list.$('tbody tr:nth(0) td:nth(1) > span[name="foo"]').length, 1,
+ "the foo field widget should have been rendered as readonly");
+
+ list.$('tbody tr:nth(0) td:nth(2) input').click();
+ assert.strictEqual(list.$('tbody tr:nth(0) td:nth(1) > input[name="foo"]').length, 1,
+ "the foo field widget should have been rerendered as editable");
+ assert.strictEqual(list.$('tbody td.o_readonly').length, 2,
+ "the foo field widget parent cell should not be readonly anymore");
+
+ list.$('tbody tr:nth(0) td:nth(2) input').click();
+ assert.strictEqual(list.$('tbody tr:nth(0) td:nth(1) > span[name="foo"]').length, 1,
+ "the foo field widget should have been rerendered as readonly");
+ assert.strictEqual(list.$('tbody td.o_readonly').length, 3,
+ "the foo field widget parent cell should now be readonly again");
+
+ // Reswitch the cell to editable and save the row
+ list.$('tbody tr:nth(0) td:nth(2) input').click();
+ list.$('thead').click();
+
+ assert.strictEqual(list.$('tbody td.o_readonly').length, 2,
+ "there should be 2 readonly foo cells in readonly mode");
+
+ list.destroy();
+ });
+
+ QUnit.test('required attrs on fields are re-evaluated on field change', function (assert) {
+ assert.expect(7);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch:
+ '<tree editable="top">' +
+ '<field name="foo" attrs="{\'required\': [[\'bar\', \'=\', True]]}"/>' +
+ '<field name="bar"/>' +
+ '</tree>',
+ });
+
+ assert.strictEqual(list.$('tbody td.o_form_required').length, 3,
+ "there should be 3 required foo cells in readonly mode");
+
+ // Make first line editable
+ list.$('tbody tr:nth(0) td:nth(1)').click();
+
+ assert.strictEqual(list.$('tbody tr:nth(0) td:nth(1) > input[name="foo"].o_form_required').length, 1,
+ "the foo field widget should have been rendered as required");
+
+ list.$('tbody tr:nth(0) td:nth(2) input').click();
+ assert.strictEqual(list.$('tbody tr:nth(0) td:nth(1) > input[name="foo"]:not(.o_form_required)').length, 1,
+ "the foo field widget should have been marked as non-required");
+ assert.strictEqual(list.$('tbody td.o_form_required').length, 2,
+ "the foo field widget parent cell should not be required anymore");
+
+ list.$('tbody tr:nth(0) td:nth(2) input').click();
+ assert.strictEqual(list.$('tbody tr:nth(0) td:nth(1) > input[name="foo"].o_form_required').length, 1,
+ "the foo field widget should have been marked as required again");
+ assert.strictEqual(list.$('tbody td.o_form_required').length, 3,
+ "the foo field widget parent cell should now be required again");
+
+ // Reswitch the cell to editable and save the row
+ list.$('tbody tr:nth(0) td:nth(2) input').click();
+ list.$('thead').click();
+
+ assert.strictEqual(list.$('tbody td.o_form_required').length, 2,
+ "there should be 2 required foo cells in readonly mode");
+
+ list.destroy();
+ });
+
+ QUnit.test('discarding (or not) changes after row edition', function (assert) {
+ assert.expect(8);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch:
+ '<tree editable="bottom">' +
+ '<field name="foo" required="1"/>' +
+ '<field name="bar"/>' +
+ '</tree>',
+ });
+
+ // Start first line edition
+ var $firstFooTd = list.$('tbody tr:nth(0) td:nth(1)');
+ $firstFooTd.click();
+
+ // Remove required foo field value
+ $firstFooTd.find('input').val("").trigger("input");
+
+ // Try starting other line edition
+ var $secondFooTd = list.$('tbody tr:nth(1) td:nth(1)');
+ $secondFooTd.click();
+
+ assert.strictEqual($firstFooTd.parent('.o_selected_row').length, 1,
+ "first line should still be in edition as cannot be auto discarded");
+ assert.strictEqual(list.$('tbody tr.o_selected_row').length, 1,
+ "no other line should be in edition");
+
+ // Prevent discard of first line
+ $('.modal-footer:visible > .btn-default').click();
+
+ assert.strictEqual($firstFooTd.parent('.o_selected_row').length, 1,
+ "first line should still be in edition as not discarded");
+ assert.strictEqual(list.$('tbody tr.o_selected_row').length, 1,
+ "no other line should be in edition");
+
+ // Retry starting other line edition
+ $secondFooTd.click();
+
+ assert.strictEqual($firstFooTd.parent('.o_selected_row').length, 1,
+ "first line should still be in edition as cannot be auto discarded");
+ assert.strictEqual(list.$('tbody tr.o_selected_row').length, 1,
+ "no other line should be in edition");
+
+ // Discard first line
+ $('.modal-footer:visible > .btn-primary').click();
+
+ assert.strictEqual($secondFooTd.parent('.o_selected_row').length, 1,
+ "second line should now be in edition");
+ assert.strictEqual(list.$('tbody tr.o_selected_row').length, 1,
+ "no other line should be in edition");
+
+ list.destroy();
+ });
+});
});