diff --git a/addons/web/static/src/js/_deprecated/basic_fields.js b/addons/web/static/src/js/_deprecated/basic_fields.js index 2a70c3793c0..7920edbb47d 100644 --- a/addons/web/static/src/js/_deprecated/basic_fields.js +++ b/addons/web/static/src/js/_deprecated/basic_fields.js @@ -91,6 +91,28 @@ var FieldBoolean = AbstractField.extend({ // Private //-------------------------------------------------------------------------- + /** + * @private + * @override + * @params {Object} extraInfo + * @params {boolean} extraInfo.value + */ + _quickEdit: function (extraInfo) { + this._super(...arguments); + // toggle if extraInfo.value === undefined (if clicked on label) + // otherwise force the new value + this._setValue(extraInfo.value === undefined ? !this.value : extraInfo.value); + }, + /** + * @private + * @override + * @returns {Object} + */ + _getQuickEditExtraInfo() { + return { + value: !this.value, + }; + }, /** * The actual checkbox is designed in css to have full control over its * appearance, as opposed to letting the browser and the os decide how @@ -103,7 +125,7 @@ var FieldBoolean = AbstractField.extend({ _render: function () { var $checkbox = this._formatValue(this.value); this.$input = $checkbox.find('input'); - this.$input.prop('disabled', this.mode === 'readonly'); + this.$input.prop('disabled', this.hasReadonlyModifier); this.$el.addClass($checkbox.attr('class')); this.$el.empty().append($checkbox.contents()); }, diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js index 16e928ea288..cbb9267f933 100644 --- a/addons/web/static/src/js/fields/abstract_field.js +++ b/addons/web/static/src/js/fields/abstract_field.js @@ -35,12 +35,25 @@ var Widget = require('web.Widget'); var AbstractField = Widget.extend({ events: { + 'click': '_onClick', 'keydown': '_onKeydown', }, custom_events: { navigation_move: '_onNavigationMove', }, + /** + * Determines if the field can be quick editable which means that when + * the field is clicked and if it's quick editable, it will trigger an event + * to the form view to switch into edit mode and then it'll perform + * some action (@see quickEdit) + */ + isQuickEditable: false, + /** + * List of selectors matching elements that must never trigger the 'quick_edit' + * event when being clicked on. + */ + quickEditExclusion: [], /** * An object representing fields to be fetched by the model eventhough not present in the view * This object contains "field name" as key and an object as value. @@ -164,6 +177,9 @@ var AbstractField = Widget.extend({ // recreate a new field widget. this.mode = options.mode || "readonly"; + // the widget can be readonly because of a modifier + this.hasReadonlyModifier = options.hasReadonlyModifier || false; + // this flag tracks if the widget is in a valid state, meaning that the // current value represented in the DOM is a value that can be parsed // and saved. For example, a float field can only use a number and not @@ -194,6 +210,7 @@ var AbstractField = Widget.extend({ if (this.attrs.decorations) { this.resetOnAnyFieldChange = true; } + this._canQuickEdit = this.isQuickEditable; }, /** * When a field widget is appended to the DOM, its start method is called, @@ -206,6 +223,7 @@ var AbstractField = Widget.extend({ return this._super.apply(this, arguments).then(function () { self.$el.attr('name', self.name); self.$el.addClass('o_field_widget'); + self.$el.toggleClass('o_quick_editable', self._canQuickEdit); return self._render(); }); }, @@ -254,6 +272,19 @@ var AbstractField = Widget.extend({ * @returns {Promise|undefined} */ commitChanges: function () {}, + /** + * This function is called when the form view auto focus a field for + * quick editing. Some fields have a special behaviour for the quick edit + * like the checkbox: when we click on checkbox to quick-edit it, + * it toggles its value as we've already been in edit mode. + * + * @param {any} extraInfo info to change the behaviour + */ + quickEdit: function (extraInfo) { + if (this._canQuickEdit && this.mode !== "readonly") { + this._quickEdit(extraInfo); + } + }, /** * Returns the main field's DOM element (jQuery form) which can be focused * by the browser. @@ -379,6 +410,14 @@ var AbstractField = Widget.extend({ self.$el.toggleClass(className, isToggled); }); }, + /** + * @private + * @see quickEdit + * @param {any} extraInfo info to change the behaviour + */ + _quickEdit: function (extraInfo) { + this.activate({noAutomaticCreate: true}); + }, /** * Converts the value from the field to a string representation. * @@ -403,6 +442,14 @@ var AbstractField = Widget.extend({ _getClassFromDecoration: function (decoration) { return `text-${decoration.split('-')[1]}`; }, + /** + * @private + * @param {MouseEvent} ev + * @returns {Object} + */ + _getQuickEditExtraInfo(ev) { + return {}; + }, /** * Compares the given value with the last value that has been set. * Note that we compare unparsed values. Handles the special case where no @@ -548,6 +595,23 @@ var AbstractField = Widget.extend({ // Handlers //-------------------------------------------------------------------------- + /** + * Triggers quick edit only if the field is authorized to do it. + * + * @private + * @param {MouseEvent} ev + */ + _onClick: function (ev) { + if (this._canQuickEdit && this.mode === 'readonly' && + !this.quickEditExclusion.some(x => ev.target.closest(x)) + ) { + this.trigger_up('quick_edit', { + fieldName: this.name, + target: this.el, + extraInfo: this._getQuickEditExtraInfo(ev), + }); + } + }, /** * Intercepts navigation keyboard events to prevent their default behavior * and notifies the view so that it can handle it its own way. diff --git a/addons/web/static/src/js/fields/abstract_field_owl.js b/addons/web/static/src/js/fields/abstract_field_owl.js index 43e3d468d78..2035c9d7a6f 100644 --- a/addons/web/static/src/js/fields/abstract_field_owl.js +++ b/addons/web/static/src/js/fields/abstract_field_owl.js @@ -62,11 +62,13 @@ odoo.define('web.AbstractFieldOwl', function (require) { constructor() { super(...arguments); + this._canQuickEdit = this.constructor.isQuickEditable; this._isValid = true; // 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; + useListener('click', this._onClick); useListener('keydown', this._onKeydown); useListener('navigation-move', this._onNavigationMove); onMounted(() => this._applyDecorations()); @@ -87,6 +89,7 @@ odoo.define('web.AbstractFieldOwl', function (require) { const res = super.__patch(...arguments); this.el.setAttribute('name', this.name); this.el.classList.add('o_field_widget'); + this.el.classList.toggle('o_quick_editable', this._canQuickEdit); return res; } /** @@ -171,6 +174,14 @@ odoo.define('web.AbstractFieldOwl', function (require) { return this.attrs.widget in field_utils.format ? this.attrs.widget : this.field.type; } + /** + * Returns true if the component is readonly from a modifier + * + * @returns {boolean} + */ + get hasReadonlyModifier() { + return this.options.hasReadonlyModifier || false; + } /** * Returns whether or not the field is empty and can thus be hidden. This * method is typically called when the component is in readonly, to hide it @@ -376,6 +387,19 @@ odoo.define('web.AbstractFieldOwl', function (require) { * @returns {Promise|undefined} */ commitChanges() {} + /** + * This function is called when the form view auto focus a field for + * quick editing. Some fields have a special behaviour for the quick edit + * like the checkbox: when we click on checkbox to quick-edit it, + * it toggles its value as we've already been in edit mode. + * + * @param {any} extraInfo info to change the behaviour + */ + quickEdit(extraInfo) { + if (this._canQuickEdit && this.mode !== 'readonly') { + this._quickEdit(extraInfo); + } + } /** * Remove the invalid class on a field * @@ -429,6 +453,14 @@ odoo.define('web.AbstractFieldOwl', function (require) { this.el.classList.toggle(className, isToggled); } } + /** + * @private + * @see quickEdit + * @param {any} extraInfo info to change the behaviour + */ + _quickEdit(extraInfo) { + this.activate({noAutomaticCreate: true}); + } /** * Converts the value from the field to a string representation. * @@ -453,6 +485,14 @@ odoo.define('web.AbstractFieldOwl', function (require) { _getClassFromDecoration(decoration) { return `text-${decoration.split('-')[1]}`; } + /** + * @private + * @param {MouseEvent} ev + * @returns {Object} + */ + _getQuickEditExtraInfo(ev) { + return {}; + } /** * Compares the given value with the last value that has been set. * Note that we compare unparsed values. Handles the special case where no @@ -546,6 +586,23 @@ odoo.define('web.AbstractFieldOwl', function (require) { // Handlers //---------------------------------------------------------------------- + /** + * Triggers quick edit only if the field is authorized to do it. + * + * @private + * @param {MouseEvent} ev + */ + _onClick(ev) { + if (this._canQuickEdit && + !this.constructor.quickEditExclusion.some(x => ev.target.closest(x)) + ) { + this.trigger('quick-edit', { + fieldName: this.name, + target: this.el, + extraInfo: this._getQuickEditExtraInfo(ev), + }); + } + } /** * Intercepts navigation keyboard events to prevent their default behavior * and notifies the view so that it can handle it its own way. @@ -643,6 +700,18 @@ odoo.define('web.AbstractFieldOwl', function (require) { * If set, this value will be displayed as column name. */ AbstractField.label = ""; + /** + * Determines if the field can be quick editable which means that when + * the field is clicked and if it's quick editable, it will trigger an event + * to the form view to switch into edit mode and then it'll perform + * some action (@see quickEdit) + */ + AbstractField.isQuickEditable = false; + /** + * List of selectors matching elements that must never trigger the 'quick_edit' + * event when being clicked on. + */ + AbstractField.quickEditExclusion = []; return AbstractField; }); diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index e8b54e0e732..0844030833f 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -568,6 +568,7 @@ var FieldChar = InputField.extend(TranslatableFieldMixin, { className: 'o_field_char', tagName: 'span', supportedFieldTypes: ['char'], + isQuickEditable: true, //-------------------------------------------------------------------------- // Private @@ -657,6 +658,7 @@ var FieldDateRange = InputField.extend({ '/web/static/lib/daterangepicker/daterangepicker.css', ], supportedFieldTypes: ['date', 'datetime'], + isQuickEditable: true, /** * @override */ @@ -813,6 +815,7 @@ var FieldDate = InputField.extend({ className: "o_field_date", tagName: "span", supportedFieldTypes: ['date', 'datetime'], + isQuickEditable: true, // we don't need to listen on 'input' nor 'change' events because the // datepicker widget is already listening, and will correctly notify changes events: AbstractField.prototype.events, @@ -885,6 +888,15 @@ var FieldDate = InputField.extend({ _doDebouncedAction: function () { this.datewidget.changeDatetime(); }, + /** + * @private + * @override + */ + _quickEdit: function () { + if (this.datewidget) { + this.datewidget.$input.click(); + } + }, /** * return the datepicker value @@ -949,6 +961,7 @@ var FieldDate = InputField.extend({ var FieldDateTime = FieldDate.extend({ description: _lt("Date & Time"), supportedFieldTypes: ['datetime'], + isQuickEditable: true, /** * @override @@ -1055,6 +1068,7 @@ var FieldMonetary = NumericField.extend({ tagName: 'span', supportedFieldTypes: ['float', 'monetary'], resetOnAnyFieldChange: true, // Have to listen to currency changes + isQuickEditable: true, /** * Float fields using a monetary widget have an additional currency_field @@ -1165,6 +1179,7 @@ var FieldInteger = NumericField.extend({ description: _lt("Integer"), className: 'o_field_integer o_field_number', supportedFieldTypes: ['integer'], + isQuickEditable: true, //-------------------------------------------------------------------------- // Private @@ -1199,6 +1214,7 @@ var FieldFloat = NumericField.extend({ description: _lt("Decimal"), className: 'o_field_float o_field_number', supportedFieldTypes: ['float'], + isQuickEditable: true, /** * Float fields have an additional precision parameter that is read from @@ -1221,6 +1237,7 @@ var FieldFloatTime = FieldFloat.extend({ // 'float_time', but for the sake of clarity, we explicitely define a // FieldFloatTime widget with formatType = 'float_time'. formatType: 'float_time', + isQuickEditable: true, init: function () { this._super.apply(this, arguments); @@ -1232,6 +1249,7 @@ var FieldFloatFactor = FieldFloat.extend({ supportedFieldTypes: ['float'], className: 'o_field_float_factor', formatType: 'float_factor', + isQuickEditable: true, /** * @constructor @@ -1423,6 +1441,7 @@ var FieldText = InputField.extend(TranslatableFieldMixin, { className: 'o_field_text', supportedFieldTypes: ['text', 'html'], tagName: 'span', + isQuickEditable: true, /** * @constructor @@ -1463,6 +1482,19 @@ var FieldText = InputField.extend(TranslatableFieldMixin, { } }); }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @override + */ + _quickEdit: function () { + this.activate({noselect: true, noAutomaticCreate: true}); + }, + //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- @@ -1519,6 +1551,7 @@ var FieldEmail = InputField.extend({ }), prefix: 'mailto', supportedFieldTypes: ['char'], + isQuickEditable: true, /** * In readonly, emails should be a link, not a span. @@ -1622,6 +1655,7 @@ var UrlWidget = InputField.extend({ 'click': '_onClick', }), supportedFieldTypes: ['char'], + isQuickEditable: true, /** * Urls are links in readonly mode. @@ -2669,6 +2703,7 @@ var LabelSelection = AbstractField.extend({ var BooleanToggle = FieldBoolean.extend({ description: _lt("Toggle"), className: FieldBoolean.prototype.className + ' o_boolean_toggle', + isQuickEditable: true, events: { 'click': '_onClick' }, @@ -2737,6 +2772,7 @@ var FieldPercentPie = AbstractField.extend({ description: _lt("Percentage Pie"), template: 'FieldPercentPie', supportedFieldTypes: ['integer', 'float'], + isQuickEditable: true, /** * Register some useful references for later use throughout the widget. diff --git a/addons/web/static/src/js/fields/basic_fields_owl.js b/addons/web/static/src/js/fields/basic_fields_owl.js index 507f883a85a..86da1feccb9 100644 --- a/addons/web/static/src/js/fields/basic_fields_owl.js +++ b/addons/web/static/src/js/fields/basic_fields_owl.js @@ -82,6 +82,33 @@ odoo.define('web.basic_fields_owl', function (require) { this.el.querySelector('label').setAttribute('for', id); } + //---------------------------------------------------------------------- + // Private + //---------------------------------------------------------------------- + + /** + * @private + * @override + * @param {Object} extraInfo + * @param {boolean} extraInfo.value + */ + _quickEdit(extraInfo) { + super._quickEdit(...arguments); + // toggle if extraInfo.value === undefined (if clicked on label) + // otherwise force the new value + this._setValue(extraInfo.value === undefined ? !this.value : extraInfo.value); + } + /** + * @private + * @override + * @returns {Object} + */ + _getQuickEditExtraInfo() { + return { + value: !this.value, + }; + } + //---------------------------------------------------------------------- // Handlers //---------------------------------------------------------------------- @@ -123,6 +150,7 @@ odoo.define('web.basic_fields_owl', function (require) { FieldBoolean.description = _lt("Checkbox"); FieldBoolean.supportedFieldTypes = ['boolean']; FieldBoolean.template = 'web.FieldBoolean'; + FieldBoolean.isQuickEditable = true; return { diff --git a/addons/web/static/src/js/fields/field_wrapper.js b/addons/web/static/src/js/fields/field_wrapper.js index ad32d046d79..f396f7d6f37 100644 --- a/addons/web/static/src/js/fields/field_wrapper.js +++ b/addons/web/static/src/js/fields/field_wrapper.js @@ -93,6 +93,9 @@ odoo.define('web.FieldWrapper', function (require) { get noLabel() { return this.Component.noLabel; } + get isQuickEditable() { + return this.Component.isQuickEditable; + } //---------------------------------------------------------------------- // Public @@ -104,6 +107,9 @@ odoo.define('web.FieldWrapper', function (require) { commitChanges() { return this.componentRef.comp.commitChanges(...arguments); } + quickEdit() { + return this.componentRef.comp.quickEdit(...arguments); + } getFocusableElement() { return $(this.componentRef.comp.focusableElement); } @@ -127,7 +133,17 @@ odoo.define('web.FieldWrapper', function (require) { return isSet; } isValid() { - return this.componentRef.comp.isValid; + if (this.componentRef.comp) { + return this.componentRef.comp.isValid; + } + // because of the willStart, the real field component may not be + // instantiated yet when the renderer first asks if it is set + // (only the wrapper is instantiated), so we instantiate one + // with the same props, get its 'isValid' status, and destroy it. + const c = new this.Component(null, this.props); + const isValid = c.isValid; + c.destroy(); + return isValid; } removeInvalidClass() { return this.componentRef.comp.removeInvalidClass(...arguments); diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index 213998b9274..39c66abbfab 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -92,10 +92,10 @@ var FieldMany2One = AbstractField.extend({ 'focusout input': '_onInputFocusout', 'keyup input': '_onInputKeyup', 'click .o_external_button': '_onExternalButtonClick', - 'click': '_onClick', }), AUTOCOMPLETE_DELAY: 200, SEARCH_MORE_LIMIT: 320, + isQuickEditable: true, /** * @override @@ -352,6 +352,14 @@ var FieldMany2One = AbstractField.extend({ } return tmp; }, + /** + * @private + * @override + */ + _quickEdit: function () { + this._super(...arguments); + this._toggleAutoComplete(); + }, /** * @private * @returns {Array} @@ -442,6 +450,18 @@ var FieldMany2One = AbstractField.extend({ }); return values; }, + /** + * @private + */ + _toggleAutoComplete: function () { + if (this.$input.autocomplete("widget").is(":visible")) { + this.$input.autocomplete("close"); + } else if (this.floating) { + this.$input.autocomplete("search"); // search with the input's content + } else { + this.$input.autocomplete("search", ''); // search with the empty string + } + }, /** * Listens to events 'field_changed' to keep track of the last event that * has been trigerred. This allows to detect that all changes have been @@ -692,22 +712,26 @@ var FieldMany2One = AbstractField.extend({ /** * @private + * @override * @param {MouseEvent} event */ _onClick: function (event) { var self = this; - if (this.mode === 'readonly' && !this.noOpen) { + if (this.mode === 'readonly') { event.preventDefault(); - event.stopPropagation(); - this._rpc({ + if (this.noOpen) { + this._super(...arguments); + } else { + event.stopPropagation(); + this._rpc({ model: this.field.relation, method: 'get_formview_action', args: [[this.value.res_id]], context: this.record.getContext(this.recordParams), - }) - .then(function (action) { + }).then(function (action) { self.trigger_up('do_action', {action: action}); }); + } } }, @@ -764,13 +788,7 @@ var FieldMany2One = AbstractField.extend({ * @private */ _onInputClick: function () { - if (this.$input.autocomplete("widget").is(":visible")) { - this.$input.autocomplete("close"); - } else if (this.floating) { - this.$input.autocomplete("search"); // search with the input's content - } else { - this.$input.autocomplete("search", ''); // search with the empty string - } + this._toggleAutoComplete(); }, /** * @private @@ -1056,6 +1074,12 @@ var FieldX2Many = AbstractField.extend(WidgetAdapterMixin, { * useSubview is used in form view to load view of the related model of the x2many field */ useSubview: true, + isQuickEditable: true, + quickEditExclusion: [ + '.o_x2m_control_panel', + 'thead', + '.o_widget', + ], /** * @override @@ -1095,6 +1119,9 @@ var FieldX2Many = AbstractField.extend(WidgetAdapterMixin, { !!JSON.parse(arch.attrs.delete) : true; this.editable = arch.attrs.editable; + this._canQuickEdit = arch.tag === 'tree'; + } else { + this._canQuickEdit = false; } this._computeAvailableActions(record); if (this.attrs.columnInvisibleFields) { @@ -1264,6 +1291,29 @@ var FieldX2Many = AbstractField.extend(WidgetAdapterMixin, { new Domain(this.nodeOptions.delete, evalContext).compute(evalContext) : true; }, + /** + * @private + * @override + * @param {Object} extraInfo + * @param {string} extraInfo.row + * @param {string} extraInfo.subFieldName + */ + _quickEdit: function (extraInfo) { + const parts = []; + if (extraInfo.row) { + parts.push(`.o_data_row[data-id="${extraInfo.row}"]`); + } + if (extraInfo.subFieldName) { + parts.push(`[name="${extraInfo.subFieldName}"]`); + } + + if (parts.length) { + const el = this.el.querySelector(parts.join(' ')); + if (el) { + el.click(); + } + } + }, /** * Evaluates the 'column_invisible' modifier for the parent record. * @@ -1290,6 +1340,22 @@ var FieldX2Many = AbstractField.extend(WidgetAdapterMixin, { create_text: this.nodeOptions.create_text, }; }, + /** + * @private + * @override + * @param {MouseEvent} ev + * @returns {Object} + */ + _getQuickEditExtraInfo: function (ev) { + const row = ev.target.closest('.o_data_row'); + const field = ev.target.closest('.o_data_row .o_field_widget') || + ev.target.closest('.o_field_cell'); + + return { + row: row && row.dataset.id, + subFieldName: row && field && field.getAttribute('name'), + }; + }, /** * Computes the default renderer to use depending on the view type. * We create this as a method so we can override it if we want to use @@ -1311,7 +1377,7 @@ var FieldX2Many = AbstractField.extend(WidgetAdapterMixin, { * @returns {boolean} true iff the list should contain a 'create' line. */ _hasCreateLine: function () { - return !this.isReadonly && ( + return !this.hasReadonlyModifier && ( (this.activeActions.create && this.canCreate) || (this.isMany2Many) ); @@ -1364,7 +1430,8 @@ var FieldX2Many = AbstractField.extend(WidgetAdapterMixin, { addCreateLine: this._hasCreateLine(), addTrashIcon: this._hasTrashIcon(), isMany2Many: this.isMany2Many, - no_open: this.isReadonly && toBoolElse(arch.attrs.no_open || '', false), + no_open: (this.isReadonly && !this.hasReadonlyModifier) && + (this._canQuickEdit || toBoolElse(arch.attrs.no_open || '', false)), columnInvisibleFields: this.currentColInvisibleFields, }); } @@ -1844,6 +1911,7 @@ var FieldOne2Many = FieldX2Many.extend({ //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- + /** * @override * @param {Object} record @@ -1870,6 +1938,60 @@ var FieldOne2Many = FieldX2Many.extend({ // Private //-------------------------------------------------------------------------- + /** + * @private + * @param {*} data + */ + _addCreateRecordRow(data) { + const self = this; + if (this.editable || data.forceEditable) { + if (!this.activeActions.create) { + if (data.onFail) { + data.onFail(); + } + } else if (!this.creatingRecord) { + this.creatingRecord = true; + this.trigger_up('edited_list', { id: this.value.id }); + this._setValue({ + operation: 'CREATE', + position: this.editable || data.forceEditable, + context: data.context, + }, { + allowWarning: data.allowWarning + }).then(function () { + self.creatingRecord = false; + }).then(function (){ + if (data.onSuccess){ + data.onSuccess(); + } + }).guardedCatch(function() { + self.creatingRecord = false; + }); + } + } else { + this._openFormDialog({ + context: data.context && data.context[0], + disable_multiple_selection: data.disable_multiple_selection, + on_saved: function (record) { + self._setValue({ operation: 'ADD', id: record.id }); + }, + }); + } + }, + /** + * @private + * @override + * @param {Object} extraInfo + * @param {boolean} [extraInfo.__addRecord] + * @param {Object} [extraInfo.data] + */ + _quickEdit: function (extraInfo) { + if (extraInfo.__addRecord) { + this._addCreateRecordRow(extraInfo.data); + } else { + this._super(...arguments); + } + }, /** * @override * @private @@ -1947,45 +2069,19 @@ var FieldOne2Many = FieldX2Many.extend({ * to the list even if warnings are triggered (e.g: stock warning for product availability) */ _onAddRecord: function (ev) { - var self = this; - var data = ev.data || {}; + const data = ev.data || {}; // we don't want interference with the components upstream. ev.stopPropagation(); - if (this.editable || data.forceEditable) { - if (!this.activeActions.create) { - if (data.onFail) { - data.onFail(); - } - } else if (!this.creatingRecord) { - this.creatingRecord = true; - this.trigger_up('edited_list', { id: this.value.id }); - this._setValue({ - operation: 'CREATE', - position: this.editable || data.forceEditable, - context: data.context, - }, { - allowWarning: data.allowWarning - }).then(function () { - self.creatingRecord = false; - }).then(function (){ - if (data.onSuccess){ - data.onSuccess(); - } - }).guardedCatch(function() { - self.creatingRecord = false; - }) - ; - } - } else { - this._openFormDialog({ - context: data.context && data.context[0], - disable_multiple_selection: data.disable_multiple_selection, - on_saved: function (record) { - self._setValue({ operation: 'ADD', id: record.id }); - }, + if (this._canQuickEdit && this.isReadonly) { + this.trigger_up('quick_edit', { + fieldName: this.name, + target: this.el, + extraInfo: { __addRecord: true, data }, }); + } else { + this._addCreateRecordRow(data); } }, /** @@ -2584,6 +2680,8 @@ var FormFieldMany2ManyTags = FieldMany2ManyTags.extend({ 'mousedown .o_colorpicker a': '_onUpdateColor', 'mousedown .o_colorpicker .o_hide_in_kanban': '_onUpdateColor', }), + isQuickEditable: true, + quickEditExclusion: ['.dropdown-toggle'], /** * @override */ @@ -2591,6 +2689,20 @@ var FormFieldMany2ManyTags = FieldMany2ManyTags.extend({ this._super.apply(this, arguments); this.hasDropdown = !!this.colorField; + this._canQuickEdit = !this.nodeOptions.no_edit_color; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @override + */ + _quickEdit: function () { + this._super(...arguments); + this.many2one.$input.click(); }, //-------------------------------------------------------------------------- @@ -2671,7 +2783,7 @@ var KanbanFieldMany2ManyTags = FieldMany2ManyTags.extend({ // KanbanRecord, which should definitely be cleaned. // Anyway, those handlers are only necessary in Form and List views, so we // can removed them here. - events: AbstractField.prototype.events, + events: _.omit(AbstractField.prototype.events, 'click'), //-------------------------------------------------------------------------- // Private @@ -2713,6 +2825,7 @@ var FieldMany2ManyCheckBoxes = AbstractField.extend({ }), specialData: "_fetchSpecialRelation", supportedFieldTypes: ['many2many'], + isQuickEditable: true, init: function () { this._super.apply(this, arguments); this.m2mValues = this.record.specialData[this.name]; @@ -2722,6 +2835,13 @@ var FieldMany2ManyCheckBoxes = AbstractField.extend({ // Public //-------------------------------------------------------------------------- + /** + * @override + * @returns {jQuery} + */ + getFocusableElement: function () { + return this.$el; + }, isSet: function () { return true; }, @@ -2731,30 +2851,55 @@ var FieldMany2ManyCheckBoxes = AbstractField.extend({ //-------------------------------------------------------------------------- /** + * @override + * @private + * @param {MouseEvent} e + * @returns {Object} + */ + _getQuickEditExtraInfo(e) { + const ids = new Set(this.value.res_ids); + + let input = $(e.target); + if (e.target.matches('label')) { + input = this.$(`input#${e.target.getAttribute('for')}`); + } + const id = input.data('record-id'); + if (ids.has(id)) { + ids.delete(id); + } else { + ids.add(id); + } + + return { + ids: Array.from(ids), + }; + }, + /** + * @override + * @private + * @param {Object} extraInfo + * @param {number[]} [extraInfo.ids] + */ + _quickEdit(extraInfo) { + if (extraInfo.hasOwnProperty('ids')) { + this._setValue({ + operation: 'REPLACE_WITH', + ids: extraInfo.ids, + }); + } + }, + /** + * @override * @private */ - _renderCheckboxes: function () { + _render: function () { var self = this; this.m2mValues = this.record.specialData[this.name]; this.$el.html(qweb.render(this.template, {widget: this})); _.each(this.value.res_ids, function (id) { self.$('input[data-record-id="' + id + '"]').prop('checked', true); }); - }, - /** - * @override - * @private - */ - _renderEdit: function () { - this._renderCheckboxes(); - }, - /** - * @override - * @private - */ - _renderReadonly: function () { - this._renderCheckboxes(); - this.$("input").prop("disabled", true); + this.$("input").prop("disabled", this.hasReadonlyModifier); }, //-------------------------------------------------------------------------- @@ -2765,13 +2910,15 @@ var FieldMany2ManyCheckBoxes = AbstractField.extend({ * @private */ _onChange: function () { - var ids = _.map(this.$('input:checked'), function (input) { - return $(input).data("record-id"); - }); - this._setValue({ - operation: 'REPLACE_WITH', - ids: ids, - }); + if (this.mode !== 'readonly') { + var ids = _.map(this.$('input:checked'), function (input) { + return $(input).data("record-id"); + }); + this._setValue({ + operation: 'REPLACE_WITH', + ids: ids, + }); + } }, }); @@ -2899,6 +3046,7 @@ var FieldSelection = AbstractField.extend({ events: _.extend({}, AbstractField.prototype.events, { 'change': '_onChange', }), + isQuickEditable: true, /** * @override */ @@ -3034,6 +3182,7 @@ var FieldRadio = FieldSelection.extend({ events: _.extend({}, AbstractField.prototype.events, { 'click input': '_onInputClick', }), + isQuickEditable: true, /** * @constructs FieldRadio */ @@ -3103,7 +3252,7 @@ var FieldRadio = FieldSelection.extend({ index: index, name: self.unique_id, value: value, - disabled: self.mode !== 'edit', + disabled: self.hasReadonlyModifier, })); }); }, 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 16ecc473915..260de94c7a9 100644 --- a/addons/web/static/src/js/views/basic/basic_renderer.js +++ b/addons/web/static/src/js/views/basic/basic_renderer.js @@ -727,6 +727,10 @@ var BasicRenderer = AbstractRenderer.extend(WidgetAdapterMixin, { var Widget = record.fieldsInfo[this.viewType][fieldName].Widget; const legacy = !(Widget.prototype instanceof owl.Component); const widgetOptions = { + // Distinct readonly from renderer and readonly from modifier, + // renderer can be readonly while modifier not. + // This is needed as modifiers are set after first render + hasReadonlyModifier: modifiers.readonly, mode: modifiers.readonly ? 'readonly' : mode, viewType: this.viewType, }; 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 a68135cd77a..3fbb43a3413 100644 --- a/addons/web/static/src/js/views/form/form_controller.js +++ b/addons/web/static/src/js/views/form/form_controller.js @@ -18,6 +18,7 @@ var FormController = BasicController.extend({ toggle_column_order: '_onToggleColumnOrder', focus_control_button: '_onFocusControlButton', form_dialog_discarded: '_onFormDialogDiscarded', + quick_edit: '_onQuickEdit', }), /** * @override @@ -436,6 +437,19 @@ var FormController = BasicController.extend({ } return this._super.apply(this, arguments); }, + /** + * @private + * @returns {Promise} + */ + _setEditMode: function () { + this._disableButtons(); + // wait for potential pending changes to be saved (done with widgets + // allowing to edit in readonly) + return this.mutex.getUnlockedDef() + .then(this._setMode.bind(this, 'edit')) + .then(this._enableButtons.bind(this)) + .guardedCatch(this._enableButtons.bind(this)); + }, /** * Calls unfreezeOrder when changing the mode. * Also, when there is a change of mode, the tracking of last activated @@ -556,16 +570,9 @@ var FormController = BasicController.extend({ /** * Called when the user wants to edit the current record -> @see _setMode * - * @private */ _onEdit: function () { - this._disableButtons(); - // wait for potential pending changes to be saved (done with widgets - // allowing to edit in readonly) - this.mutex.getUnlockedDef() - .then(this._setMode.bind(this, 'edit')) - .then(this._enableButtons.bind(this)) - .guardedCatch(this._enableButtons.bind(this)); + this._setEditMode(); }, /** * This method is called when someone tries to freeze the order, most likely @@ -670,6 +677,15 @@ var FormController = BasicController.extend({ title: _t("Open: ") + ev.data.string, }).open(); }, + /** + * @private + * @param {OdooEvent} ev + */ + _onQuickEdit: async function (ev) { + ev.stopPropagation(); + await this._setEditMode(); + this.renderer.quickEdit(ev.data); + }, /** * Called when the user wants to save the current record -> @see saveRecord * 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 b6e1593b215..46adbf5383d 100644 --- a/addons/web/static/src/js/views/form/form_renderer.js +++ b/addons/web/static/src/js/views/form/form_renderer.js @@ -19,14 +19,21 @@ var FormRenderer = BasicRenderer.extend({ 'click .o_notification_box .oe_field_translate': '_onTranslate', 'click .o_notification_box .close': '_onTranslateNotificationClose', 'shown.bs.tab a[data-toggle="tab"]': '_onNotebookTabChanged', + 'click .o_form_label': '_onFieldLabelClicked', }), custom_events: _.extend({}, BasicRenderer.prototype.custom_events, { 'navigation_move':'_onNavigationMove', 'activate_next_widget' : '_onActivateNextWidget', + 'quick_edit': '_onQuickEdit', }), // default col attributes for the rendering of groups INNER_GROUP_COL: 2, OUTER_GROUP_COL: 2, + quickEditExclusion: [ + '.o_list_view tbody', + '.oe_button_box', + '.oe_subtotal_footer', + ], /** * @override @@ -205,6 +212,10 @@ var FormRenderer = BasicRenderer.extend({ */ getLocalState: function () { const state = {}; + const sheetBg = this.el.querySelector('.o_form_sheet_bg'); + if (sheetBg) { + state.scrollValue = sheetBg.scrollTop; + } for (const notebook of this.el.querySelectorAll(':scope div.o_notebook')) { const name = notebook.dataset.name; const navs = notebook.querySelectorAll(':scope .o_notebook_headers .nav-item > .nav-link'); @@ -214,6 +225,24 @@ var FormRenderer = BasicRenderer.extend({ } return state; }, + /** + * Quick edits the field we clicked on before the mode switch. + * + * @param {Object} quickEditInfo + * @param {string} quickEditInfo.fieldName + * @param {any} quickEditInfo.extraInfo + */ + quickEdit: function (quickEditInfo) { + const { fieldName, extraInfo } = quickEditInfo; + // We cannot use the field's id because a same field could be defined + // twice, one in edit only and the other in read only. Because of this + // case, we have to search the field by its name and check if it is focusable. + const fieldWidget = this.allFieldWidgets[this.state.id] + .find(field => field.name === fieldName && field.isFocusable()); + if (fieldWidget) { + fieldWidget.quickEdit(extraInfo); + } + }, /** * Reset the tracking of the last activated field. The fast entry with * keyboard navigation needs to track the last activated field in order to @@ -261,6 +290,10 @@ var FormRenderer = BasicRenderer.extend({ core.bus.trigger('DOM_updated'); } } + const sheetBg = this.el.querySelector('.o_form_sheet_bg'); + if (sheetBg) { + sheetBg.scrollTop = state.scrollValue; + } }, /** * @override method from AbstractRenderer @@ -1112,6 +1145,24 @@ var FormRenderer = BasicRenderer.extend({ var index = this.allFieldWidgets[this.state.id].indexOf(ev.data.target); this._activateNextFieldWidget(this.state, index); }, + /** + * @private + * @param {MouseEvent} ev + */ + _onFieldLabelClicked: function (ev) { + const idForLabel = ev.currentTarget.getAttribute('for'); + const entry = Object.entries(this.idsForLabels) + .find(x => x[1] === idForLabel); + if (entry) { + const fieldWidget = this.allFieldWidgets[this.state.id] + .find(field => field[symbol] === entry[0]); + this.trigger_up('quick_edit', { + fieldName: fieldWidget.name, + target: ev.currentTarget, + extraInfo: {}, + }); + } + }, /** * @override * @private @@ -1149,6 +1200,17 @@ var FormRenderer = BasicRenderer.extend({ _onNotebookTabChanged: function () { core.bus.trigger('DOM_updated'); }, + /** + * @private + * @param {OdooEvent} ev + */ + _onQuickEdit: function (ev) { + if (this.mode !== 'readonly' || + this.quickEditExclusion.some(x => ev.data.target.closest(x)) + ) { + ev.stopPropagation(); + } + }, /** * open the translation view for the current field * 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 912bed1ff71..0f66554857d 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -568,7 +568,7 @@ var ListRenderer = BasicRenderer.extend({ if (field.type !== 'boolean') { title = formatter(value, field, _.extend(formatOptions, {escape: false})); } - return $td.html(formattedValue).attr('title', title); + return $td.html(formattedValue).attr('title', title).attr('name', name); }, /** * Renders the button element associated to the given node and record. diff --git a/addons/web/static/src/scss/form_view.scss b/addons/web/static/src/scss/form_view.scss index ec356efb629..e6d2cd725ad 100644 --- a/addons/web/static/src/scss/form_view.scss +++ b/addons/web/static/src/scss/form_view.scss @@ -98,6 +98,10 @@ display: inline-block; } } + + .o_quick_editable:not(.o_form_uri) { + cursor: default; + } } .o_form_uri { diff --git a/addons/web/static/src/xml/fields.xml b/addons/web/static/src/xml/fields.xml index ce271ba2123..ee2e4f1fdae 100644 --- a/addons/web/static/src/xml/fields.xml +++ b/addons/web/static/src/xml/fields.xml @@ -2,8 +2,8 @@ - + diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js index 7069246e7dc..7af03e92ae6 100644 --- a/addons/web/static/tests/chrome/action_manager_tests.js +++ b/addons/web/static/tests/chrome/action_manager_tests.js @@ -550,6 +550,7 @@ QUnit.module('ActionManager', { assert.containsOnce(actionManager, '.o_form_view'); await testUtils.dom.click(actionManager.$('.o_form_view .o_data_row:first')); + await testUtils.nextTick(); // wait for quick edit assert.containsOnce(document.body, '.modal .o_form_view'); await actionManager.doAction(1); // target != 'new' @@ -581,6 +582,7 @@ QUnit.module('ActionManager', { assert.containsOnce(actionManager, '.o_form_view'); await testUtils.dom.click(actionManager.$('.o_form_view .o_data_row:first')); + await testUtils.nextTick(); // wait for quick edit assert.containsOnce(document.body, '.modal .o_form_view'); await actionManager.doAction(5); // target 'new' diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 192859fb877..05dab6201ad 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -212,7 +212,7 @@ QUnit.module('basic_fields', { QUnit.module('FieldBoolean'); QUnit.test('boolean field in form view', async function (assert) { - assert.expect(13); + assert.expect(15); var form = await createView({ View: FormView, @@ -224,11 +224,15 @@ QUnit.module('basic_fields', { assert.containsOnce(form, '.o_field_boolean input:checked', "checkbox should be checked"); + assert.containsNone(form, '.o_field_boolean input:disabled', + 'checkbox should not be disabled'); // switch to edit mode and check the result await testUtils.form.clickEdit(form); assert.containsOnce(form, '.o_field_boolean input:checked', "checkbox should still be checked"); + assert.containsNone(form, '.o_field_boolean input:disabled', + 'checkbox should not be disabled'); // uncheck the checkbox await testUtils.dom.click(form.$('.o_field_boolean input:checked')); @@ -350,6 +354,41 @@ QUnit.module('basic_fields', { list.destroy(); }); + QUnit.test('readonly boolean field', async function (assert) { + assert.expect(6); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + `, + res_id: 1, + }); + + assert.containsOnce(form, '.o_field_boolean input:checked', + "checkbox should be checked"); + assert.containsOnce(form, '.o_field_boolean input:disabled', + 'checkbox should be disabled'); + + await testUtils.form.clickEdit(form); + assert.containsOnce(form, '.o_field_boolean input:checked', + "checkbox should still be checked"); + assert.containsOnce(form, '.o_field_boolean input:disabled', + 'checkbox should still be disabled'); + + await testUtils.dom.click(form.$('.o_field_boolean label')); + + assert.containsOnce(form, '.o_field_boolean input:checked', + "checkbox should still be checked"); + assert.containsOnce(form, '.o_field_boolean input:disabled', + 'checkbox should still be disabled'); + + form.destroy(); + }); + QUnit.module('FieldBooleanToggle'); QUnit.test('use boolean toggle widget in form view', async function (assert) { @@ -2879,10 +2918,19 @@ QUnit.module('basic_fields', { data: this.data, arch: '
' + '' + - '' + - '' + + '' + + // use kanban view as the tree will trigger edit mode + // and thus won't display the field + '' + '' + - '' + + '' + + '' + + '
' + + '' + + '
' + + '
' + + '
' + + '' + '' + '' + '' + @@ -2903,11 +2951,11 @@ QUnit.module('basic_fields', { }); assert.verifySteps(["The view's image should have been fetched"]); - assert.containsOnce(form, 'tr.o_data_row', + assert.containsOnce(form, '.o_kanban_record.oe_kanban_global_click', 'There should be one record in the many2many'); // Actual flow: click on an element of the m2m to get its form view - await testUtils.dom.click(form.$('tbody td:contains(gold)')); + await testUtils.dom.click(form.$('.oe_kanban_global_click')); assert.strictEqual($('.modal').length, 1, 'The modal should have opened'); assert.verifySteps(["The dialog's image should have been fetched"]); @@ -3032,10 +3080,19 @@ QUnit.module('basic_fields', { data: this.data, arch: '
' + '' + - '' + - '' + + '' + + // use kanban view as the tree will trigger edit mode + // and thus won't display the field + '' + '' + - '' + + '' + + '' + + '
' + + '' + + '
' + + '
' + + '
' + + '' + '' + '' + '' + @@ -3056,11 +3113,11 @@ QUnit.module('basic_fields', { }); assert.verifySteps(["The view's image should have been fetched"]); - assert.containsOnce(form, 'tr.o_data_row', + assert.containsOnce(form, '.o_kanban_record.oe_kanban_global_click', 'There should be one record in the many2many'); // Actual flow: click on an element of the m2m to get its form view - await testUtils.dom.click(form.$('tbody td:contains(gold)')); + await testUtils.dom.click(form.$('.oe_kanban_global_click')); assert.strictEqual($('.modal').length, 1, 'The modal should have opened'); assert.verifySteps(["The dialog's image should have been fetched"]); @@ -4524,7 +4581,8 @@ QUnit.module('basic_fields', { '' + '' + '
' + - '' + + // display datetime in readonly as modal will open in edit + '' + '' + '' + '', @@ -4568,7 +4626,8 @@ QUnit.module('basic_fields', { '' + '' + '
' + - '' + + // display datetime in readonly as modal will open in edit + '' + '' + '' + '', diff --git a/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js b/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js index ed5fcd7f985..4bacfee8862 100644 --- a/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js +++ b/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js @@ -459,8 +459,8 @@ QUnit.module('fields', {}, function () { }); assert.containsNone(form.$('.o_list_record_remove'), 'delete icon should not be visible in readonly'); - assert.containsNone(form.$('.o_field_x2many_list_row_add'), - '"Add an item" should not be visible in readonly'); + assert.containsOnce(form, '.o_field_x2many_list_row_add', + '"Add an item" should be visible in readonly'); await testUtils.form.clickEdit(form); @@ -566,8 +566,8 @@ QUnit.module('fields', {}, function () { assert.ok(!form.$('.o_list_record_remove').length, 'delete icon should not be visible in readonly'); - assert.ok(!form.$('.o_field_x2many_list_row_add').length, - '"Add an item" should not be visible in readonly'); + assert.ok(form.$('.o_field_x2many_list_row_add').length, + '"Add an item" should be visible in readonly'); await testUtils.form.clickEdit(form); @@ -701,8 +701,8 @@ QUnit.module('fields', {}, function () { res_id: 1, }); - assert.containsNone(form, '.o_field_x2many_list_row_add', - '"Add an item" link should not be available in readonly'); + assert.containsOnce(form, '.o_field_x2many_list_row_add', + '"Add an item" link should be available in readonly'); await testUtils.form.clickEdit(form); diff --git a/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js b/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js index 48c9ceee9d5..b830803527d 100644 --- a/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js +++ b/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js @@ -431,7 +431,7 @@ QUnit.module('fields', {}, function () { }); QUnit.test('show_address works in a view embedded in a view of another type', async function (assert) { - assert.expect(1); + assert.expect(2); this.data.turtle.records[1].turtle_trululu = 2; @@ -449,7 +449,7 @@ QUnit.module('fields', {}, function () { '' + '' + '', - "turtle,false,list": '' + + "turtle,false,list": '' + '' + '', }, @@ -468,13 +468,15 @@ QUnit.module('fields', {}, function () { // click the turtle field, opens a modal with the turtle form view await testUtils.dom.click(form.$('.o_data_row:first td.o_data_cell')); - assert.strictEqual($('[name="turtle_trululu"]').text(), "second recordrue morgueparis 75013", - "The partner's address should be displayed"); + assert.strictEqual($('[name="turtle_trululu"] .o_input').val(), + "second record", "many2one value should be displayed in input"); + assert.strictEqual($('[name="turtle_trululu"] .o_field_many2one_extra').text(), + "rue morgueparis 75013", "The partner's address should be displayed"); form.destroy(); }); QUnit.test('many2one data is reloaded if there is a context to take into account', async function (assert) { - assert.expect(1); + assert.expect(2); this.data.turtle.records[1].turtle_trululu = 2; @@ -492,7 +494,7 @@ QUnit.module('fields', {}, function () { '' + '' + '', - "turtle,false,list": '' + + "turtle,false,list": '' + '' + '' + '', @@ -512,8 +514,10 @@ QUnit.module('fields', {}, function () { // click the turtle field, opens a modal with the turtle form view await testUtils.dom.click(form.$('.o_data_row:first')); - assert.strictEqual($('.modal [name=turtle_trululu]').text(), "second recordrue morgueparis 75013", - "The partner's address should be displayed"); + assert.strictEqual($('.modal [name="turtle_trululu"] .o_input').val(), + "second record", "many2one value should be displayed in input"); + assert.strictEqual($('.modal [name=turtle_trululu] .o_field_many2one_extra').text(), + "rue morgueparis 75013", "The partner's address should be displayed"); form.destroy(); }); diff --git a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js index ecfc02fe9ee..a43c0744a3d 100644 --- a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js +++ b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js @@ -406,8 +406,8 @@ QUnit.module('fields', {}, function () { assert.containsNone(form, 'td.o_list_record_selector', "embedded one2many should not have a selector"); - assert.ok(!form.$('.o_field_x2many_list_row_add').length, - "embedded one2many should not be editable"); + assert.ok(form.$('.o_field_x2many_list_row_add').length, + "embedded one2many should be editable"); assert.ok(!form.$('td.o_list_record_remove').length, "embedded one2many records should not have a remove icon"); @@ -2703,8 +2703,8 @@ QUnit.module('fields', {}, function () { assert.ok(!form.$('.o_list_record_remove').length, 'remove icon should not be visible in readonly'); - assert.ok(!form.$('.o_field_x2many_list_row_add').length, - '"Add an item" should not be visible in readonly'); + assert.ok(form.$('.o_field_x2many_list_row_add').length, + '"Add an item" should be visible in readonly'); await testUtils.form.clickEdit(form); @@ -2763,15 +2763,12 @@ QUnit.module('fields', {}, function () { res_id: 1, }); - assert.ok(!form.$('.o_field_x2many_list_row_add').length, - '"Add an item" link should not be available in readonly'); + assert.ok(form.$('.o_field_x2many_list_row_add').length, + '"Add an item" link should be available in readonly'); await testUtils.dom.click(form.$('.o_list_view tbody td:first()')); - assert.ok($('.modal .o_form_readonly').length, - 'in readonly, clicking on a subrecord should open it in readonly in a dialog'); - await testUtils.dom.click($('.modal .o_form_button_cancel')); - - await testUtils.form.clickEdit(form); + assert.ok(form.$('.o_form_view.o_form_editable').length, + 'should toggle form mode to edit'); assert.ok(form.$('.o_field_x2many_list_row_add').length, '"Add an item" link should be available in edit'); @@ -4560,10 +4557,10 @@ QUnit.module('fields', {}, function () { }); await testUtils.dom.click(form.$('.o_data_row:first')); + await testUtils.nextTick(); // wait for quick edit assert.strictEqual($('.modal .modal-title').first().text().trim(), 'Open: one2many turtle field', "modal should use the python field string as title"); await testUtils.dom.click($('.modal .o_form_button_cancel')); - await testUtils.form.clickEdit(form); // edit the first one2many record await testUtils.dom.click(form.$('.o_data_row:first')); @@ -5083,12 +5080,7 @@ QUnit.module('fields', {}, function () { 'second record', "m2m values should have been correctly fetched"); await testUtils.dom.click(form.$('.o_data_row:first')); - - assert.strictEqual($('.modal .o_field_widget').text(), "xphone", - 'should display the form view dialog with the many2one value'); - await testUtils.dom.click($('.modal-footer button')); - - await testUtils.form.clickEdit(form); + assert.containsOnce(form, '.o_form_view.o_form_editable', 'should toggle form mode to edit'); // edit the m2m of first row await testUtils.dom.click(form.$('.o_list_view tbody td:first()')); @@ -5337,6 +5329,7 @@ QUnit.module('fields', {}, function () { 'should display the 2 turtles'); await testUtils.dom.click(form.$('.o_data_row:first')); + await testUtils.nextTick(); // wait for quick edit assert.strictEqual($('.modal .o_field_widget[name="partner_ids"] .o_list_view').length, 1, 'should display many2many list view in the modal'); @@ -5370,7 +5363,7 @@ QUnit.module('fields', {}, function () { await testUtils.dom.click(form.$('.o_data_row:first')); - assert.strictEqual($('.modal .o_field_widget[name="turtle_foo"]').text(), 'blip', + assert.strictEqual($('.modal .o_field_widget[name="turtle_foo"]').val(), 'blip', 'should open the modal and display the form field'); form.destroy(); @@ -5678,7 +5671,7 @@ QUnit.module('fields', {}, function () { await testUtils.dom.click(form.$('.o_data_row:first')); - assert.strictEqual($('.modal .o_field_widget[name="turtle_foo"]').text(), 'blip', + assert.strictEqual($('.modal .o_field_widget[name="turtle_foo"]').val(), 'blip', 'should open the modal and display the form field'); form.destroy(); @@ -6680,7 +6673,7 @@ QUnit.module('fields', {}, function () { arch: `
- + @@ -6702,17 +6695,21 @@ QUnit.module('fields', {}, function () { res_id: 1, }); + await testUtils.dom.click(form.$('.o_field_widget[name="name"]')); + await testUtils.fields.editInput($('.o_field_widget[name="name"]'), 'readonly'); + assert.containsOnce(form, '.o_data_row', "the one2many should contain one row"); // open the o2m record in readonly first await testUtils.dom.click(form.$('.o_data_row td:first')); assert.containsOnce(document.body, ".modal .o_form_readonly"); + await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel')); + await testUtils.form.clickDiscard(form); // switch to edit mode and open it again - await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_data_row td:first')); - + await testUtils.nextTick(); // wait for quick edit assert.containsOnce(document.body, ".modal .o_form_editable"); assert.containsOnce(document.body, '.modal .o_data_row', "the one2many should contain one row"); @@ -8261,6 +8258,7 @@ QUnit.module('fields', {}, function () { 'there should 2 and only 2 onchange from closing the partner modal'); await testUtils.dom.click(form.$('.o_data_row:eq(0)')); + await testUtils.nextTick(); // wait for quick edit assert.strictEqual($('.modal .o_data_row').length, 1, 'only 1 turtle for first partner'); assert.strictEqual($('.modal .o_data_row').text(), 'donatello', diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 4e70f92d4ec..e0327cfeb6d 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -378,8 +378,8 @@ QUnit.module('relational_fields', { }, }); - assert.strictEqual(form.$('.o_field_one2many[name=turtles] tbody').text().trim(), "kawablip", - 'The o2m should not have been sorted.'); + assert.strictEqual(form.$('.o_field_one2many[name=turtles] .o_data_row') + .text().trim(), "kawablip", 'The o2m should not have been sorted.'); form.destroy(); }); @@ -1884,6 +1884,7 @@ QUnit.module('relational_fields', { // open the x2m form view await testUtils.dom.click(form.$('.o_field_one2many[name="turtles"] .o_list_view td.o_data_cell:first')); + await testUtils.nextTick(); // wait for quick edit assert.strictEqual($('.modal .o_form_view .o_field_many2many[name="partner_ids"] .o_list_view .o_data_cell').text(), "blipMy little Foo Value", "the list view should be correctly rendered with foo"); @@ -1891,7 +1892,6 @@ QUnit.module('relational_fields', { assert.strictEqual(form.$('.o_field_one2many[name="turtles"] .o_list_view .o_field_many2manytags[name="partner_ids"]').text().replace(/\s/g, ''), "secondrecordaaa", "the tags should still be correctly rendered"); - await testUtils.form.clickEdit(form); assert.strictEqual(form.$('.o_field_one2many[name="turtles"] .o_list_view .o_field_many2manytags[name="partner_ids"]').text().replace(/\s/g, ''), "secondrecordaaa", "the tags should still be correctly rendered"); @@ -2405,8 +2405,8 @@ QUnit.module('relational_fields', { assert.notOk(form.$('div.o_field_widget div.custom-checkbox input').eq(1).prop('checked'), "second checkbox should not be checked"); - assert.ok(form.$('div.o_field_widget div.custom-checkbox input').prop('disabled'), - "the checkboxes should be disabled"); + assert.notOk(form.$('div.o_field_widget div.custom-checkbox input').prop('disabled'), + "the checkboxes should not be disabled"); await testUtils.form.clickEdit(form); @@ -2435,6 +2435,50 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('widget many2many_checkboxes (readonly)', async function (assert) { + assert.expect(7); + + this.data.partner.records[0].timmy = [12]; + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` + + + + + `, + res_id: 1, + }); + + assert.containsN(form, 'div.o_field_widget div.custom-checkbox', 2, + "should have fetched and displayed the 2 values of the many2many"); + + assert.ok(form.$('div.o_field_widget div.custom-checkbox input').eq(0).prop('checked'), + "first checkbox should be checked"); + assert.notOk(form.$('div.o_field_widget div.custom-checkbox input').eq(1).prop('checked'), + "second checkbox should not be checked"); + + assert.ok(form.$('div.o_field_widget div.custom-checkbox input').prop('disabled'), + "the checkboxes should be disabled"); + + await testUtils.form.clickEdit(form); + + assert.ok(form.$('div.o_field_widget div.custom-checkbox input').prop('disabled'), + "the checkboxes should be disabled"); + + await testUtils.dom.click(form.$('div.o_field_widget div.custom-checkbox > label').eq(1)); + + assert.ok(form.$('div.o_field_widget div.custom-checkbox input').eq(0).prop('checked'), + "first checkbox should be checked"); + assert.notOk(form.$('div.o_field_widget div.custom-checkbox input').eq(1).prop('checked'), + "second checkbox should not be checked"); + + form.destroy(); + }); + QUnit.test('widget many2many_checkboxes: start non empty, then remove twice', async function (assert) { assert.expect(2); @@ -2663,7 +2707,8 @@ QUnit.module('relational_fields', { '' + '', archs: { - 'partner,false,form': '
', + // make field reference readonly as the modal opens in edit mode + 'partner,false,form': '
', 'partner,false,list': '', }, res_id: 1, diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index 7d6db34f022..66d7d6bf594 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -2829,12 +2829,14 @@ QUnit.module('Views', { assert.containsOnce(form, 'td:contains(gold)', "should display the name of the many2many on the original form"); await testUtils.dom.click(form.$('td:contains(gold)')); + await testUtils.nextTick(); // wait for quick edit assert.containsOnce(document.body, '.modal'); assert.containsOnce($('.modal'), 'td:contains(first record)', "should display the name of the many2many on the modal form"); await testUtils.dom.click('.modal td:contains(first record)'); + await testUtils.nextTick(); // wait for quick edit assert.containsN(document.body, '.modal', 2, "there should be 2 modals (partner on top of partner_type) opened"); @@ -5670,8 +5672,9 @@ QUnit.module('Views', { assert.strictEqual(row.children()[1].textContent, '1 record', "the cell should contains the number of record: 1"); await testUtils.dom.click(row); + await testUtils.nextTick(); // wait for quick edit var modal_row = $('.modal-body .o_form_sheet .o_field_one2many .o_list_view .o_data_row'); - assert.strictEqual(modal_row.children().length, 2, + assert.strictEqual(modal_row.children('.o_data_cell').length, 2, "the row should contains the 2 fields defined in the form view"); assert.strictEqual($(modal_row).text(), "gold2", "the value of the fields should be fetched and displayed"); @@ -7939,7 +7942,7 @@ QUnit.module('Views', { }); // in readonly - await testUtils.dom.click(form.$('[name="display_name"]')); + await testUtils.dom.click(form.$('div.oe_title')); assert.hasClass(form.$('.o_form_button_edit'), 'o_catch_attention'); // in edit @@ -9849,6 +9852,553 @@ QUnit.module('Views', { actionManager.destroy(); }); + + QUnit.test('Quick Edition: click on a quick editable field', async function (assert) { + assert.expect(3); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + +
`, + res_id: 1, + }); + + assert.containsOnce(form, '.o_form_view.o_form_readonly'); + + await testUtils.dom.click(form.$('.o_field_widget[name="display_name"]')); + + assert.containsOnce(form, '.o_form_view.o_form_editable'); + assert.strictEqual(document.activeElement, $('.o_field_widget[name="display_name"]')[0]); + + form.destroy(); + }); + + QUnit.test('Quick Edition: click on a non quick editable field', async function (assert) { + assert.expect(4); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + +
`, + res_id: 1, + }); + + assert.containsOnce(form, '.o_form_view.o_form_readonly'); + assert.containsNone(form, '.o_priority_star[aria-checked="true"]'); + + await testUtils.dom.click(form.$('.o_field_widget[name="priority"] a:first')); + + assert.containsOnce(form, '.o_form_view.o_form_readonly'); + assert.containsOnce(form, '.o_priority_star[aria-checked="true"]'); + + form.destroy(); + }); + + QUnit.test('Quick Edition: Label click', async function (assert) { + assert.expect(3); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + +
`, + res_id: 1, + }); + + assert.containsOnce(form, '.o_form_view.o_form_readonly'); + + await testUtils.dom.click(form.$('.o_form_label:first')); + + assert.containsOnce(form, '.o_form_view.o_form_editable'); + assert.strictEqual(document.activeElement, form.$('input.o_field_widget[name="foo"]')[0]); + + form.destroy(); + }); + + QUnit.test('Quick Edition: Checkbox click', async function (assert) { + assert.expect(11); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + +
`, + res_id: 1, + }); + + assert.containsOnce(form, '.o_form_view.o_form_readonly'); + assert.containsOnce(form, '.o_field_boolean input:checked'); + assert.containsNone(form, '.o_field_boolean input:disabled'); + + await testUtils.dom.click(form.$('.o_field_widget[name="bar"]')); + await testUtils.nextTick(); // wait for quick edit + + assert.containsOnce(form, '.o_form_view.o_form_editable'); + assert.containsNone(form, '.o_field_boolean input:checked'); + + await testUtils.form.clickSave(form); + + assert.containsOnce(form, '.o_form_view.o_form_readonly'); + assert.containsNone(form, '.o_field_boolean input:checked'); + assert.containsNone(form, '.o_field_boolean input:disabled'); + + await testUtils.dom.click(form.$('.o_field_widget[name="bar"]')); + await testUtils.nextTick(); // wait for quick edit + + assert.containsOnce(form, '.o_form_view.o_form_editable'); + assert.containsOnce(form, '.o_field_boolean input:checked'); + assert.containsNone(form, '.o_field_boolean input:disabled'); + + form.destroy(); + }); + + QUnit.test('Quick Edition: Checkbox click on label', async function (assert) { + assert.expect(8); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + +
`, + res_id: 1, + }); + + assert.containsOnce(form, '.o_form_view.o_form_readonly'); + assert.containsOnce(form, '.o_field_boolean input:checked'); + + await testUtils.dom.click(form.$('.o_td_label .o_form_label')); + await testUtils.nextTick(); // wait for quick edit + + assert.containsOnce(form, '.o_form_view.o_form_editable'); + assert.containsNone(form, '.o_field_boolean input:checked'); + + await testUtils.form.clickSave(form); + + assert.containsOnce(form, '.o_form_view.o_form_readonly'); + assert.containsNone(form, '.o_field_boolean input:checked'); + + await testUtils.dom.click(form.$('.o_td_label .o_form_label')); + await testUtils.nextTick(); // wait for quick edit + + assert.containsOnce(form, '.o_form_view.o_form_editable'); + assert.containsOnce(form, '.o_field_boolean input:checked'); + + form.destroy(); + }); + + QUnit.test('Quick Edition: Readonly one2many list', async function (assert) { + assert.expect(5); + + this.data.partner.records[0].p.push(2); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + + + +
`, + res_id: 1, + }); + + assert.containsOnce(form, '.o_form_view.o_form_readonly'); + assert.containsNone(form, '.o_field_x2many_list_row_add', + 'create line should not be displayed'); + + await testUtils.dom.click(form.$('.o_field_cell:first')); + + assert.containsOnce(form, '.o_form_view.o_form_editable', + 'should switch into edit mode'); + assert.containsNone(form, '.o_field_x2many_list_row_add', + 'create line should still not be displayed'); + assert.containsNone(form, 'input'); + + form.destroy(); + }); + + QUnit.test('Quick Edition: Editable one2many list (click cell: editable)', async function (assert) { + assert.expect(3); + + this.data.partner.records[0].p.push(2); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + + + +
`, + res_id: 1, + }); + + assert.containsOnce(form, '.o_form_view.o_form_readonly'); + + await testUtils.dom.click(form.$('.o_field_cell[name="foo"]')); + + assert.containsOnce(form, '.o_form_view.o_form_editable', + 'should switch into edit mode'); + assert.strictEqual(document.activeElement, form.$('.o_field_cell[name="foo"] input')[0]); + + form.destroy(); + }); + + QUnit.test('Quick Edition: Editable one2many list (click cell: not editable)', async function (assert) { + assert.expect(5); + + this.data.partner.records[0].p.push(2); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + + + + + + + `, + res_id: 1, + }); + + assert.containsOnce(form, '.o_form_view.o_form_readonly'); + assert.containsNone(document.body, '.modal'); + + await testUtils.dom.click(form.$('.o_field_cell[name="foo"]')); + + assert.containsOnce(form, '.o_form_view.o_form_editable', + 'should switch into edit mode'); + assert.containsOnce(document.body, '.modal'); + assert.containsOnce(document.body, '.modal input.o_field_widget[name="foo"]'); + + form.destroy(); + }); + + QUnit.test('Quick Edition: Editable one2many list (add a line: editable)', async function (assert) { + assert.expect(4); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + + + +
`, + res_id: 1, + }); + + assert.containsOnce(form, '.o_form_view.o_form_readonly'); + assert.containsOnce(form, '.o_field_x2many_list_row_add', + 'create line should be displayed'); + + await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); + await testUtils.nextTick(); // wait for quick edit + + assert.containsOnce(form, '.o_form_view.o_form_editable', + 'should switch into edit mode'); + assert.strictEqual(document.activeElement, form.$('.o_field_cell[name="foo"] input')[0]); + + form.destroy(); + }); + + QUnit.test('Quick Edition: Editable one2many list (add a line: not editable)', async function (assert) { + assert.expect(5); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + + + + + + + `, + res_id: 1, + }); + + assert.containsOnce(form, '.o_form_view.o_form_readonly'); + assert.containsNone(document.body, '.modal'); + + await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); + + assert.containsOnce(form, '.o_form_view.o_form_editable', + 'should switch into edit mode'); + assert.containsOnce(document.body, '.modal'); + assert.containsOnce(document.body, '.modal input.o_field_widget[name="foo"]'); + + form.destroy(); + }); + + QUnit.test('Quick Edition: Date picker', async function (assert) { + assert.expect(4); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + `, + res_id: 1, + }); + + assert.containsOnce(form, '.o_form_view.o_form_readonly'); + + await testUtils.dom.click(form.$('.o_field_widget[name="date"]')); + + assert.containsOnce(form, '.o_form_view.o_form_editable', + 'should switch into edit mode'); + assert.strictEqual(document.activeElement, form.$('.o_field_widget[name="date"] input')[0]); + assert.containsOnce(document.body, '.bootstrap-datetimepicker-widget'); + + form.destroy(); + }); + + QUnit.test('Quick Edition: Many2one', async function (assert) { + assert.expect(5); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + `, + res_id: 1, + mockRPC(route, { method }) { + assert.step(method); + return this._super(...arguments); + }, + }); + + assert.containsOnce(form, '.o_form_view.o_form_readonly'); + + await testUtils.dom.click(form.$('.o_field_widget[name="trululu"]')); + + assert.containsOnce(form, '.o_form_view.o_form_readonly'); + assert.verifySteps(['read', 'get_formview_action']) + + form.destroy(); + }); + + QUnit.test('Quick Edition: Many2Many checkbox', async function (assert) { + assert.expect(7); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + `, + res_id: 1, + }); + + assert.containsOnce(form, '.o_form_view.o_form_readonly'); + assert.containsNone(form, 'input[type="checkbox"]:checked'); + assert.containsNone(form, 'input[type="checkbox"]:disabled'); + + await testUtils.dom.click(form.$('.o_field_widget[name="timmy"] label:eq(1)')); + + assert.containsOnce(form, '.o_form_view.o_form_editable'); + assert.containsOnce(form, 'input[type="checkbox"]:checked'); + assert.containsOnce(form, 'input[type="checkbox"]:eq(1):checked'); + assert.containsNone(form, 'input[type="checkbox"]:disabled'); + + form.destroy(); + }); + + QUnit.test('Quick Edition: Many2Many checkbox readonly', async function (assert) { + assert.expect(6); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + `, + res_id: 1, + }); + + assert.containsOnce(form, '.o_form_view.o_form_readonly'); + assert.containsNone(form, 'input[type="checkbox"]:checked'); + assert.containsNone(form, 'input[type="checkbox"]:not(:disabled)'); + + await testUtils.dom.click(form.$('.o_field_widget[name="timmy"] label:eq(1)')); + + assert.containsOnce(form, '.o_form_view.o_form_editable'); + assert.containsNone(form, 'input[type="checkbox"]:not(:disabled)'); + assert.containsNone(form, 'input[type="checkbox"]:checked'); + + form.destroy(); + }); + + QUnit.test('Quick Edition: Many2Many checkbox click on label', async function (assert) { + assert.expect(4); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + +
`, + res_id: 1, + }); + + assert.containsOnce(form, '.o_form_view.o_form_readonly'); + assert.containsNone(form, 'input[type="checkbox"]:checked'); + + await testUtils.dom.click(form.$('.o_td_label .o_form_label')); + + assert.containsOnce(form, '.o_form_view.o_form_editable'); + assert.containsNone(form, 'input[type="checkbox"]:checked'); + + form.destroy(); + }); + + QUnit.test('Quick Edition: Many2one radio', async function (assert) { + assert.expect(6); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + `, + res_id: 1, + }); + + assert.containsOnce(form, '.o_form_view.o_form_readonly'); + assert.containsNone(form, 'input[type="radio"]:eq(1):checked'); + assert.containsNone(form, 'input[type="radio"]:disabled'); + + await testUtils.dom.click(form.$('.o_field_widget[name="trululu"] label:eq(1)')); + + assert.containsOnce(form, '.o_form_view.o_form_editable'); + assert.containsOnce(form, 'input[type="radio"]:eq(1):checked'); + assert.containsNone(form, 'input[type="radio"]:disabled'); + + form.destroy(); + }); + + QUnit.test('Quick Edition: Many2Many radio readonly', async function (assert) { + assert.expect(6); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + `, + res_id: 1, + }); + + assert.containsOnce(form, '.o_form_view.o_form_readonly'); + assert.containsOnce(form, 'input[type="radio"]:eq(2):checked'); + assert.containsNone(form, 'input[type="radio"]:not(:disabled)'); + + await testUtils.dom.click(form.$('.o_field_widget[name="trululu"] label:eq(1)')); + + assert.containsOnce(form, '.o_form_view.o_form_editable'); + assert.containsOnce(form, 'input[type="radio"]:eq(2):checked'); + assert.containsNone(form, 'input[type="checkbox"]:not(:disabled)'); + + form.destroy(); + }); + + QUnit.test('Quick Edition: Many2one radio click on label', async function (assert) { + assert.expect(4); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + +
`, + res_id: 1, + }); + + assert.containsOnce(form, '.o_form_view.o_form_readonly'); + assert.containsOnce(form, 'input[type="radio"]:eq(2):checked'); + + await testUtils.dom.click(form.$('.o_td_label .o_form_label')); + + assert.containsOnce(form, '.o_form_view.o_form_editable'); + assert.containsOnce(form, 'input[type="radio"]:eq(2):checked'); + + form.destroy(); + }); }); }); diff --git a/odoo/addons/test_new_api/static/tests/tours/x2many.js b/odoo/addons/test_new_api/static/tests/tours/x2many.js index 9e0080f3423..a903efea1d0 100644 --- a/odoo/addons/test_new_api/static/tests/tours/x2many.js +++ b/odoo/addons/test_new_api/static/tests/tours/x2many.js @@ -288,7 +288,7 @@ odoo.define('web.test.x2many', function (require) { }, { // add message ddd content: "create new message ddd", trigger: '.tab-pane:eq(0) .o_field_x2many_list_row_add a', - extra_trigger: '.tab-pane:eq(0) .o_field_widget tbody tr:has(td:containsExact(d))', + extra_trigger: '.o_form_editable .tab-pane:eq(0) .o_field_widget tbody tr:has(td:containsExact(d))', }, { content: "select another user", trigger: '.o_field_many2one .o_input_dropdown > input', @@ -427,7 +427,7 @@ odoo.define('web.test.x2many', function (require) { }, { // add message eee content: "create new message eee", trigger: '.tab-pane:eq(0) .o_field_x2many_list_row_add a', - extra_trigger: 'a[data-toggle="tab"].active:contains(Messages)', + extra_trigger: '.o_form_editable a[data-toggle="tab"].active:contains(Messages)', }, { content: "change text value", trigger: 'textarea[name="body"]:first',