diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 9112756aaed..033d0f670ae 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -214,21 +214,63 @@ var BasicModel = AbstractModel.extend({ * It is useful for the cases where a record element is shared between * various views, such as a one2many with a tree and a form view. * - * @param {string} recordID a valid element ID + * @param {string} datapointID a valid element ID (of type 'list' or 'record') * @param {Object} viewInfo * @param {Object} viewInfo.fields - * @param {Object} viewInfo.fieldsInfo + * @param {Object} viewInfo.fieldInfo + * @param {string} viewInfo.viewType + * @returns {Promise} resolved when the fieldInfo have been set on the given + * datapoint and all its children, and all rawChanges have been applied */ - addFieldsInfo: function (recordID, viewInfo) { - var record = this.localData[recordID]; - record.fields = _.extend({}, record.fields, viewInfo.fields); - // complete the given fieldsInfo with the fields of the main view, so + addFieldsInfo: function (dataPointID, viewInfo) { + var dataPoint = this.localData[dataPointID]; + dataPoint.fields = _.extend({}, dataPoint.fields, viewInfo.fields); + // complete the given fieldInfo with the fields of the main view, so // that those field will be reloaded if a reload is triggered by the // secondary view - var fieldsInfo = _.mapObject(viewInfo.fieldsInfo, function (fieldsInfo) { - return _.defaults({}, fieldsInfo, record.fieldsInfo[record.viewType]); + dataPoint.fieldsInfo = dataPoint.fieldsInfo || {}; + const mainFieldInfo = dataPoint.fieldsInfo[dataPoint[viewInfo.viewType]]; + dataPoint.fieldsInfo[viewInfo.viewType] = _.defaults({}, viewInfo.fieldInfo, mainFieldInfo); + + // Some fields in the new fields info might not be in the previous one, + // so we might have stored changes for them (e.g. coming from onchange + // RPCs), that we haven't been able to process earlier (because those + // fields were unknown at that time). So we now try to process them. + return this.applyRawChanges(dataPointID, viewInfo.viewType).then(() => { + const proms = []; + const fieldInfo = dataPoint.fieldsInfo[viewInfo.viewType]; + // recursively apply the new field info on sub datapoints + if (dataPoint.type === 'list') { + // case 'list': on all datapoints in the list + Object.values(dataPoint._cache).forEach(subDataPointID => { + proms.push(this.addFieldsInfo(subDataPointID, { + fields: dataPoint.fields, + fieldInfo: dataPoint.fieldsInfo[viewInfo.viewType], + viewType: viewInfo.viewType, + })); + }); + } else { + // case 'record': on datapoints of all x2many fields + const values = _.extend({}, dataPoint.data, dataPoint._changes); + Object.keys(fieldInfo).forEach(fieldName => { + const fieldType = dataPoint.fields[fieldName].type; + if (fieldType === 'one2many' || fieldType === 'many2many') { + const mode = fieldInfo[fieldName].mode; + const views = fieldInfo[fieldName].views; + const x2mDataPointID = values[fieldName]; + if (views[mode] && x2mDataPointID) { + proms.push(this.addFieldsInfo(x2mDataPointID, { + fields: views[mode].fields, + fieldInfo: views[mode].fieldsInfo[mode], + viewType: mode, + })); + } + } + }); + } + return Promise.all(proms); }); - record.fieldsInfo = _.extend({}, record.fieldsInfo, fieldsInfo); + }, /** * Add and process default values for a given record. Those values are @@ -3735,8 +3777,8 @@ var BasicModel = AbstractModel.extend({ * @returns {boolean} */ _isFieldProtected: function (record, fieldName, viewType) { - var fieldInfo = record.fieldsInfo && - (record.fieldsInfo[viewType || record.viewType][fieldName]); + viewType = viewType || record.viewType; + var fieldInfo = viewType && record.fieldsInfo && record.fieldsInfo[viewType][fieldName]; if (fieldInfo) { var rawModifiers = fieldInfo.modifiers || {}; var modifiers = this._evalModifiers(record, _.pick(rawModifiers, 'readonly')); diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js index 4566d0b9471..6668392b823 100644 --- a/addons/web/static/src/js/views/basic/basic_view.js +++ b/addons/web/static/src/js/views/basic/basic_view.js @@ -96,14 +96,15 @@ var BasicView = AbstractView.extend({ * @private * @returns {Promise} */ - _loadData: function (model) { + _loadData: async function (model) { if (this.recordID) { // Add the fieldsInfo of the current view to the given recordID, // as it will be shared between two views, and it must be able to // handle changes on fields that are only on this view. - model.addFieldsInfo(this.recordID, { + await model.addFieldsInfo(this.recordID, { fields: this.fields, - fieldsInfo: this.fieldsInfo, + fieldInfo: this.fieldsInfo[this.viewType], + viewType: this.viewType, }); var record = model.get(this.recordID); @@ -119,7 +120,7 @@ var BasicView = AbstractView.extend({ // in the form view (e.g. if field is a many2many list in the form // view, or if it is displayed by a widget requiring specialData). // So when this happens, F is added to the list of fieldNames to fetch. - _.each(viewFields, function (name) { + _.each(viewFields, (name) => { if (!_.contains(fieldNames, name)) { var fieldType = record.fields[name].type; var fieldInfo = fieldsInfo[name]; @@ -137,30 +138,28 @@ var BasicView = AbstractView.extend({ // X2Many case: field is an x2many displayed as a list or // kanban view, but the related fields haven't been loaded yet. if ((fieldType === 'one2many' || fieldType === 'many2many')) { - if (!('fieldsInfo' in record.data[name])) { + var x2mFieldInfo = record.fieldsInfo[this.viewType][name]; + var viewType = x2mFieldInfo.viewType || x2mFieldInfo.mode; + var knownFields = Object.keys(record.data[name].fieldsInfo[record.data[name].viewType] || {}); + var newFields = Object.keys(record.data[name].fieldsInfo[viewType]); + if (_.difference(newFields, knownFields).length) { fieldNames.push(name); - } else { - var fieldViews = fieldInfo.views || fieldInfo.fieldsInfo || {}; - var fieldViewTypes = Object.keys(fieldViews); - var recordViewTypes = Object.keys(record.data[name].fieldsInfo); - if (_.difference(fieldViewTypes, recordViewTypes).length) { - fieldNames.push(name); - } + } - if (record.data[name].viewType === 'default') { - // Use case: x2many (tags) in x2many list views - // When opening the x2many record form view, the - // x2many will be reloaded but it may not have - // the same fields (ex: tags in list and list in - // form) so we need to merge the fieldsInfo to - // avoid losing the initial fields (display_name) - var defaultFieldInfo = record.data[name].fieldsInfo.default; - _.each(fieldViews, function (fieldView) { - _.each(fieldView.fieldsInfo, function (x2mFieldInfo) { - _.defaults(x2mFieldInfo, defaultFieldInfo); - }); + if (record.data[name].viewType === 'default') { + // Use case: x2many (tags) in x2many list views + // When opening the x2many record form view, the + // x2many will be reloaded but it may not have + // the same fields (ex: tags in list and list in + // form) so we need to merge the fieldsInfo to + // avoid losing the initial fields (display_name) + var fieldViews = fieldInfo.views || fieldInfo.fieldsInfo || {}; + var defaultFieldInfo = record.data[name].fieldsInfo.default; + _.each(fieldViews, function (fieldView) { + _.each(fieldView.fieldsInfo, function (x2mFieldInfo) { + _.defaults(x2mFieldInfo, defaultFieldInfo); }); - } + }); } } // Many2one: context is not the same between the different views @@ -176,25 +175,18 @@ var BasicView = AbstractView.extend({ var def; if (fieldNames.length) { - // Some fields in the new view weren't in the previous one, so - // we might have stored changes for them (e.g. coming from - // onchange RPCs), that we haven't been able to process earlier - // (because those fields were unknow at that time). So we ask - // the model to process them. - def = model.applyRawChanges(record.id, viewType).then(function () { - if (model.isNew(record.id)) { - return model.applyDefaultValues(record.id, {}, { - fieldNames: fieldNames, - viewType: viewType, - }); - } else { - return model.reload(record.id, { - fieldNames: fieldNames, - keepChanges: true, - viewType: viewType, - }); - } - }); + if (model.isNew(record.id)) { + def = model.applyDefaultValues(record.id, {}, { + fieldNames: fieldNames, + viewType: viewType, + }); + } else { + def = model.reload(record.id, { + fieldNames: fieldNames, + keepChanges: true, + viewType: viewType, + }); + } } return Promise.resolve(def).then(function () { return model.get(record.id); 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 ad68ede00d2..7b470cda22a 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 @@ -5409,6 +5409,108 @@ QUnit.module('fields', {}, function () { form.destroy(); }); + QUnit.test('nested x2many (inline form view) and onchanges', async function (assert) { + assert.expect(6); + + this.data.partner.onchanges.bar = function (obj) { + if (!obj.bar) { + obj.p = [[5], [0, 0, { + turtles: [[0, 0, { + turtle_foo: 'new turtle', + }]], + }]]; + } + }; + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: `
+ + + + + + + + + + + + + + `, + }); + + assert.containsNone(form, '.o_data_row'); + + await testUtils.dom.click(form.$('.o_field_widget[name=bar] input')); + assert.containsOnce(form, '.o_data_row'); + assert.strictEqual(form.$('.o_data_row').text(), '1 record'); + + await testUtils.dom.click(form.$('.o_data_row:first')); + + assert.containsOnce(document.body, '.modal .o_form_view'); + assert.containsOnce(document.body, '.modal .o_form_view .o_data_row'); + assert.strictEqual($('.modal .o_form_view .o_data_row').text(), 'new turtle'); + + form.destroy(); + }); + + QUnit.test('nested x2many (non inline form view) and onchanges', async function (assert) { + assert.expect(6); + + this.data.partner.onchanges.bar = function (obj) { + if (!obj.bar) { + obj.p = [[5], [0, 0, { + turtles: [[0, 0, { + turtle_foo: 'new turtle', + }]], + }]]; + } + }; + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + + + + + `, + archs: { + 'partner,false,form': ` +
+ + + + + +
`, + }, + }); + + assert.containsNone(form, '.o_data_row'); + + await testUtils.dom.click(form.$('.o_field_widget[name=bar] input')); + assert.containsOnce(form, '.o_data_row'); + assert.strictEqual(form.$('.o_data_row').text(), '1 record'); + + await testUtils.dom.click(form.$('.o_data_row:first')); + + assert.containsOnce(document.body, '.modal .o_form_view'); + assert.containsOnce(document.body, '.modal .o_form_view .o_data_row'); + assert.strictEqual($('.modal .o_form_view .o_data_row').text(), 'new turtle'); + + form.destroy(); + }); + QUnit.test('one2many (who contains display_name) with tree view and without form view', async function (assert) { assert.expect(1);