[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>
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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>",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
+1
-1
@@ -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(
|
||||
|
||||
+1
-2
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ Object.assign(env, {
|
||||
loadingBaseDelayDuration: 400,
|
||||
messaging: undefined,
|
||||
messagingBus: new EventBus(),
|
||||
modelManager: new ModelManager(),
|
||||
modelManager: new ModelManager(env),
|
||||
store,
|
||||
});
|
||||
|
||||
|
||||
@@ -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|<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 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|<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}.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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]];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -462,7 +462,7 @@ function factory(dependencies) {
|
||||
related: 'thread.model',
|
||||
}),
|
||||
threadViewer: one2one('mail.thread_viewer', {
|
||||
autocreate: true,
|
||||
default: [['create']],
|
||||
isCausal: true,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -111,7 +111,7 @@ function factory(dependencies) {
|
||||
}
|
||||
}
|
||||
this._subtypesListDialog = this.env.messaging.dialogManager.open('mail.follower_subtype_list', {
|
||||
follower: [['replace', this]],
|
||||
follower: [['link', this]],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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']];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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]];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)]];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user