From 9216bdcb6f0bbb20900a33bbf7283e6e4fda1b2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Theys?= Date: Wed, 5 Aug 2020 14:03:05 +0000 Subject: [PATCH] [FIX] mail: clean up JS relations by storing records MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the support and the conversion from record to localId and back from localId to record. Part of task-2308312 closes odoo/odoo#55481 X-original-commit: fc5e8be4e65518770a281f0dffae07854d20073a Signed-off-by: Alexandre Kühn (aku) --- .../chat_window_manager_tests.js | 9 +- .../src/components/chatter/chatter_tests.js | 5 + .../chatter_topbar/chatter_topbar_tests.js | 10 + .../static/src/components/dialog/dialog.js | 4 +- .../dialog_manager/dialog_manager.xml | 2 +- .../message_seen_indicator_tests.js | 2 +- addons/mail/static/src/env/test_env.js | 3 +- addons/mail/static/src/js/main.js | 2 +- addons/mail/static/src/model/model_field.js | 527 +++++------------- addons/mail/static/src/model/model_manager.js | 214 +++---- .../src/models/attachment/attachment.js | 2 +- .../chat_window_manager.js | 6 +- .../mail/static/src/models/chatter/chatter.js | 22 +- .../mail/static/src/models/dialog/dialog.js | 22 +- .../models/dialog_manager/dialog_manager.js | 59 +- .../mail/static/src/models/discuss/discuss.js | 2 +- .../static/src/models/follower/follower.js | 2 +- .../message_seen_indicator.js | 4 +- .../static/src/models/messaging/messaging.js | 18 +- addons/mail/static/src/models/model/model.js | 6 +- .../mail/static/src/models/thread/thread.js | 18 +- .../src/models/thread_cache/thread_cache.js | 8 +- .../src/models/thread_viewer/thread_viewer.js | 4 +- addons/mail/static/src/utils/test_utils.js | 4 + 24 files changed, 366 insertions(+), 589 deletions(-) diff --git a/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js b/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js index fb4c92d70c7..6e284a7156c 100644 --- a/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js +++ b/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js @@ -750,7 +750,7 @@ QUnit.test('focus next visible chat window when closing current chat window with QUnit.test('[technical] chat window: composer state conservation on toggle home menu', async function (assert) { // technical as show/hide home menu simulation are involved and home menu implementation // have side-effects on DOM that may make chat window components not work - assert.expect(6); + assert.expect(7); Object.assign(this.data.initMessaging, { channel_slots: { @@ -775,6 +775,11 @@ QUnit.test('[technical] chat window: composer state conservation on toggle home document.querySelector(`.o_ComposerTextInput_textarea`).focus(); document.execCommand('insertText', false, 'XDU for the win !'); }); + assert.containsNone( + document.body, + '.o_Composer .o_Attachment', + "composer should have no attachment initially" + ); // Set attachments of the composer const files = [ await createFile({ @@ -803,7 +808,7 @@ QUnit.test('[technical] chat window: composer state conservation on toggle home document.body, '.o_Composer .o_Attachment', 2, - "verify chat window composer initial attachment count" + "composer should have 2 total attachments after adding 2 attachments" ); await this.hideHomeMenu(); diff --git a/addons/mail/static/src/components/chatter/chatter_tests.js b/addons/mail/static/src/components/chatter/chatter_tests.js index 36b87b65f5d..0b25b9f3d4e 100644 --- a/addons/mail/static/src/components/chatter/chatter_tests.js +++ b/addons/mail/static/src/components/chatter/chatter_tests.js @@ -55,6 +55,7 @@ QUnit.module('chatter_tests.js', { QUnit.test('base rendering when chatter has no attachment', async function (assert) { assert.expect(6); + this.data['res.partner'].records.push({ id: 100 }); const messages = [...Array(60).keys()].map(id => { return { author_id: [10, "Demo User"], @@ -157,6 +158,7 @@ QUnit.test('base rendering when chatter has no record', async function (assert) QUnit.test('base rendering when chatter has attachments', async function (assert) { assert.expect(3); + this.data['res.partner'].records.push({ id: 100 }); await this.start({ async mockRPC(route, args) { if (route.includes('ir.attachment/search_read')) { @@ -200,6 +202,7 @@ QUnit.test('base rendering when chatter has attachments', async function (assert QUnit.test('show attachment box', async function (assert) { assert.expect(6); + this.data['res.partner'].records.push({ id: 100 }); await this.start({ async mockRPC(route, args) { if (route.includes('ir.attachment/search_read')) { @@ -262,6 +265,7 @@ QUnit.test('show attachment box', async function (assert) { QUnit.test('composer show/hide on log note/send message', async function (assert) { assert.expect(10); + this.data['res.partner'].records.push({ id: 100 }); await this.start(); const chatter = this.env.models['mail.chatter'].create({ threadId: 100, @@ -343,6 +347,7 @@ QUnit.test('composer show/hide on log note/send message', async function (assert QUnit.test('should not display user notification messages in chatter', async function (assert) { assert.expect(1); + this.data['res.partner'].records.push({ id: 100 }); this.data['mail.message'].records = [{ author_id: [7, "Demo"], body: "

User notification

", diff --git a/addons/mail/static/src/components/chatter_topbar/chatter_topbar_tests.js b/addons/mail/static/src/components/chatter_topbar/chatter_topbar_tests.js index 352566423f5..36e34d6056f 100644 --- a/addons/mail/static/src/components/chatter_topbar/chatter_topbar_tests.js +++ b/addons/mail/static/src/components/chatter_topbar/chatter_topbar_tests.js @@ -57,6 +57,7 @@ QUnit.module('chatter_topbar_tests.js', { QUnit.test('base rendering', async function (assert) { assert.expect(8); + this.data['res.partner'].records.push({ id: 100 }); await this.start(); const chatter = this.env.models['mail.chatter'].create({ threadId: 100, @@ -155,6 +156,7 @@ QUnit.test('base disabled rendering', async function (assert) { QUnit.test('attachment loading is delayed', async function (assert) { assert.expect(4); + this.data['res.partner'].records.push({ id: 100 }); await this.start({ hasTimeControl: true, loadingBaseDelayDuration: 100, @@ -198,6 +200,7 @@ QUnit.test('attachment loading is delayed', async function (assert) { QUnit.test('attachment counter while loading attachments', async function (assert) { assert.expect(4); + this.data['res.partner'].records.push({ id: 100 }); await this.start({ async mockRPC(route) { if (route.includes('ir.attachment/search_read')) { @@ -237,6 +240,7 @@ QUnit.test('attachment counter while loading attachments', async function (asser QUnit.test('attachment counter transition when attachments become loaded)', async function (assert) { assert.expect(7); + this.data['res.partner'].records.push({ id: 100 }); const attachmentPromise = makeTestPromise(); await this.start({ async mockRPC(route) { @@ -295,6 +299,7 @@ QUnit.test('attachment counter transition when attachments become loaded)', asyn QUnit.test('attachment counter without attachments', async function (assert) { assert.expect(4); + this.data['res.partner'].records.push({ id: 100 }); await this.start(); const chatter = this.env.models['mail.chatter'].create({ threadId: 100, @@ -327,6 +332,7 @@ QUnit.test('attachment counter without attachments', async function (assert) { QUnit.test('attachment counter with attachments', async function (assert) { assert.expect(4); + this.data['res.partner'].records.push({ id: 100 }); await this.start({ async mockRPC(route) { if (route.includes('ir.attachment/search_read')) { @@ -376,6 +382,7 @@ QUnit.test('attachment counter with attachments', async function (assert) { QUnit.test('composer state conserved when clicking on another topbar button', async function (assert) { assert.expect(8); + this.data['res.partner'].records.push({ id: 100 }); await this.start(); const chatter = this.env.models['mail.chatter'].create({ threadId: 100, @@ -586,6 +593,7 @@ QUnit.test('rendering with multiple channel followers', async function (assert) QUnit.test('log note/send message switching', async function (assert) { assert.expect(8); + this.data['res.partner'].records.push({ id: 100 }); await this.start(); const chatter = this.env.models['mail.chatter'].create({ threadId: 100, @@ -645,6 +653,7 @@ QUnit.test('log note/send message switching', async function (assert) { QUnit.test('log note toggling', async function (assert) { assert.expect(4); + this.data['res.partner'].records.push({ id: 100 }); await this.start(); const chatter = this.env.models['mail.chatter'].create({ threadId: 100, @@ -684,6 +693,7 @@ QUnit.test('log note toggling', async function (assert) { QUnit.test('send message toggling', async function (assert) { assert.expect(4); + this.data['res.partner'].records.push({ id: 100 }); await this.start(); const chatter = this.env.models['mail.chatter'].create({ threadId: 100, diff --git a/addons/mail/static/src/components/dialog/dialog.js b/addons/mail/static/src/components/dialog/dialog.js index ae6aace8e03..a71be619456 100644 --- a/addons/mail/static/src/components/dialog/dialog.js +++ b/addons/mail/static/src/components/dialog/dialog.js @@ -82,7 +82,7 @@ class Dialog extends Component { ) { return; } - this.dialog.close(); + this.dialog.delete(); } /** @@ -91,7 +91,7 @@ class Dialog extends Component { */ _onKeydownDocument(ev) { if (ev.key === 'Escape') { - this.dialog.close(); + this.dialog.delete(); } } diff --git a/addons/mail/static/src/components/dialog_manager/dialog_manager.xml b/addons/mail/static/src/components/dialog_manager/dialog_manager.xml index 994d7d9c9d7..35d39773cd3 100644 --- a/addons/mail/static/src/components/dialog_manager/dialog_manager.xml +++ b/addons/mail/static/src/components/dialog_manager/dialog_manager.xml @@ -3,7 +3,7 @@
- + Test

", id: 100, - originThread: [['link', thread.main]], + originThread: [['link', thread]], }); await this.createMessageSeenIndicatorComponent({ message, thread }); assert.containsOnce( diff --git a/addons/mail/static/src/env/test_env.js b/addons/mail/static/src/env/test_env.js index b7bf22010cb..5290634510d 100644 --- a/addons/mail/static/src/env/test_env.js +++ b/addons/mail/static/src/env/test_env.js @@ -1,7 +1,6 @@ odoo.define('mail/static/src/env/test_env.js', function (require) { 'use strict'; -const ModelManager = require('mail/static/src/model/model_manager.js'); const { makeDeferred } = require('mail/static/src/utils/deferred/deferred.js'); const { nextTick } = require('mail/static/src/utils/utils.js'); @@ -74,7 +73,7 @@ function addMessagingToEnv(providedEnv = {}) { messaging: undefined, messagingInitializedDeferred: makeDeferred(), messagingBus: new EventBus(), - modelManager: new ModelManager(), + modelManager: undefined, store, }); diff --git a/addons/mail/static/src/js/main.js b/addons/mail/static/src/js/main.js index 6b275ab9851..5137e062df9 100644 --- a/addons/mail/static/src/js/main.js +++ b/addons/mail/static/src/js/main.js @@ -43,7 +43,7 @@ Object.assign(env, { loadingBaseDelayDuration: 400, messaging: undefined, messagingBus: new EventBus(), - modelManager: new ModelManager(), + modelManager: new ModelManager(env), store, }); diff --git a/addons/mail/static/src/model/model_field.js b/addons/mail/static/src/model/model_field.js index 4a003d9f24f..7bdd6beef29 100644 --- a/addons/mail/static/src/model/model_field.js +++ b/addons/mail/static/src/model/model_field.js @@ -13,28 +13,21 @@ class ModelField { //-------------------------------------------------------------------------- constructor({ - autocreate = false, compute, default: def, dependencies = [], dependents = [], + env, fieldName, fieldType, hashes: extraHashes = [], inverse, isCausal = false, - modelManager, related, relationType, to, } = {}) { const id = _.uniqueId('field_'); - /** - * This prop only makes sense for fields of type "relation". If set, - * it automatically creates a new record for this field on creation of - * record, and auto-links with this record. - */ - this.autocreate = autocreate; /** * If set, this field acts as a computed field, and this prop * contains the name of the instance method that computes the value @@ -62,6 +55,10 @@ class ModelField { * `related`. */ this.dependents = dependents; + /** + * The messaging env. + */ + this.env = env; /** * Name of the field in the definition of fields on model. */ @@ -130,10 +127,6 @@ class ModelField { * relation is removed, the related record is automatically deleted. */ this.isCausal = isCausal; - /** - * Reference to the model manager. - */ - this.modelManager = modelManager; /** * If set, this field acts as a related field, and this prop contains * a string that references the related field. It should have the @@ -161,6 +154,11 @@ class ModelField { * model name this relation refers to. */ this.to = to; + + if (!this.default && this.fieldType === 'relation') { + // default value for relational fields is the empty command + this.default = []; + } } /** @@ -238,28 +236,18 @@ class ModelField { * field. * * @param {mail.model} record + * @returns {boolean} whether the value changed for the current field */ doCompute(record) { if (this.compute) { - record.update({ [this.fieldName]: record[this.compute]() }); - return; + return record.update({ [this.fieldName]: record[this.compute]() }); } if (this.related) { - record.update({ [this.fieldName]: this._computeRelated(record) }); - return; + return record.update({ [this.fieldName]: this._computeRelated(record) }); } throw new Error("No compute method defined on this field definition"); } - /** - * Get the env with messaging. - * - * @returns {mail/static/src/env/env.js} - */ - get env() { - return this.modelManager.env; - } - /** * Get the value associated to this field. Relations must convert record * local ids to records. @@ -272,15 +260,10 @@ class ModelField { return this.read(record); } if (this.fieldType === 'relation') { - const OtherModel = this.env.models[this.to]; if (['one2one', 'many2one'].includes(this.relationType)) { - return OtherModel.get(this.read(record)); + return this.read(record); } - const res = []; - for (const localId of this.read(record)) { - res.push(OtherModel.get(localId)); - } - return res; + return [...this.read(record)]; } throw new Error(`cannot get field with unsupported type ${this.fieldType}.`); } @@ -302,103 +285,63 @@ class ModelField { * * @param {mail.model} record * @param {any} newVal + * @returns {boolean} whether the value changed for the current field */ set(record, newVal) { + const currentValue = this.read(record); if (this.fieldType === 'attribute') { - if (this.read(record) === newVal) { - return; + if (currentValue === newVal) { + return false; } - this.write(record, newVal); + record.__values[this.fieldName] = newVal; + return true; } if (this.fieldType === 'relation') { + let hasChanged = false; for (const val of newVal) { switch (val[0]) { case 'create': - this._setRelationCreate(record, val[1]); + if (this._setRelationCreate(record, val[1])) { + hasChanged = true; + } break; case 'insert': - this._setRelationInsert(record, val[1]); + if (this._setRelationInsert(record, val[1])) { + hasChanged = true; + } break; case 'insert-and-replace': - this._setRelationInsertAndReplace(record, val[1]); + if (this._setRelationInsertAndReplace(record, val[1])) { + hasChanged = true; + } break; case 'link': - this._setRelationLink(record, val[1]); + if (this._setRelationLink(record, val[1])) { + hasChanged = true; + } break; case 'replace': // TODO IMP replace should not unlink-all (task-2270780) - this._setRelationUnlink(record, null); - this._setRelationLink(record, val[1]); + if (this._setRelationUnlink(record, currentValue)) { + hasChanged = true; + } + if (this._setRelationLink(record, val[1])) { + hasChanged = true; + } break; case 'unlink': - this._setRelationUnlink(record, val[1]); + if (this._setRelationUnlink(record, val[1])) { + hasChanged = true; + } break; case 'unlink-all': - this._setRelationUnlink(record, null); + if (this._setRelationUnlink(record, currentValue)) { + hasChanged = true; + } break; } } - } - } - - /** - * Set a value in state associated to this field. Value corresponds exactly - * that what is stored on this field, like local id or list of local ids - * for a relational field. If the value changes, dependent fields are - * automatically registered for (re-)computation. - * - * @param {mail.model} record - * @param {any} newVal - * @param {Object} [param2={}] - * @param {Object} [param2.registerDependents=true] If set, write - * on this field with changed value registers dependent fields for compute. - * Of course, we almost always want to register them, so that they reflect - * the value with their dependencies. Disabling this feature prevents - * useless potentially heavy computation, like when setting default values. - */ - write(record, newVal, { registerDependents = true } = {}) { - if (this._containsRecords(newVal)) { - throw new Error("Forbidden write operation with records!!"); - } - record.__values[this.fieldName] = newVal; - record.__state++; - - if (!registerDependents) { - return; - } - - // flag all dependent fields for compute - const Model = record.constructor; - for (const dependent of this.dependents) { - const [hash, currentFieldName, relatedFieldName] = dependent.split( - this.modelManager.DEPENDENT_INNER_SEPARATOR - ); - const field = Model.fields[currentFieldName]; - if (relatedFieldName) { - if (['one2many', 'many2many'].includes(field.relationType)) { - for (const otherRecord of record[currentFieldName]) { - const OtherModel = otherRecord.constructor; - const field = OtherModel.fields[relatedFieldName]; - if (field && field.hashes.includes(hash)) { - this.modelManager.registerToComputeField(otherRecord, field); - } - } - } else { - const otherRecord = record[currentFieldName]; - if (!otherRecord) { - continue; - } - const OtherModel = otherRecord.constructor; - const field = OtherModel.fields[relatedFieldName]; - if (field && field.hashes.includes(hash)) { - this.modelManager.registerToComputeField(otherRecord, field); - } - } - } else { - if (field && field.hashes.includes(hash)) { - this.modelManager.registerToComputeField(record, field); - } - } + return hasChanged; } } @@ -470,58 +413,23 @@ class ModelField { } } - /** - * Determines whether the provided value contains some records. Useful to - * prevent writing records directly in state of this field, which should be - * treated as buggy design. Indeed, state of field should only contain - * either a primitive type or a simple datastructure containing itself - * simple datastructures too. - * - * @private - * @param {any} val - * @returns {boolean} - */ - _containsRecords(val) { - if (!val) { - return false; - } - if (val instanceof this.env.models['mail.model']) { - return true; - } - if (!(val instanceof Array)) { - return false; - } - if (val.length > 0 && val[0] instanceof this.env.models['mail.model']) { - return true; - } - return false; - } - /** * Converts given value to expected format for x2many processing, which is - * an array of localId. + * an iterable of records. * * @private - * @param {string|mail.model|[]} newValue - * @returns {string[]} + * @param {mail.model|mail.model[]} newValue + * @returns {mail.model[]} */ _setRelationConvertX2ManyValue(newValue) { - if (newValue instanceof Array) { - return newValue.map(v => this._setRelationConvertX2OneValue(v)); + if (typeof newValue[Symbol.iterator] === 'function') { + for (const value of newValue) { + this._verifyRelationalValue(value); + } + return newValue; } - return [this._setRelationConvertX2OneValue(newValue)]; - } - - /** - * Converts given value to expected format for x2one processing, which is - * a localId. - * - * @private - * @param {string|mail.model} newValue - * @returns {string} - */ - _setRelationConvertX2OneValue(newValue) { - return newValue instanceof this.env.models['mail.model'] ? newValue.localId : newValue; + this._verifyRelationalValue(newValue); + return [newValue]; } /** @@ -533,20 +441,12 @@ class ModelField { * @private * @param {mail.model} record * @param {Object|Object[]} data + * @returns {boolean} whether the value changed for the current field */ _setRelationCreate(record, data) { const OtherModel = this.env.models[this.to]; - let other; - if (['one2one', 'many2one'].includes(this.relationType)) { - other = OtherModel.create(data); - } else { - if (data instanceof Array) { - other = data.map(d => OtherModel.create(d)); - } else { - other = OtherModel.create(data); - } - } - this._setRelationLink(record, other); + const other = OtherModel.create(data); + return this._setRelationLink(record, other); } /** @@ -558,20 +458,12 @@ class ModelField { * @private * @param {mail.model} record * @param {Object|Object[]} data + * @returns {boolean} whether the value changed for the current field */ _setRelationInsert(record, data) { const OtherModel = this.env.models[this.to]; - let other; - if (['one2one', 'many2one'].includes(this.relationType)) { - other = OtherModel.insert(data); - } else { - if (data instanceof Array) { - other = data.map(d => OtherModel.insert(d)); - } else { - other = OtherModel.insert(data); - } - } - this._setRelationLink(record, other); + const other = OtherModel.insert(data); + return this._setRelationLink(record, other); } /** @@ -582,40 +474,40 @@ class ModelField { * @private * @param {mail.model} record * @param {Object|Object[]} data + * @returns {boolean} whether the value changed for the current field */ _setRelationInsertAndReplace(record, data) { // unlink must be done before insert: // because unlink might trigger delete due to causality and new data // shouldn't be deleted just after being inserted // TODO IMP insert-and-replace should not unlink-all (task-2270780) - this._setRelationUnlink(record, null); + let hasChanged = false; + if (this._setRelationUnlink(record, this.read(record))) { + hasChanged = true; + } const OtherModel = this.env.models[this.to]; const other = OtherModel.insert(data); - this._setRelationLink(record, other); + if (this._setRelationLink(record, other)) { + hasChanged = true; + } + return hasChanged; } /** * Set a 'link' operation on this relational field. * * @private - * @param {string|string[]|mail.model|mail.model[]} newValue + * @param {mail.model|mail.model[]} newValue + * @returns {boolean} whether the value changed for the current field */ _setRelationLink(record, newValue) { - if (!record.exists()) { - // current record may be deleted due to causality - return; - } switch (this.relationType) { case 'many2many': case 'one2many': - this._setRelationLinkX2Many(record, newValue); - break; + return this._setRelationLinkX2Many(record, newValue); case 'many2one': - this._setRelationLinkMany2One(record, newValue); - break; case 'one2one': - this._setRelationLinkOne2One(record, newValue); - break; + return this._setRelationLinkX2One(record, newValue); } } @@ -624,141 +516,54 @@ class ModelField { * * @private * @param {mail.model} record - * @param {string|mail.model|[]} newValue + * @param {mail.model|mail.model[]} newValue + * @returns {boolean} whether the value changed for the current field */ _setRelationLinkX2Many(record, newValue) { - // convert newValue to array of localId - const newLocalIds = this._setRelationConvertX2ManyValue(newValue); - const OtherModel = this.env.models[this.to]; + const toAddOtherRecords = this._setRelationConvertX2ManyValue(newValue); + const otherRecords = this.read(record); - const prevLocalIds = this.read(record); - const newOtherRecords = newLocalIds - .map(newLocalId => OtherModel.get(newLocalId)) - .filter(newOtherRecord => { - // other record may be deleted due to causality, avoid linking - // deleted records - if (!newOtherRecord) { - return false; - } - // other record already linked, avoid linking twice - if (prevLocalIds.has(newOtherRecord.localId)) { - return false; - } - return true; - }); - if (newOtherRecords.length === 0) { - return; - } - // link other records to current record - for (const newOtherRecord of newOtherRecords) { - prevLocalIds.add(newOtherRecord.localId); - } - this.write(record, prevLocalIds); - - // link current record to other records - for (const newOtherRecord of newOtherRecords) { - newOtherRecord.update({ + let isAdding = false; + for (const toAddOtherRecord of toAddOtherRecords) { + // other record already linked, avoid linking twice + if (otherRecords.has(toAddOtherRecord)) { + continue; + } + isAdding = true; + // link other records to current record + otherRecords.add(toAddOtherRecord); + // link current record to other records + toAddOtherRecord.update({ [this.inverse]: [['link', record]], }); } + return isAdding; } /** - * Handling of a `set` 'link' of a many2one relational field. + * Handling of a `set` 'link' of an x2one relational field. * * @private * @param {mail.model} record - * @param {string|mail.model} newValue + * @param {mail.model} toAddOtherRecord + * @returns {boolean} whether the value changed for the current field */ - _setRelationLinkMany2One(record, newValue) { - // convert newValue to localId - const newLocalId = this._setRelationConvertX2OneValue(newValue); - const OtherModel = this.env.models[this.to]; - const prevLocalId = this.read(record); - + _setRelationLinkX2One(record, toAddOtherRecord) { + this._verifyRelationalValue(toAddOtherRecord); + const prevOtherRecord = this.read(record); // other record already linked, avoid linking twice - if (prevLocalId === newLocalId) { - return; + if (prevOtherRecord === toAddOtherRecord) { + return false; } - - // unlink previous other record from current record - this.write(record, undefined); - - const prevOtherRecord = OtherModel.get(prevLocalId); - // there may be no previous other record or the previous other record - // may be deleted due to causality - if (prevOtherRecord) { - // unlink current record from previous other record - prevOtherRecord.update({ - [this.inverse]: [['unlink', record]], - }); - } - - const newOtherRecord = OtherModel.get(newLocalId); - // other record may be deleted due to causality, avoid linking - // deleted records - if (!newOtherRecord) { - return; - } - - // link other record to current records - this.write(record, newLocalId); - - // link current record to other record - newOtherRecord.update({ - [this.inverse]: [['link', record]], - }); - } - - /** - * Handling of a `set` 'link' of an one2one relational field. - * - * @private - * @param {mail.model} record - * @param {string|mail.model} value - */ - _setRelationLinkOne2One(record, newValue) { - // convert newValue to localId - const newLocalId = this._setRelationConvertX2OneValue(newValue); - const prevLocalId = this.read(record); - const OtherModel = this.env.models[this.to]; - - // other record already linked, avoid linking twice - if (prevLocalId === newLocalId) { - return; - } - - // unlink previous other record from current record - this.write(record, undefined); - - const prevOtherRecord = OtherModel.get(prevLocalId); - // there may be no previous other record or the previous other record - // may be deleted due to causality - if (prevOtherRecord) { - // unlink current record from previous other record - prevOtherRecord.update({ - [this.inverse]: [['unlink', record]], - }); - // apply causality - if (this.isCausal) { - prevOtherRecord.delete(); - } - } - - const newOtherRecord = OtherModel.get(newLocalId); - // other record may be deleted due to causality, avoid linking deleted - // records - if (!newOtherRecord) { - return; - } - + // unlink to properly update previous inverse before linking new value + this._setRelationUnlinkX2One(record); // link other record to current record - this.write(record, newLocalId); - + record.__values[this.fieldName] = toAddOtherRecord; // link current record to other record - newOtherRecord.update({ + toAddOtherRecord.update({ [this.inverse]: [['link', record]], }); + return true; } /** @@ -766,24 +571,17 @@ class ModelField { * * @private * @param {mail.model} record - * @param {string|string[]|mail.model|mail.model[]|null} newValue + * @param {mail.model|mail.model[]} newValue + * @returns {boolean} whether the value changed for the current field */ _setRelationUnlink(record, newValue) { - if (!record.exists()) { - // current record may be deleted due to causality - return; - } switch (this.relationType) { case 'many2many': case 'one2many': - this._setRelationUnlinkX2Many(record, newValue); - break; + return this._setRelationUnlinkX2Many(record, newValue); case 'many2one': - this._setRelationUnlinkMany2One(record); - break; case 'one2one': - this._setRelationUnlinkOne2One(record); - break; + return this._setRelationUnlinkX2One(record); } } @@ -792,97 +590,72 @@ class ModelField { * * @private * @param {mail.model} record - * @param {string|mail.model|[]|null} newValue + * @param {mail.model|mail.model[]} newValue + * @returns {boolean} whether the value changed for the current field */ _setRelationUnlinkX2Many(record, newValue) { - // convert newValue to array of localId, null is considered unlink all - const otherLocalIds = newValue === null - ? [...this.read(record)] - : this._setRelationConvertX2ManyValue(newValue); - const OtherModel = this.env.models[this.to]; + const toDeleteOtherRecords = this._setRelationConvertX2ManyValue(newValue); + const otherRecords = this.read(record); - const prevLocalIds = this.read(record); let isDeleting = false; - for (const otherLocalId of otherLocalIds) { - const wasDeleted = prevLocalIds.delete(otherLocalId); - if (wasDeleted) { - isDeleting = true; - } - } - if (!isDeleting) { - return; - } - // unlink other record from current record - this.write(record, prevLocalIds); - - for (const otherLocalId of otherLocalIds) { - const otherRecord = OtherModel.get(otherLocalId); - // other record may be deleted due to causality, avoid useless - // processing - if (otherRecord) { - // unlink current record from other record - otherRecord.update({ - [this.inverse]: [['unlink', record]], - }); + for (const toDeleteOtherRecord of toDeleteOtherRecords) { + // unlink other record from current record + const wasDeleted = otherRecords.delete(toDeleteOtherRecord); + if (!wasDeleted) { + continue; + } + isDeleting = true; + // unlink current record from other records + toDeleteOtherRecord.update({ + [this.inverse]: [['unlink', record]], + }); + // apply causality + if (this.isCausal) { + toDeleteOtherRecord.delete(); } } + return isDeleting; } /** - * Handling of a `set` 'unlink' of a many2one relational field. + * Handling of a `set` 'unlink' of a x2one relational field. * * @private * @param {mail.model} record + * @returns {boolean} whether the value changed for the current field */ - _setRelationUnlinkMany2One(record) { - const otherLocalId = this.read(record); - const OtherModel = this.env.models[this.to]; - + _setRelationUnlinkX2One(record) { + const toDeleteOtherRecord = this.read(record); // other record already unlinked, avoid useless processing - if (!otherLocalId) { - return; + if (!toDeleteOtherRecord) { + return false; } - // unlink other record from current record - this.write(record, undefined); - - const otherRecord = OtherModel.get(otherLocalId); - // other record may be deleted due to causality, avoid useless - // processing - if (otherRecord) { - // unlink current record from other record - otherRecord.update({ - [this.inverse]: [['unlink', record]], - }); + record.__values[this.fieldName] = undefined; + // unlink current record from other record + toDeleteOtherRecord.update({ + [this.inverse]: [['unlink', record]], + }); + // apply causality + if (this.isCausal) { + toDeleteOtherRecord.delete(); } + return true; } /** - * Handling of a `set` 'unlink' of an one2one relational field. + * Verifies the given relational value makes sense for the current field. + * In particular the given value must be a record, it must be non-deleted, + * and it must originates from relational `to` model (or its subclasses). * * @private * @param {mail.model} record + * @throws {Error} if record does not satisfy related model */ - _setRelationUnlinkOne2One(record) { - const otherLocalId = this.read(record); + _verifyRelationalValue(record) { const OtherModel = this.env.models[this.to]; - - // other record already unlinked, avoid useless processing - if (!otherLocalId) { - return; - } - - // unlink other record from current record - this.write(record, undefined); - - const otherRecord = OtherModel.get(otherLocalId); - // other record may be deleted due to causality, avoid useless - // processing - if (otherRecord) { - // unlink current record from other record - otherRecord.update({ - [this.inverse]: [['unlink', record]], - }); + if (!OtherModel.get(record.localId, { isCheckingInheritance: true })) { + throw Error(`Record ${record.localId} is not valid for relational field ${this.fieldName}.`); } } diff --git a/addons/mail/static/src/model/model_manager.js b/addons/mail/static/src/model/model_manager.js index 3b1d52a36e1..0267f4b293e 100644 --- a/addons/mail/static/src/model/model_manager.js +++ b/addons/mail/static/src/model/model_manager.js @@ -30,13 +30,10 @@ class ModelManager { * Set as public attribute so that it can be used by model field. */ this.DEPENDENT_INNER_SEPARATOR = DEPENDENT_INNER_SEPARATOR; - /** - * The messaging env. It is passed on start(), due to the way mocking - * of OWL env in tests work... + * The messaging env. */ - this.env = undefined; - + this.env = env; /** * Whether this is currently handling an "update after" on a record. * Useful to determine if we should process computed/related fields. @@ -73,8 +70,7 @@ class ModelManager { * Called when all JS modules that register or patch models have been * done. This launches generation of models. */ - start(env) { - this.env = env; + start() { /** * Generate the models. */ @@ -154,36 +150,28 @@ class ModelManager { Model.__records[record.localId] = record; record.init(); - // Make default values of its fields for newly created record. + // Ensure X2many relations are Set initially (other fields can stay undefined). for (const field of fields) { - if (field.fieldType === 'attribute') { - field.write(record, field.default, { registerDependents: false }); - } if (field.fieldType === 'relation') { if (['one2many', 'many2many'].includes(field.relationType)) { - // Ensure X2many relations are Set by defaults. - field.write(record, new Set(), { registerDependents: false }); - } else { - field.write(record, undefined, { registerDependents: false }); + record.__values[field.fieldName] = new Set(); } } } - const data2 = Object.assign({}, data); + const data2 = {}; for (const field of fields) { - if (field.fieldType !== 'relation') { - continue; + if (field.fieldName in data) { + data2[field.fieldName] = data[field.fieldName]; + } else { + data2[field.fieldName] = field.default; } - if (!field.autocreate) { - continue; - } - data2[field.fieldName] = [['create']]; } for (const field of fields) { if (field.compute || field.related) { // new record should always invoke computed fields. - this.registerToComputeField(record, field); + this._registerToComputeField(record, field); } } @@ -206,33 +194,16 @@ class ModelManager { this._updateCycle(() => { const Model = record.constructor; if (!record.exists()) { - // Record has already been deleted. - // (e.g. unlinking one of its reverse relation was causal) - return; + throw Error(`Cannot delete already deleted record ${record.localId}.`); } - const data = {}; - const recordRelations = Object.values(Model.fields) - .filter(field => field.fieldType === 'relation'); - for (const relation of recordRelations) { - if (relation.isCausal) { - switch (relation.relationType) { - case 'one2one': - case 'many2one': - if (record[relation.fieldName]) { - record[relation.fieldName].delete(); - } - break; - case 'one2many': - case 'many2many': - for (const relatedRecord of record[relation.fieldName]) { - relatedRecord.delete(); - } - break; - } + for (const field of Object.values(Model.fields)) { + if (field.fieldType === 'relation') { + // ensure inverses are properly unlinked + field.set(record, [['unlink-all']]); } - data[relation.fieldName] = [['unlink-all']]; } - record.update(data); + this._toComputeFields.delete(record); + this._toUpdateAfters.delete(record); delete Model.__records[record.localId]; }); } @@ -283,9 +254,11 @@ class ModelManager { * * @param {mail.model} Model class * @param {string} localId + * @param {Object} param2 + * @param {boolean} [param2.isCheckingInheritance=false] * @returns {mail.model|undefined} record, if exists */ - get(Model, localId) { + get(Model, localId, { isCheckingInheritance = false } = {}) { if (!localId) { return; } @@ -293,13 +266,20 @@ class ModelManager { if (record) { return record; } + if (!isCheckingInheritance) { + return; + } // support for inherited models (eg. relation targeting `mail.model`) - for (const Model of Object.values(this.env.models)) { - const record = Model.__records[localId]; + for (const SubModel of Object.values(this.env.models)) { + if (!(SubModel.prototype instanceof Model)) { + continue; + } + const record = SubModel.__records[localId]; if (record) { return record; } } + return; } /** @@ -339,27 +319,39 @@ class ModelManager { * * @param {mail.model} record * @param {Object} data + * @returns {boolean} whether any value changed for the current record */ update(record, data) { - this._updateCycle(() => { - this._updateDirect(record, data); + return this._updateCycle(() => { + if (!record.exists()) { + throw Error(`Cannot update already deleted record ${record.localId}.`); + } + if (!this._toUpdateAfters.has(record)) { + // queue updateAfter before calling field.set to ensure previous + // contains the value at the start of update cycle + this._toUpdateAfters.set(record, record._updateBefore()); + } + const Model = record.constructor; + let hasChanged = false; + for (const fieldName of Object.keys(data)) { + const field = Model.fields[fieldName]; + if (!field) { + throw new Error(`Cannot create/update record with data unrelated to a field. (model: "${Model.modelName}", non-field attempted update: "${fieldName}")`); + } + if (!field.set(record, data[fieldName])) { + continue; + } + hasChanged = true; + // flag all dependent fields for compute + this._registerComputeOfDependents(record, field); + } + if (hasChanged) { + record.__state++; + } + return hasChanged; }); } - /** - * Register a pair record/field for the compute step of the update cycle in - * progress. - * - * @param {mail.model} record - * @param {ModelField} field - */ - registerToComputeField(record, field) { - if (!this._toComputeFields.has(record)) { - this._toComputeFields.set(record, new Set()); - } - this._toComputeFields.get(record).add(field); - } - //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- @@ -408,7 +400,6 @@ class ModelManager { if (field.fieldType === 'attribute') { const invalidKeys = Object.keys(field).filter(key => ![ - 'autocreate', 'compute', 'default', 'dependencies', @@ -423,8 +414,8 @@ class ModelManager { if (field.fieldType === 'relation') { const invalidKeys = Object.keys(field).filter(key => ![ - 'autocreate', 'compute', + 'default', 'dependencies', 'fieldType', 'inverse', @@ -740,6 +731,7 @@ class ModelManager { {}, relFunc(Model.modelName, { inverse: field.fieldName }), { + env: this.env, fieldName: `_inverse_${Model.modelName}/${field.fieldName}`, modelManager: this, } @@ -770,6 +762,7 @@ class ModelManager { // Make fields aware of their field name. for (const [fieldName, fieldData] of Object.entries(Model.fields)) { Model.fields[fieldName] = new ModelField(Object.assign({}, fieldData, { + env: this.env, fieldName, modelManager: this, })); @@ -880,6 +873,61 @@ class ModelManager { } } + /** + * Registers compute of dependents for the given field, if applicable. + * + * @param {mail.model} record + * @param {ModelField} field + */ + _registerComputeOfDependents(record, field) { + const Model = record.constructor; + for (const dependent of field.dependents) { + const [hash, currentFieldName, relatedFieldName] = dependent.split( + this.DEPENDENT_INNER_SEPARATOR + ); + const field = Model.fields[currentFieldName]; + if (relatedFieldName) { + if (['one2many', 'many2many'].includes(field.relationType)) { + for (const otherRecord of record[currentFieldName]) { + const OtherModel = otherRecord.constructor; + const field = OtherModel.fields[relatedFieldName]; + if (field && field.hashes.includes(hash)) { + this._registerToComputeField(otherRecord, field); + } + } + } else { + const otherRecord = record[currentFieldName]; + if (!otherRecord) { + continue; + } + const OtherModel = otherRecord.constructor; + const field = OtherModel.fields[relatedFieldName]; + if (field && field.hashes.includes(hash)) { + this._registerToComputeField(otherRecord, field); + } + } + } else { + if (field && field.hashes.includes(hash)) { + this._registerToComputeField(record, field); + } + } + } + } + + /** + * Register a pair record/field for the compute step of the update cycle in + * progress. + * + * @param {mail.model} record + * @param {ModelField} field + */ + _registerToComputeField(record, field) { + if (!this._toComputeFields.has(record)) { + this._toComputeFields.set(record, new Set()); + } + this._toComputeFields.get(record).add(field); + } + /** * Process registered computed fields in the current update cycle. * @@ -890,7 +938,7 @@ class ModelManager { for (const [record, fields] of this._toComputeFields) { this._toComputeFields.delete(record); if (!record.exists()) { - continue; + throw Error(`Cannot execute computes for already deleted record ${record.localId}.`); } while (fields.size > 0) { for (const field of fields) { @@ -924,9 +972,10 @@ class ModelManager { while (this._toUpdateAfters.size > 0) { for (const [record, previous] of this._toUpdateAfters) { this._toUpdateAfters.delete(record); - if (record.exists()) { - record._updateAfter(previous); + if (!record.exists()) { + throw Error(`Cannot _updateAfter for already deleted record ${record.localId}.`); } + record._updateAfter(previous); } } this._isHandlingToUpdateAfters = false; @@ -950,29 +999,6 @@ class ModelManager { return res; } - /** - * Process a direct update on given record with provided data. - * - * @private - * @param {mail.model} record - * @param {Object} data - */ - _updateDirect(record, data) { - if (!this._toUpdateAfters.has(record)) { - // queue updateAfter before calling field.set to ensure previous - // contains the value at the start of update cycle - this._toUpdateAfters.set(record, record._updateBefore()); - } - const Model = record.constructor; - for (const fieldName of Object.keys(data)) { - const field = Model.fields[fieldName]; - if (!field) { - throw new Error(`Cannot create/update record with data unrelated to a field. (model: "${Model.modelName}", non-field attempted update: "${fieldName}")`); - } - field.set(record, data[fieldName]); - } - } - } return ModelManager; diff --git a/addons/mail/static/src/models/attachment/attachment.js b/addons/mail/static/src/models/attachment/attachment.js index d4dcfb20fac..a9adce2579f 100644 --- a/addons/mail/static/src/models/attachment/attachment.js +++ b/addons/mail/static/src/models/attachment/attachment.js @@ -87,7 +87,7 @@ function factory(dependencies) { return; } this.env.messaging.dialogManager.open('mail.attachment_viewer', { - attachment: [['replace', attachment]], + attachment: [['link', attachment]], attachments: [['replace', attachments]], }); } diff --git a/addons/mail/static/src/models/chat_window_manager/chat_window_manager.js b/addons/mail/static/src/models/chat_window_manager/chat_window_manager.js index 3b054f83a30..5070508b621 100644 --- a/addons/mail/static/src/models/chat_window_manager/chat_window_manager.js +++ b/addons/mail/static/src/models/chat_window_manager/chat_window_manager.js @@ -254,9 +254,9 @@ function factory(dependencies) { _computeLastVisible() { const { length: l, [l - 1]: lastVisible } = this.allOrderedVisible; if (!lastVisible) { - return [['unlink-all']]; + return [['unlink']]; } - return [['replace', lastVisible]]; + return [['link', lastVisible]]; } /** @@ -268,7 +268,7 @@ function factory(dependencies) { if (!chatWindow) { return [['unlink']]; } - return [['replace', chatWindow]]; + return [['link', chatWindow]]; } /** diff --git a/addons/mail/static/src/models/chatter/chatter.js b/addons/mail/static/src/models/chatter/chatter.js index 4df875a0898..61757004a91 100644 --- a/addons/mail/static/src/models/chatter/chatter.js +++ b/addons/mail/static/src/models/chatter/chatter.js @@ -50,6 +50,10 @@ function factory(dependencies) { } async refreshActivities() { + if (!this.thread || this.thread.isTemporary) { + this.update({ activities: [['unlink-all']] }); + return; + } // A bit "extreme", may be improved const [{ activity_ids: newActivityIds }] = await this.async(() => this.env.services.rpc({ model: this.thread.model, @@ -166,18 +170,24 @@ function factory(dependencies) { } } - if (previous.activityIds.join(',') !== this.activityIds.join(',')) { + if ( + !previous.activityIds || + previous.activityIds.join(',') !== this.activityIds.join(',') + ) { this.refreshActivities(); } if ( - previous.followerIds.join(',') !== this.followerIds.join(',') && - !this.thread.isTemporary + !previous.followerIds || + previous.followerIds.join(',') !== this.followerIds.join(',') ) { - this.thread.refreshFollowers(); + if (this.thread) { + this.thread.refreshFollowers(); + } } if ( + !previous.messageIds || previous.thread !== this.thread || - (this.thread && this.messageIds.join(',') !== previous.messageIds.join(',')) + this.messageIds.join(',') !== previous.messageIds.join(',') ) { this.refresh(); } @@ -316,7 +326,7 @@ function factory(dependencies) { threadId: attr(), threadModel: attr(), threadViewer: one2one('mail.thread_viewer', { - autocreate: true, + default: [['create']], }), todayActivities: one2many('mail.activity', { compute: '_computeTodayActivities', diff --git a/addons/mail/static/src/models/dialog/dialog.js b/addons/mail/static/src/models/dialog/dialog.js index d4b0bcddfb5..018951fe092 100644 --- a/addons/mail/static/src/models/dialog/dialog.js +++ b/addons/mail/static/src/models/dialog/dialog.js @@ -6,27 +6,7 @@ const { many2one, one2one } = require('mail/static/src/model/model_field.js'); function factory(dependencies) { - class Dialog extends dependencies['mail.model'] { - - /** - * @override - */ - delete() { - if (this.manager) { - this.manager.unregister(this); - } - super.delete(); - } - - //---------------------------------------------------------------------- - // Public - //---------------------------------------------------------------------- - - close() { - this.delete(); - } - - } + class Dialog extends dependencies['mail.model'] {} Dialog.fields = { manager: many2one('mail.dialog_manager', { diff --git a/addons/mail/static/src/models/dialog_manager/dialog_manager.js b/addons/mail/static/src/models/dialog_manager/dialog_manager.js index 024172f7cdf..4d86e340cee 100644 --- a/addons/mail/static/src/models/dialog_manager/dialog_manager.js +++ b/addons/mail/static/src/models/dialog_manager/dialog_manager.js @@ -2,7 +2,7 @@ odoo.define('mail/static/src/models/dialog_manager/dialog_manager.js', function 'use strict'; const { registerNewModel } = require('mail/static/src/model/model_core.js'); -const { attr, one2many } = require('mail/static/src/model/model_field.js'); +const { one2many } = require('mail/static/src/model/model_field.js'); function factory(dependencies) { @@ -12,22 +12,11 @@ function factory(dependencies) { // Public //---------------------------------------------------------------------- - /** - * @param {mail.dialog} dialog - */ - close(dialog) { - this.unregister(dialog); - dialog.delete(); - } - /** * @param {string} modelName * @param {Object} [recordData] */ open(modelName, recordData) { - const dialog = this.env.models['mail.dialog'].create({ - manager: [['link', this]], - }); if (!modelName) { throw new Error("Dialog should have a link to a model"); } @@ -36,53 +25,17 @@ function factory(dependencies) { throw new Error(`No model exists with name ${modelName}`); } const record = Model.create(recordData); - dialog.update({ record: [['link', record]] }); - this.update({ _ordered: this._ordered.concat([dialog.localId]) }); + const dialog = this.env.models['mail.dialog'].create({ + manager: [['link', this]], + record: [['link', record]], + }); return dialog; } - /** - * @param {mail.dialog} dialog - */ - unregister(dialog) { - if (!this.allOrdered.includes(dialog)) { - return; - } - this.update({ - _ordered: this._ordered.filter( - dialogLocalId => dialogLocalId !== dialog.localId - ), - dialogs: [['unlink', dialog]], - }); - } - - //---------------------------------------------------------------------- - // Private - //---------------------------------------------------------------------- - - /** - * FIXME: dependent on implementation that uses arbitrary order in relations!! - * - * @private - * @returns {mail.dialog} - */ - _computeAllOrdered() { - return [['replace', this._ordered.map(dialogLocalId => - this.env.models['mail.dialog'].get(dialogLocalId) - )]]; - } } DialogManager.fields = { - _ordered: attr({ default: [] }), - // FIXME: dependent on implementation that uses arbitrary order in relations!! - allOrdered: one2many('mail.dialog', { - compute: '_computeAllOrdered', - dependencies: [ - '_ordered', - 'dialogs', - ], - }), + // FIXME: dependent on implementation that uses insert order in relations!! dialogs: one2many('mail.dialog', { inverse: 'manager', isCausal: true, diff --git a/addons/mail/static/src/models/discuss/discuss.js b/addons/mail/static/src/models/discuss/discuss.js index 7b4961c87be..b111c8726f8 100644 --- a/addons/mail/static/src/models/discuss/discuss.js +++ b/addons/mail/static/src/models/discuss/discuss.js @@ -462,7 +462,7 @@ function factory(dependencies) { related: 'thread.model', }), threadViewer: one2one('mail.thread_viewer', { - autocreate: true, + default: [['create']], isCausal: true, }), }; diff --git a/addons/mail/static/src/models/follower/follower.js b/addons/mail/static/src/models/follower/follower.js index 42e7216392e..4c44b788d4d 100644 --- a/addons/mail/static/src/models/follower/follower.js +++ b/addons/mail/static/src/models/follower/follower.js @@ -111,7 +111,7 @@ function factory(dependencies) { } } this._subtypesListDialog = this.env.messaging.dialogManager.open('mail.follower_subtype_list', { - follower: [['replace', this]], + follower: [['link', this]], }); } diff --git a/addons/mail/static/src/models/message_seen_indicator/message_seen_indicator.js b/addons/mail/static/src/models/message_seen_indicator/message_seen_indicator.js index 19b4706ba1f..78f7431214f 100644 --- a/addons/mail/static/src/models/message_seen_indicator/message_seen_indicator.js +++ b/addons/mail/static/src/models/message_seen_indicator/message_seen_indicator.js @@ -20,8 +20,8 @@ function factory(dependencies) { */ static computeId(message, thread) { if (message && thread) { - const messageId = Number.isInteger(message) ? message : message.id; - const threadId = Number.isInteger(thread) ? thread : thread.id; + const messageId = typeof message !== 'object' ? message : message.id; + const threadId = typeof thread !== 'object' ? thread : thread.id; return [messageId, threadId].join('-'); } return undefined; diff --git a/addons/mail/static/src/models/messaging/messaging.js b/addons/mail/static/src/models/messaging/messaging.js index b58de718cde..a4e5d27eced 100644 --- a/addons/mail/static/src/models/messaging/messaging.js +++ b/addons/mail/static/src/models/messaging/messaging.js @@ -132,7 +132,7 @@ function factory(dependencies) { default: {}, }), chatWindowManager: one2one('mail.chat_window_manager', { - autocreate: true, + default: [['create']], inverse: 'messaging', isCausal: true, }), @@ -142,15 +142,15 @@ function factory(dependencies) { currentPartner: one2one('mail.partner'), currentUser: one2one('mail.user'), device: one2one('mail.device', { - autocreate: true, + default: [['create']], isCausal: true, }), dialogManager: one2one('mail.dialog_manager', { - autocreate: true, + default: [['create']], isCausal: true, }), discuss: one2one('mail.discuss', { - autocreate: true, + default: [['create']], inverse: 'messaging', isCausal: true, }), @@ -163,7 +163,7 @@ function factory(dependencies) { */ inbox: one2one('mail.thread'), initializer: one2one('mail.messaging_initializer', { - autocreate: true, + default: [['create']], inverse: 'messaging', isCausal: true, }), @@ -171,11 +171,11 @@ function factory(dependencies) { default: false, }), locale: one2one('mail.locale', { - autocreate: true, + default: [['create']], isCausal: true, }), messagingMenu: one2one('mail.messaging_menu', { - autocreate: true, + default: [['create']], inverse: 'messaging', isCausal: true, }), @@ -184,11 +184,11 @@ function factory(dependencies) { */ moderation: one2one('mail.thread'), notificationGroupManager: one2one('mail.notification_group_manager', { - autocreate: true, + default: [['create']], isCausal: true, }), notificationHandler: one2one('mail.messaging_notification_handler', { - autocreate: true, + default: [['create']], inverse: 'messaging', isCausal: true, }), diff --git a/addons/mail/static/src/models/model/model.js b/addons/mail/static/src/models/model/model.js index c7e4e4f2641..be02afeeaee 100644 --- a/addons/mail/static/src/models/model/model.js +++ b/addons/mail/static/src/models/model/model.js @@ -89,10 +89,12 @@ function factory() { * * @static * @param {string} localId + * @param {Object} param1 + * @param {boolean} [param1.isCheckingInheritance] * @returns {mail.model|undefined} */ - static get(localId) { - return this.env.modelManager.get(this, localId); + static get(localId, { isCheckingInheritance } = {}) { + return this.env.modelManager.get(this, localId, { isCheckingInheritance }); } /** diff --git a/addons/mail/static/src/models/thread/thread.js b/addons/mail/static/src/models/thread/thread.js index 4459912d6f6..6b6f9a4c1d4 100644 --- a/addons/mail/static/src/models/thread/thread.js +++ b/addons/mail/static/src/models/thread/thread.js @@ -542,7 +542,7 @@ function factory(dependencies) { openExpanded() { const discuss = this.env.messaging.discuss; if (['mail.channel', 'mail.box'].includes(this.model)) { - discuss.threadViewer.update({ thread: [['replace', this]] }); + discuss.threadViewer.update({ thread: [['link', this]] }); this.env.bus.trigger('do-action', { action: 'mail.action_discuss', options: { @@ -581,6 +581,10 @@ function factory(dependencies) { * Refresh followers information from server. */ async refreshFollowers() { + if (this.isTemporary) { + this.update({ followers: [['unlink-all']] }); + return; + } // FIXME Do that with only one RPC (see task-2243180) const [{ message_follower_ids: followerIds }] = await this.async(() => this.env.services.rpc({ model: this.model, @@ -895,7 +899,7 @@ function factory(dependencies) { ) { return [['unlink-all']]; } - return [['replace', currentPartnerOrderedSeenMessages.slice().pop()]]; + return [['link', currentPartnerOrderedSeenMessages.slice().pop()]]; } /** @@ -907,7 +911,10 @@ function factory(dependencies) { length: l, [l - 1]: lastMessage, } = this.orderedMessages; - return [['replace', lastMessage]]; + if (lastMessage) { + return [['link', lastMessage]]; + } + return [['unlink']]; } /** @@ -922,7 +929,10 @@ function factory(dependencies) { length: l, [l - 1]: lastNeedactionMessage, } = orderedNeedactionMessages; - return [['replace', lastNeedactionMessage]]; + if (lastNeedactionMessage) { + return [['link', lastNeedactionMessage]]; + } + return [['unlink']]; } /** diff --git a/addons/mail/static/src/models/thread_cache/thread_cache.js b/addons/mail/static/src/models/thread_cache/thread_cache.js index 9d0b4e98e57..0c2ef2af35f 100644 --- a/addons/mail/static/src/models/thread_cache/thread_cache.js +++ b/addons/mail/static/src/models/thread_cache/thread_cache.js @@ -142,9 +142,9 @@ function factory(dependencies) { [l - 1]: lastFetchedMessage, } = this.orderedFetchedMessages; if (!lastFetchedMessage) { - return [['unlink-all']]; + return [['unlink']]; } - return [['replace', lastFetchedMessage]]; + return [['link', lastFetchedMessage]]; } /** @@ -157,9 +157,9 @@ function factory(dependencies) { [l - 1]: lastMessage, } = this.orderedMessages; if (!lastMessage) { - return [['unlink-all']]; + return [['unlink']]; } - return [['replace', lastMessage]]; + return [['link', lastMessage]]; } /** diff --git a/addons/mail/static/src/models/thread_viewer/thread_viewer.js b/addons/mail/static/src/models/thread_viewer/thread_viewer.js index 52fadc74015..413448c6c6f 100644 --- a/addons/mail/static/src/models/thread_viewer/thread_viewer.js +++ b/addons/mail/static/src/models/thread_viewer/thread_viewer.js @@ -72,7 +72,7 @@ function factory(dependencies) { */ handleVisibleMessage(message) { if (!this.lastVisibleMessage || this.lastVisibleMessage.id < message.id) { - this.update({ lastVisibleMessage: [['replace', message]] }); + this.update({ lastVisibleMessage: [['link', message]] }); } } @@ -102,7 +102,7 @@ function factory(dependencies) { if (!this.thread) { return []; } - return [['replace', this.thread.cache(this.stringifiedDomain)]]; + return [['link', this.thread.cache(this.stringifiedDomain)]]; } /** diff --git a/addons/mail/static/src/utils/test_utils.js b/addons/mail/static/src/utils/test_utils.js index dd305be0a49..801daddba20 100644 --- a/addons/mail/static/src/utils/test_utils.js +++ b/addons/mail/static/src/utils/test_utils.js @@ -7,6 +7,7 @@ const { addMessagingToEnv, addTimeControlToEnv, } = require('mail/static/src/env/test_env.js'); +const ModelManager = require('mail/static/src/model/model_manager.js'); const ChatWindowService = require('mail/static/src/services/chat_window_service/chat_window_service.js'); const DialogService = require('mail/static/src/services/dialog_service/dialog_service.js'); const { nextTick } = require('mail/static/src/utils/utils.js'); @@ -754,6 +755,9 @@ async function start(param0 = {}) { } testEnv = Component.env; + Object.assign(testEnv, { + modelManager: new ModelManager(testEnv), + }); /** * Components cannot use web.bus, because they cannot use