[IMP] mail: split localId and record indexing

Part of task-2741982

closes odoo/odoo#97026

Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Sébastien Theys
2022-08-18 21:08:56 +02:00
parent edb4cb6f8c
commit 5b21f8b346
10 changed files with 290 additions and 216 deletions
@@ -64,11 +64,17 @@ export class ChatterContainer extends Component {
if (owl.status(this) === "destroyed") {
return;
}
const values = { id: this.chatterId, ...props, className: undefined };
const values = { id: this.chatterId, ...props };
delete values.className;
if (values.threadId === undefined) {
values.threadId = clear();
}
this.chatter = messaging.models['Chatter'].insert(values);
if (owl.status(this) === "destroyed") {
// insert might trigger a re-render which might destroy the current component
this.chatter.delete();
return;
}
/**
* Refresh the chatter when the parent view is (re)loaded.
* This serves mainly at loading initial data, but also on reload there
@@ -1,6 +1,5 @@
/** @odoo-module **/
import { useModels } from '@mail/component_hooks/use_models';
// ensure components are registered beforehand.
import '@mail/components/discuss_public_view/discuss_public_view';
import { getMessagingComponent } from "@mail/utils/messaging_component";
@@ -13,16 +12,17 @@ export class DiscussPublicViewContainer extends Component {
* @override
*/
setup() {
useModels();
super.setup();
}
get discussPublicView() {
return this.messaging && this.messaging.models['DiscussPublicView'].findFromIdentifyingData(this.messaging);
}
get messaging() {
return this.env.services.messaging.modelManager.messaging;
this.env.services.messaging.get().then(messaging => {
messaging.models['Thread'].insert(messaging.models['Thread'].convertData(this.props.data.channelData));
this.discussPublicView = messaging.models['DiscussPublicView'].insert(this.props.data.discussPublicViewData);
if (this.discussPublicView.shouldDisplayWelcomeViewInitially) {
this.discussPublicView.switchToWelcomeView();
} else {
this.discussPublicView.switchToThreadView();
}
this.render();
});
}
}
@@ -30,4 +30,7 @@ export class DiscussPublicViewContainer extends Component {
Object.assign(DiscussPublicViewContainer, {
components: { DiscussPublicView: getMessagingComponent('DiscussPublicView') },
template: 'mail.DiscussPublicViewContainer',
props: {
data: Object,
},
});
+12 -2
View File
@@ -850,9 +850,19 @@ export class ModelField {
throw Error(`${record} is not a record. Did you try to use link() instead of insert() with data?`);
}
const otherModel = record.modelManager.models[this.to];
if (!otherModel.modelManager.get(otherModel, record.localId, { isCheckingInheritance: true })) {
throw Error(`Record ${record.localId} is not valid for relational field ${this.fieldName}.`);
if (otherModel.__records.has(record)) {
return;
}
// support for inherited models (eg. relation targeting `Record`)
for (const subModel of Object.values(this.models)) {
if (!(subModel.prototype instanceof otherModel)) {
continue;
}
if (subModel.__records.has(record)) {
return;
}
}
throw Error(`Record ${record} is not valid for relational field ${this.fieldName}.`);
}
}
@@ -0,0 +1,70 @@
/** @odoo-module **/
export class ModelIndexAnd {
constructor(model) {
this.model = model;
this.recordsByValuesTree = new Map();
this.valuesByRecords = new Map();
this.singleton = undefined;
}
addRecord(record, data) {
if (this.model.__identifyingFieldNames.size === 0) {
this.singleton = record;
return;
}
const valuesOfRecord = [];
let res = this.recordsByValuesTree;
const { length, [length - 1]: lastFieldName } = [...this.model.__identifyingFieldNames];
for (const fieldName of this.model.__identifyingFieldNames) {
const fieldValue = data[fieldName];
if (fieldValue === undefined) {
throw new Error(`Identifying field "${fieldName}" is lacking a value on ${this.model} with 'and' identifying mode`);
}
valuesOfRecord.push(fieldValue);
if (!res.has(fieldValue)) {
res.set(fieldValue, fieldName === lastFieldName ? record : new Map());
}
res = res.get(fieldValue);
}
this.valuesByRecords.set(record, valuesOfRecord);
}
findRecord(data) {
if (this.model.__identifyingFieldNames.size === 0) {
return this.singleton;
}
let res = this.recordsByValuesTree;
for (const fieldName of this.model.__identifyingFieldNames) {
const fieldValue = data[fieldName];
if (fieldValue === undefined) {
throw new Error(`Identifying field "${fieldName}" is lacking a value on ${this.model} with 'and' identifying mode`);
}
res = res.get(fieldValue);
if (!res) {
return;
}
}
return res;
}
removeRecord(record) {
if (this.model.__identifyingFieldNames.size === 0) {
this.singleton = undefined;
return;
}
const values = this.valuesByRecords.get(record);
let res = this.recordsByValuesTree;
for (let i = 0; i < values.length; i++) {
const index = values[i];
if (i === values.length - 1) {
res.delete(index);
break;
}
res = res.get(index);
}
this.valuesByRecords.delete(record);
}
}
@@ -0,0 +1,52 @@
/** @odoo-module **/
export class ModelIndexXor {
constructor(model) {
this.model = model;
this.recordsByValuesByFields = new Map();
this.fieldNameAndValueByRecords = new Map();
}
addRecord(record, data) {
const [fieldName, fieldValue] = [...this.model.__identifyingFieldNames].reduce(([fieldName, fieldValue], currentFieldName) => {
const currentFieldValue = data[currentFieldName];
if (currentFieldValue === undefined) {
return [fieldName, fieldValue];
}
if (fieldName) {
throw new Error(`Identifying field on ${this.model} with 'xor' identifying mode should have only one of the conditional values given in data. Currently have both "${fieldName}" and "${currentFieldName}".`);
}
return [currentFieldName, currentFieldValue];
}, [undefined, undefined]);
if (!this.recordsByValuesByFields.has(fieldName)) {
this.recordsByValuesByFields.set(fieldName, new Map());
}
this.recordsByValuesByFields.get(fieldName).set(fieldValue, record);
this.fieldNameAndValueByRecords.set(record, [fieldName, fieldValue]);
}
findRecord(data) {
const [fieldName, fieldValue] = [...this.model.__identifyingFieldNames].reduce(([fieldName, fieldValue], currentFieldName) => {
const currentFieldValue = data[currentFieldName];
if (currentFieldValue === undefined) {
return [fieldName, fieldValue];
}
if (fieldName) {
throw new Error(`Identifying field on ${this.model} with 'xor' identifying mode should have only one of the conditional values given in data. Currently have both "${fieldName}" and "${currentFieldName}".`);
}
return [currentFieldName, currentFieldValue];
}, [undefined, undefined]);
if (!this.recordsByValuesByFields.has(fieldName)) {
this.recordsByValuesByFields.set(fieldName, new Map());
}
return this.recordsByValuesByFields.get(fieldName).get(fieldValue);
}
removeRecord(record) {
const [fieldName, fieldValue] = this.fieldNameAndValueByRecords.get(record);
this.recordsByValuesByFields.get(fieldName).delete(fieldValue);
this.fieldNameAndValueByRecords.delete(record);
}
}
@@ -31,29 +31,29 @@ export class Listener {
this.name = name;
this.onChange = onChange;
/**
* Set of localIds that have been accessed on model manager between the
* Set of records that have been accessed on model manager between the
* last call to `startListening` and `stopListening` with this listener
* as parameter.
* Each localId has its own way to know the listeners that are observing
* Each record has its own way to know the listeners that are observing
* it (to be able to notify them if it changes). This set holds the
* inverse of that information, which is useful to be able to remove
* this listener (when the need arises) from those localIds without
* this listener (when the need arises) from those records without
* having to verify the presence of this listener on each possible
* localId one by one.
* record one by one.
*/
this.lastObservedLocalIds = new Set();
this.lastObservedRecords = new Set();
/**
* Map between localIds and a set of fields on those localIds that have
* Map between records and a set of fields on those records that have
* been accessed on model manager between the last call to
* `startListening` and `stopListening` with this listener as parameter.
* Each field of each localId has its own way to know the listeners that
* Each field of each record has its own way to know the listeners that
* are observing it (to be able to notify them if it changes). This map
* holds the inverse of that information, which is useful to be able to
* remove this listener (when the need arises) from those fields without
* having to verify the presence of this listener on each possible field
* one by one.
*/
this.lastObservedFieldsByLocalId = new Map();
this.lastObservedFieldsByRecord = new Map();
/**
* Set of model that have been accessed with `all()` on model manager
* between the last call to `startListening` and `stopListening` with
+123 -183
View File
@@ -2,12 +2,13 @@
import { registry } from '@mail/model/model_core';
import { ModelField } from '@mail/model/model_field';
import { ModelIndexAnd } from '@mail/model/model_index_and';
import { ModelIndexXor } from '@mail/model/model_index_xor';
import { FieldCommand, unlinkAll } from '@mail/model/model_field_command';
import { RelationSet } from '@mail/model/model_field_relation_set';
import { Listener } from '@mail/model/model_listener';
import { followRelations } from '@mail/model/model_utils';
import { makeDeferred } from '@mail/utils/deferred';
/**
* Object that manage models and records, notably their update cycle: whenever
* some records are requested for update (either with model static method
@@ -71,16 +72,14 @@ export class ModelManager {
*/
this._listenersObservingAllByModel = new Map();
/**
* Map between localId and a set of listeners that are using it. The
* following fields use localId instead of record reference because the
* listener might try to use a record that doesn't exist yet.
* Map between record and a set of listeners that are using it.
*/
this._listenersObservingLocalId = new Map();
this._listenersObservingRecord = new Map();
/**
* Map between fields of localId and a set of listeners that are
* Map between fields of record and a set of listeners that are
* using it.
*/
this._listenersObservingFieldOfLocalId = new Map();
this._listenersObservingFieldOfRecord = new Map();
/**
* Map of listeners that should be notified at the end of the current
* update cycle. Value contains list of info to help for debug.
@@ -169,7 +168,7 @@ export class ModelManager {
entry.set(listener, [info]);
}
}
const allRecords = Object.values(model.__records);
const allRecords = [...model.__records];
if (filterFunc) {
return allRecords.filter(filterFunc);
}
@@ -213,7 +212,7 @@ export class ModelManager {
* @returns {boolean}
*/
exists(model, record) {
return Boolean(record.localId);
return model.__records.has(record);
}
/**
@@ -221,45 +220,30 @@ export class ModelManager {
* identifying data, if it exists.
*
* @param {Object} model
* @param {Object} data
* @param {Object} [data={}]
* @returns {Record|undefined}
*/
findFromIdentifyingData(model, data) {
return this.get(model, this._getRecordIndex(model, data));
}
/**
* This method returns the record of provided model that matches provided
* local id. Useful to convert a local id to a record.
* Note that even if there's a record in the system having provided local
* id, if the resulting record is not an instance of this model, this getter
* assumes the record does not exist.
*
* @param {Object} model
* @param {string} localId
* @param {Object} param2
* @param {boolean} [param2.isCheckingInheritance=false]
* @returns {Record|undefined} record, if exists
*/
get(model, localId, { isCheckingInheritance = false } = {}) {
if (!localId) {
findFromIdentifyingData(model, data = {}) {
const data2 = { ...data };
for (const fieldName of model.__identifyingFieldNames) {
const field = model.__fieldMap[fieldName];
if (data2[field.fieldName] === undefined && field.default !== undefined) {
data2[field.fieldName] = field.default;
}
}
const record = model.__recordsIndex.findRecord(this._preInsertIdentifyingFieldsFromData(model, data2));
if (!record) {
return;
}
if (!isCheckingInheritance && this.isDebug) {
const modelName = localId.split('(')[0];
if (modelName !== model.name) {
throw Error(`wrong format of localId ${localId} for ${model}.`);
}
}
for (const listener of this._listeners) {
listener.lastObservedLocalIds.add(localId);
if (!this._listenersObservingLocalId.has(localId)) {
this._listenersObservingLocalId.set(localId, new Map());
listener.lastObservedRecords.add(record);
if (!this._listenersObservingRecord.has(record)) {
this._listenersObservingRecord.set(record, new Map());
}
const entry = this._listenersObservingLocalId.get(localId);
const entry = this._listenersObservingRecord.get(record);
const info = {
listener,
reason: `get record - ${localId}`,
reason: `findFromIdentifyingData record - ${record}`,
};
if (entry.has(listener)) {
entry.get(listener).push(info);
@@ -267,24 +251,7 @@ export class ModelManager {
entry.set(listener, [info]);
}
}
const record = model.__records[localId];
if (record) {
return record;
}
if (!isCheckingInheritance) {
return;
}
// support for inherited models (eg. relation targeting `Record`)
for (const subModel of Object.values(this.models)) {
if (!(subModel.prototype instanceof model)) {
continue;
}
const record = subModel.__records[localId];
if (record) {
return record;
}
}
return;
return record;
}
/**
@@ -342,18 +309,18 @@ export class ModelManager {
this._listeners.delete(listener);
this._listenersToNotifyInUpdateCycle.delete(listener);
this._listenersToNotifyAfterUpdateCycle.delete(listener);
for (const localId of listener.lastObservedLocalIds) {
this._listenersObservingLocalId.get(localId).delete(listener);
const listenersObservingFieldOfLocalId = this._listenersObservingFieldOfLocalId.get(localId);
for (const field of listener.lastObservedFieldsByLocalId.get(localId) || []) {
listenersObservingFieldOfLocalId.get(field).delete(listener);
for (const record of listener.lastObservedRecords) {
this._listenersObservingRecord.get(record).delete(listener);
const listenersObservingFieldOfRecord = this._listenersObservingFieldOfRecord.get(record);
for (const field of listener.lastObservedFieldsByRecord.get(record) || []) {
listenersObservingFieldOfRecord.get(field).delete(listener);
}
}
for (const model of listener.lastObservedAllByModel) {
this._listenersObservingAllByModel.get(model).delete(listener);
}
listener.lastObservedLocalIds.clear();
listener.lastObservedFieldsByLocalId.clear();
listener.lastObservedRecords.clear();
listener.lastObservedFieldsByRecord.clear();
listener.lastObservedAllByModel.clear();
}
@@ -436,9 +403,17 @@ export class ModelManager {
// Make model manager accessible from model.
model.modelManager = this;
model.fields = {};
// Contains all records. key is local id, while value is the record.
model.__records = {};
model.identifyingMode = definition.get('identifyingMode');
model.__records = new Set();
model.__recordCount = 0;
model.__recordsIndex = (() => {
switch (model.identifyingMode) {
case 'and':
return new ModelIndexAnd(model);
case 'xor':
return new ModelIndexXor(model);
}
})();
this._listenersObservingAllByModel.set(model, new Map());
this.models[model.name] = model;
}
@@ -650,10 +625,10 @@ export class ModelManager {
/**
* @private
* @param {Object} model
* @param {string} localId
* @returns {Record}
*/
_create(model, localId) {
_create(model) {
const localId = `${model.name}_${++model.__recordCount}`;
/**
* Prepare record state. Assign various keys and values that are
* expected to be found on every record.
@@ -690,17 +665,17 @@ export class ModelManager {
}
}
}
if (!this._listenersObservingLocalId.has(localId)) {
this._listenersObservingLocalId.set(localId, new Map());
if (!this._listenersObservingRecord.has(record)) {
this._listenersObservingRecord.set(record, new Map());
}
this._listenersObservingFieldOfLocalId.set(localId, new Map());
this._listenersObservingFieldOfRecord.set(record, new Map());
for (const field of model.__fieldList) {
this._listenersObservingFieldOfLocalId.get(localId).set(field, new Map());
this._listenersObservingFieldOfRecord.get(record).set(field, new Map());
}
/**
* Register record.
*/
model.__records[record.localId] = record;
model.__records.add(record);
/**
* Auto-bind record methods so that `this` always refer to the record.
*/
@@ -722,10 +697,10 @@ export class ModelManager {
infoList,
});
}
for (const [listener, infoList] of this._listenersObservingLocalId.get(localId)) {
for (const [listener, infoList] of this._listenersObservingRecord.get(record)) {
this._markListenerToNotify(listener, {
listener,
reason: `_create: localId - ${record}`,
reason: `_create: record - ${record}`,
infoList,
});
}
@@ -740,7 +715,7 @@ export class ModelManager {
this._ensureNoLockingListener();
const model = record.constructor;
if (!record.exists()) {
throw Error(`Cannot delete already deleted record ${record.localId}.`);
throw Error(`Cannot delete already deleted record ${record}.`);
}
const lifecycleHooks = registry.get(model.name).get('lifecycleHooks');
if (lifecycleHooks.has('_willDelete')) {
@@ -758,14 +733,15 @@ export class ModelManager {
}
}
}
model.__recordsIndex.removeRecord(record);
this._createdRecordsComputes.delete(record);
this._createdRecordsCreated.delete(record);
this._createdRecordsOnChange.delete(record);
this._updatedRecordsCheckRequired.delete(record);
for (const [listener, infoList] of this._listenersObservingLocalId.get(record.localId)) {
for (const [listener, infoList] of this._listenersObservingRecord.get(record)) {
this._markListenerToNotify(listener, {
listener,
reason: `_delete: localId - ${record}`,
reason: `_delete: record - ${record}`,
infoList,
});
}
@@ -778,7 +754,7 @@ export class ModelManager {
}
delete record.__values;
delete record.__listeners;
delete model.__records[record.localId];
model.__records.delete(record);
delete record.localId;
}
@@ -966,91 +942,6 @@ export class ModelManager {
this._checkProcessedFieldsOnModels();
}
/**
* Returns an index on the given model for the given data.
*
* @param {Object} model
* @param {Object|Record} data insert data or record
* @returns {string}
*/
_getRecordIndex(model, data) {
const parts = [];
switch (model.identifyingMode) {
case 'and':
for (const fieldName of model.__identifyingFieldNames) {
const fieldValue = this._getValueFromDataOrFromDefault(model, fieldName, data);
if (fieldValue === undefined) {
throw new Error(`Identifying field "${fieldName}" is lacking a value on ${model} with 'and' identifying mode`);
}
parts.push(this._getRecordIndexFromField(model, fieldName, fieldValue));
}
break;
case 'xor': {
const [fieldName, fieldValue] = [...model.__identifyingFieldNames].reduce(([fieldName, fieldValue], currentFieldName) => {
const currentFieldValue = this._getValueFromDataOrFromDefault(model, currentFieldName, data);
if (currentFieldValue === undefined) {
return [fieldName, fieldValue];
}
if (fieldName) {
throw new Error(`Identifying field on ${model} with 'xor' identifying mode should have only one of the conditional values given in data. Currently have both "${fieldName}" and "${currentFieldName}".`);
}
return [currentFieldName, currentFieldValue];
}, [undefined, undefined]);
parts.push(this._getRecordIndexFromField(model, fieldName, fieldValue));
break;
}
}
return `${model.name}(${parts.join(', ')})`;
}
/**
* Returns the part of the record index that comes from the given fieldName
* with the given fieldValue.
*
* @param {Object} model
* @param {string} fieldName
* @param {any} fieldValue
* @returns {string}
*/
_getRecordIndexFromField(model, fieldName, fieldValue) {
const relationTo = model.__fieldMap[fieldName].to;
if (!relationTo) {
return `${fieldName}: ${fieldValue}`;
}
const OtherModel = this.models[relationTo];
if (fieldValue instanceof OtherModel) {
return `${fieldName}: ${this._getRecordIndex(OtherModel, fieldValue)}`;
}
const fieldValue2 = model.__fieldMap[fieldName].convertToFieldCommandList(fieldValue)[0];
if (!(fieldValue2 instanceof FieldCommand)) {
throw new Error(`Identifying element "${model}/${fieldName}" is expecting a command for relational field.`);
}
if (!['link', 'insert', 'replace', 'insert-and-replace'].includes(fieldValue2._name)) {
throw new Error(`Identifying element "${model}/${fieldName}" is expecting a command of type "insert-and-replace", "replace", "insert" or "link". "${fieldValue2._name}" was given instead.`);
}
const relationValue = fieldValue2._value;
if (!relationValue) {
throw new Error(`Identifying element "${model}/${fieldName}" is lacking a relation value.`);
}
return `${fieldName}: ${this._getRecordIndex(OtherModel, relationValue)}`;
}
/**
* Returns the value for the given fieldName from the provided data if it is
* defined or from default value of this field otherwise.
*
* @param {Object} model
* @param {string} fieldName
* @param {Object} data
* @returns {any}
*/
_getValueFromDataOrFromDefault(model, fieldName, data) {
if (data[fieldName] !== undefined) {
return data[fieldName];
}
return model.__fieldMap[fieldName].default;
}
/**
* @private
* @param {Object} model
@@ -1062,11 +953,12 @@ export class ModelManager {
this._ensureNoLockingListener();
const records = [];
for (const data of dataList) {
const localId = this._getRecordIndex(model, data);
let record = this.get(model, localId);
let record = this.findFromIdentifyingData(model, data);
if (!record) {
record = this._create(model, localId);
this._update(record, this._addDefaultData(model, data), { ...options, allowWriteReadonly: true });
const data2 = this._preInsertIdentifyingFieldsFromData(model, this._addDefaultData(model, data));
record = this._create(model);
model.__recordsIndex.addRecord(record, data2);
this._update(record, data2, { ...options, allowWriteReadonly: true });
} else {
this._update(record, data, options);
}
@@ -1134,7 +1026,7 @@ export class ModelManager {
* @param {ModelField} field
*/
_markRecordFieldAsChanged(record, field) {
for (const [listener, infoList] of this._listenersObservingFieldOfLocalId.get(record.localId).get(field)) {
for (const [listener, infoList] of this._listenersObservingFieldOfRecord.get(record).get(field)) {
this._markListenerToNotify(listener, {
listener,
reason: `_update: ${field} of ${record}`,
@@ -1183,6 +1075,54 @@ export class ModelManager {
}
}
/**
* Processes all commands given in data that concerns relation fields that
* are identifying to execute their respective "insert-and-replace" commands
* and to replace them by corresponding "replace" commands.
*
* @param {Object} model
* @param {Object} [data={}]
* @returns {Object}
*/
_preInsertIdentifyingFieldsFromData(model, data) {
const data2 = { ...data };
for (const [fieldName, value] of Object.entries(data2)) {
const field = model.__fieldMap[fieldName];
if (!field) {
throw new Error(`"${fieldName}" is not a field on "${model}".`);
}
if (!field.to) {
continue;
}
if (!field.identifying) {
continue;
}
const commands = field.convertToFieldCommandList(value);
if (commands.length !== 1) {
throw new Error(`Identifying field "${model}/${fieldName}" should receive a single command.`);
}
const [command] = commands;
if (!(command instanceof FieldCommand)) {
throw new Error(`Identifying field "${model}/${fieldName}" should receive a command.`);
}
if (!['insert-and-replace', 'replace'].includes(command._name)) {
throw new Error(`Identifying field "${model}/${fieldName}" should receive a "replace" or "insert-and-replace" command.`);
}
if (command._name === 'replace') {
continue;
}
if (!command._value) {
throw new Error(`Identifying field "${model}/${fieldName}" is lacking a relation value.`);
}
if (typeof command._value[Symbol.iterator] === 'function') {
throw new Error(`Identifying field "${model}/${fieldName}" should receive a single data object.`);
}
const [record] = this._insert(this.models[field.to], [command._value]);
data2[fieldName] = record;
}
return data2;
}
/**
* This function processes definition of declared fields in provided models.
* Basically, models have fields declared in static prop `fields`, and this
@@ -1280,24 +1220,24 @@ export class ModelManager {
get: function getFieldValue() { // this is bound to record
const field = model.__fieldMap[fieldName];
if (this.modelManager._listeners.size) {
if (!this.modelManager._listenersObservingLocalId.has(this.localId)) {
this.modelManager._listenersObservingLocalId.set(this.localId, new Map());
if (!this.modelManager._listenersObservingRecord.has(this)) {
this.modelManager._listenersObservingRecord.set(this, new Map());
}
const entryLocalId = this.modelManager._listenersObservingLocalId.get(this.localId);
const entryRecord = this.modelManager._listenersObservingRecord.get(this);
const reason = `getField - ${field} of ${this}`;
const entryField = this.modelManager._listenersObservingFieldOfLocalId.get(this.localId).get(field);
const entryField = this.modelManager._listenersObservingFieldOfRecord.get(this).get(field);
for (const listener of this.modelManager._listeners) {
listener.lastObservedLocalIds.add(this.localId);
listener.lastObservedRecords.add(this);
const info = { listener, reason };
if (entryLocalId.has(listener)) {
entryLocalId.get(listener).push(info);
if (entryRecord.has(listener)) {
entryRecord.get(listener).push(info);
} else {
entryLocalId.set(listener, [info]);
entryRecord.set(listener, [info]);
}
if (!listener.lastObservedFieldsByLocalId.has(this.localId)) {
listener.lastObservedFieldsByLocalId.set(this.localId, new Set());
if (!listener.lastObservedFieldsByRecord.has(this)) {
listener.lastObservedFieldsByRecord.set(this, new Set());
}
listener.lastObservedFieldsByLocalId.get(this.localId).add(field);
listener.lastObservedFieldsByRecord.get(this).add(field);
if (entryField.has(listener)) {
entryField.get(listener).push(info);
} else {
@@ -1324,7 +1264,7 @@ export class ModelManager {
_update(record, data, options = {}) {
this._ensureNoLockingListener();
if (!record.exists()) {
throw Error(`Cannot update already deleted record ${record.localId}.`);
throw Error(`Cannot update already deleted record ${record}.`);
}
const { allowWriteReadonly = false } = options;
const model = record.constructor;
@@ -1349,7 +1289,7 @@ export class ModelManager {
}
const field = model.__fieldMap[fieldName];
if (!field) {
throw new Error(`Cannot create/update record with data unrelated to a field. (record: "${record.localId}", non-field attempted update: "${fieldName}")`);
throw new Error(`Cannot create/update record with data unrelated to a field. (record: "${record}", non-field attempted update: "${fieldName}")`);
}
const newVal = data[fieldName];
if (!field.parseAndExecuteCommands(record, newVal, options)) {
@@ -198,6 +198,8 @@ registerModel({
* Will be used to update the current guest's name when joining the
* channel by clicking on the 'joinButton'.
*/
pendingGuestName: attr(),
pendingGuestName: attr({
default: '',
}),
},
});
@@ -53,7 +53,7 @@ Component.env = legacyEnv;
registry.category('wowlToLegacyServiceMappers').add('make_messaging_to_legacy_env', messagingToLegacyEnv);
const mainComponentsRegistry = registry.category('main_components');
mainComponentsRegistry.add('DiscussPublicViewContainer', { Component: DiscussPublicViewContainer });
mainComponentsRegistry.add('DiscussPublicViewContainer', { Component: DiscussPublicViewContainer, props: { data } });
// needed by the attachment viewer
mainComponentsRegistry.add('DialogManagerContainer', { Component: DialogManagerContainer });
mainComponentsRegistry.add('PopoverManagerContainer', { Component: PopoverManagerContainer });
@@ -76,13 +76,4 @@ Component.env = legacyEnv;
mapLegacyEnvToWowlEnv(Component.env, env);
odoo.isReady = true;
await mount(MainComponentsContainer, document.body, { env, templates, dev: env.debug });
const messaging = await env.services.messaging.get();
messaging.models['Thread'].insert(messaging.models['Thread'].convertData(data.channelData));
const discussPublicView = messaging.models['DiscussPublicView'].insert(data.discussPublicViewData);
if (discussPublicView.shouldDisplayWelcomeViewInitially) {
discussPublicView.switchToWelcomeView();
} else {
discussPublicView.switchToThreadView();
}
})();
@@ -2193,7 +2193,7 @@ QUnit.test('sidebar quick search', async function (assert) {
.o_DiscussSidebar_categoryChannel .o_DiscussSidebarCategory_item
`).dataset.threadLocalId,
messaging.models['Thread'].findFromIdentifyingData({
id: pyEnv['mail.channel'].search([['name', '=', 'channel12']]),
id: pyEnv['mail.channel'].search([['name', '=', 'channel12']])[0],
model: 'mail.channel',
}).localId,
"should have filtered to a single channel (channel12)"