summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSébastien Theys <seb@odoo.com>2020-08-05 14:03:05 +0000
committerSébastien Theys <seb@odoo.com>2020-08-05 15:09:34 +0000
commit9216bdcb6f0bbb20900a33bbf7283e6e4fda1b2c (patch)
treece2c02403afc038f552436997fb3ba9f3ab1c1ac
parent9432d547053605a0807e82499123c0deede23d6b (diff)
[FIX] mail: clean up JS relations by storing records
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) <aku@odoo.com>
-rw-r--r--addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js9
-rw-r--r--addons/mail/static/src/components/chatter/chatter_tests.js5
-rw-r--r--addons/mail/static/src/components/chatter_topbar/chatter_topbar_tests.js10
-rw-r--r--addons/mail/static/src/components/dialog/dialog.js4
-rw-r--r--addons/mail/static/src/components/dialog_manager/dialog_manager.xml2
-rw-r--r--addons/mail/static/src/components/message_seen_indicator/message_seen_indicator_tests.js2
-rw-r--r--addons/mail/static/src/env/test_env.js3
-rw-r--r--addons/mail/static/src/js/main.js2
-rw-r--r--addons/mail/static/src/model/model_field.js527
-rw-r--r--addons/mail/static/src/model/model_manager.js214
-rw-r--r--addons/mail/static/src/models/attachment/attachment.js2
-rw-r--r--addons/mail/static/src/models/chat_window_manager/chat_window_manager.js6
-rw-r--r--addons/mail/static/src/models/chatter/chatter.js22
-rw-r--r--addons/mail/static/src/models/dialog/dialog.js22
-rw-r--r--addons/mail/static/src/models/dialog_manager/dialog_manager.js59
-rw-r--r--addons/mail/static/src/models/discuss/discuss.js2
-rw-r--r--addons/mail/static/src/models/follower/follower.js2
-rw-r--r--addons/mail/static/src/models/message_seen_indicator/message_seen_indicator.js4
-rw-r--r--addons/mail/static/src/models/messaging/messaging.js18
-rw-r--r--addons/mail/static/src/models/model/model.js6
-rw-r--r--addons/mail/static/src/models/thread/thread.js18
-rw-r--r--addons/mail/static/src/models/thread_cache/thread_cache.js8
-rw-r--r--addons/mail/static/src/models/thread_viewer/thread_viewer.js4
-rw-r--r--addons/mail/static/src/utils/test_utils.js4
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: "<p>User notification</p>",
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 @@
<t t-name="mail.DialogManager" owl="1">
<div class="o_DialogManager">
- <t t-foreach="env.messaging.dialogManager.allOrdered" t-as="dialog" t-key="dialog.localId">
+ <t t-foreach="env.messaging.dialogManager.dialogs" t-as="dialog" t-key="dialog.localId">
<Dialog
class="o_DialogManager_dialog"
dialogLocalId="dialog.localId"
diff --git a/addons/mail/static/src/components/message_seen_indicator/message_seen_indicator_tests.js b/addons/mail/static/src/components/message_seen_indicator/message_seen_indicator_tests.js
index 3f95a1d5407..b61d6721c0e 100644
--- a/addons/mail/static/src/components/message_seen_indicator/message_seen_indicator_tests.js
+++ b/addons/mail/static/src/components/message_seen_indicator/message_seen_indicator_tests.js
@@ -175,7 +175,7 @@ QUnit.test('rendering when just one has seen the message', async function (asser
author: [['insert', { id: this.env.session.partner_id, display_name: "Demo User" }]],
body: "<p>Test</p>",
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,29 +13,22 @@ 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
* for this field. This compute method is called on creation of record
@@ -63,6 +56,10 @@ class ModelField {
*/
this.dependents = dependents;
/**
+ * The messaging env.
+ */
+ this.env = env;
+ /**
* Name of the field in the definition of fields on model.
*/
this.fieldName = fieldName;
@@ -131,10 +128,6 @@ class ModelField {
*/
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
* following format: '<relationName>.<relatedFieldName>', where
@@ -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,29 +236,19 @@ 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);
- 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);
+ if (this._setRelationUnlink(record, currentValue)) {
+ hasChanged = true;
}
- }
- } 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);
+ break;
}
}
+ return hasChanged;
}
}
@@ -471,57 +414,22 @@ 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|<mail.model|string>[]} 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|<mail.model|string>[]} 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 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({
+ const toAddOtherRecords = this._setRelationConvertX2ManyValue(newValue);
+ const otherRecords = this.read(record);
+
+ 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;
- }
-
- // 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;
+ if (prevOtherRecord === toAddOtherRecord) {
+ return false;
}
-
+ // 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|<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;
+ for (const toDeleteOtherRecord of toDeleteOtherRecords) {
+ // unlink other record from current record
+ const wasDeleted = otherRecords.delete(toDeleteOtherRecord);
+ if (!wasDeleted) {
+ continue;
}
- }
- 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]],
- });
+ 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.autocreate) {
- continue;
+ if (field.fieldName in data) {
+ data2[field.fieldName] = data[field.fieldName];
+ } else {
+ data2[field.fieldName] = field.default;
}
- 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,
}));
@@ -881,6 +874,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.
*
* @private
@@ -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) {
@@ -13,21 +13,10 @@ function factory(dependencies) {
//----------------------------------------------------------------------
/**
- * @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]) });
- 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]],
+ const dialog = this.env.models['mail.dialog'].create({
+ manager: [['link', this]],
+ record: [['link', record]],
});
+ return 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