[IMP] web: add quick edit behaviour
This commit adds the quick edit behaviour. The quick edit allows to click on fields in readonly form view to switch into edit mode. After switching mode, the clicked field is automatically focused. A few fields have a custom quick edit behaviour after mode switched: - checkboxes automatically toggle. - radio buttons are set to the selected value. - one2many list's cell are focused. One2many list fields now show the "add a line" in readonly mode. task 2330101
This commit is contained in:
@@ -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());
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -98,6 +98,10 @@
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.o_quick_editable:not(.o_form_uri) {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.o_form_uri {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="web.FieldBoolean" owl="1">
|
||||
<CustomCheckbox disabled="mode === 'readonly'" value="value"
|
||||
class="o_field_boolean" t-on-change="_onChange"/>
|
||||
<CustomCheckbox disabled="hasReadonlyModifier"
|
||||
value="value" class="o_field_boolean" t-on-change="_onChange"/>
|
||||
</t>
|
||||
|
||||
<t t-name="web.FieldBadge" owl="1">
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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: `
|
||||
<form>
|
||||
<field name="bar" attrs="{'readonly': True}"/>
|
||||
</form>`,
|
||||
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: '<form string="Partners">' +
|
||||
'<field name="document" widget="image" options="{\'size\': [90, 90]}"/>' +
|
||||
'<field name="timmy" widget="many2many">' +
|
||||
'<tree>' +
|
||||
'<field name="timmy" widget="many2many" mode="kanban">' +
|
||||
// use kanban view as the tree will trigger edit mode
|
||||
// and thus won't display the field
|
||||
'<kanban>' +
|
||||
'<field name="display_name"/>' +
|
||||
'</tree>' +
|
||||
'<templates>' +
|
||||
'<t t-name="kanban-box">' +
|
||||
'<div class="oe_kanban_global_click">' +
|
||||
'<span><t t-esc="record.display_name.value"/></span>' +
|
||||
'</div>' +
|
||||
'</t>' +
|
||||
'</templates>' +
|
||||
'</kanban>' +
|
||||
'<form>' +
|
||||
'<field name="image" widget="image"/>' +
|
||||
'</form>' +
|
||||
@@ -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: '<form string="Partners">' +
|
||||
'<field name="foo" widget="image_url" options="{\'size\': [90, 90]}"/>' +
|
||||
'<field name="timmy" widget="many2many">' +
|
||||
'<tree>' +
|
||||
'<field name="timmy" widget="many2many" mode="kanban">' +
|
||||
// use kanban view as the tree will trigger edit mode
|
||||
// and thus won't display the field
|
||||
'<kanban>' +
|
||||
'<field name="display_name"/>' +
|
||||
'</tree>' +
|
||||
'<templates>' +
|
||||
'<t t-name="kanban-box">' +
|
||||
'<div class="oe_kanban_global_click">' +
|
||||
'<span><t t-esc="record.display_name.value"/></span>' +
|
||||
'</div>' +
|
||||
'</t>' +
|
||||
'</templates>' +
|
||||
'</kanban>' +
|
||||
'<form>' +
|
||||
'<field name="image" widget="image_url"/>' +
|
||||
'</form>' +
|
||||
@@ -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', {
|
||||
'<field name="datetime"/>' +
|
||||
'</tree>' +
|
||||
'<form>' +
|
||||
'<field name="datetime" widget="date"/>' +
|
||||
// display datetime in readonly as modal will open in edit
|
||||
'<field name="datetime" widget="date" attrs="{\'readonly\': 1}"/>' +
|
||||
'</form>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
@@ -4568,7 +4626,8 @@ QUnit.module('basic_fields', {
|
||||
'<field name="datetime"/>' +
|
||||
'</tree>' +
|
||||
'<form>' +
|
||||
'<field name="datetime" widget="date"/>' +
|
||||
// display datetime in readonly as modal will open in edit
|
||||
'<field name="datetime" widget="date" attrs="{\'readonly\': 1}"/>' +
|
||||
'</form>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 () {
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="turtle_trululu" context="{\'show_address\': 1}" options="{\'always_reload\': True}"/>' +
|
||||
'</form>',
|
||||
"turtle,false,list": '<tree editable="bottom">' +
|
||||
"turtle,false,list": '<tree>' +
|
||||
'<field name="display_name"/>' +
|
||||
'</tree>',
|
||||
},
|
||||
@@ -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 () {
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="turtle_trululu" context="{\'show_address\': 1}" options="{\'always_reload\': True}"/>' +
|
||||
'</form>',
|
||||
"turtle,false,list": '<tree editable="bottom">' +
|
||||
"turtle,false,list": '<tree>' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="turtle_trululu"/>' +
|
||||
'</tree>',
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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: `
|
||||
<form>
|
||||
<field name="name"/>
|
||||
<field name="p">
|
||||
<field name="p" attrs="{'readonly': [['name', '=', 'readonly']]}">
|
||||
<tree><field name="display_name"/></tree>
|
||||
<form>
|
||||
<field name="display_name"/>
|
||||
@@ -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',
|
||||
|
||||
@@ -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: `
|
||||
<form string="Partners">
|
||||
<group>
|
||||
<field name="timmy" widget="many2many_checkboxes"
|
||||
attrs="{'readonly': true}"/>
|
||||
</group>
|
||||
</form>`,
|
||||
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', {
|
||||
'<field name="p"/>' +
|
||||
'</form>',
|
||||
archs: {
|
||||
'partner,false,form': '<form><field name="reference"/></form>',
|
||||
// make field reference readonly as the modal opens in edit mode
|
||||
'partner,false,form': '<form><field name="reference" attrs="{\'readonly\': 1}"/></form>',
|
||||
'partner,false,list': '<tree><field name="display_name"/></tree>',
|
||||
},
|
||||
res_id: 1,
|
||||
|
||||
@@ -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: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="display_name"/>
|
||||
</group>
|
||||
</form>`,
|
||||
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: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="priority" widget="priority"/>
|
||||
</group>
|
||||
</form>`,
|
||||
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: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="foo"/>
|
||||
</group>
|
||||
</form>`,
|
||||
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: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="bar"/>
|
||||
</group>
|
||||
</form>`,
|
||||
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: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="bar"/>
|
||||
</group>
|
||||
</form>`,
|
||||
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: `
|
||||
<form>
|
||||
<field name="p" attrs="{'readonly': True}">
|
||||
<tree>
|
||||
<field name="foo"/>
|
||||
</tree>
|
||||
</field>
|
||||
</form>`,
|
||||
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: `
|
||||
<form>
|
||||
<field name="p">
|
||||
<tree editable="bottom">
|
||||
<field name="foo"/>
|
||||
</tree>
|
||||
</field>
|
||||
</form>`,
|
||||
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: `
|
||||
<form>
|
||||
<field name="p">
|
||||
<tree>
|
||||
<field name="foo"/>
|
||||
</tree>
|
||||
<form>
|
||||
<field name="foo"/>
|
||||
</form>
|
||||
</field>
|
||||
</form>`,
|
||||
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: `
|
||||
<form>
|
||||
<field name="p">
|
||||
<tree editable="bottom">
|
||||
<field name="foo"/>
|
||||
</tree>
|
||||
</field>
|
||||
</form>`,
|
||||
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: `
|
||||
<form>
|
||||
<field name="p">
|
||||
<tree>
|
||||
<field name="foo"/>
|
||||
</tree>
|
||||
<form>
|
||||
<field name="foo"/>
|
||||
</form>
|
||||
</field>
|
||||
</form>`,
|
||||
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: `
|
||||
<form>
|
||||
<field name="date"/>
|
||||
</form>`,
|
||||
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: `
|
||||
<form>
|
||||
<field name="trululu"/>
|
||||
</form>`,
|
||||
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: `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many_checkboxes"/>
|
||||
</form>`,
|
||||
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: `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many_checkboxes"
|
||||
attrs="{'readonly': 1}"/>
|
||||
</form>`,
|
||||
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: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="timmy" widget="many2many_checkboxes"/>
|
||||
</group>
|
||||
</form>`,
|
||||
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: `
|
||||
<form>
|
||||
<field name="trululu" widget="radio"/>
|
||||
</form>`,
|
||||
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: `
|
||||
<form>
|
||||
<field name="trululu" widget="radio"
|
||||
attrs="{'readonly': 1}"/>
|
||||
</form>`,
|
||||
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: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="trululu" widget="radio"/>
|
||||
</group>
|
||||
</form>`,
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user