diff --git a/addons/mail/static/src/model/model_core.js b/addons/mail/static/src/model/model_core.js index d7de47fcbd7..86e1c8d57a9 100644 --- a/addons/mail/static/src/model/model_core.js +++ b/addons/mail/static/src/model/model_core.js @@ -136,7 +136,7 @@ function addOnChanges(modelName, onChanges) { if (!Object.prototype.hasOwnProperty.call(onChange, 'methodName')) { throw new Error("at least one onChange definition lacks a methodName (the name of the method to be called on change)."); } - if (!Array.isArray(onChange.dependencies) || onChange.dependencies.some(d => typeof d !== 'string')) { + if (!Array.isArray(onChange.dependencies)) { throw new Error("onChange dependencies must be an array of strings."); } if (typeof onChange.methodName !== 'string') { @@ -148,6 +148,16 @@ function addOnChanges(modelName, onChanges) { throw new Error(`unknown key "${key}" in onChange definition. Allowed keys: ${allowedKeys.join(", ")}.`); } } + // paths of dependencies are splitted now to avoid having to do it + // each time the path is followed. + const splittedDependencies = []; + for (const dependency of onChange.dependencies) { + if (typeof dependency !== 'string') { + throw new Error("onChange dependencies must be an array of strings."); + } + splittedDependencies.push(dependency.split('.')); + } + onChange.dependencies = splittedDependencies; } }, `Cannot add onChanges to model "${modelName}": `); registry.get(modelName).get('onChanges').push(...onChanges); diff --git a/addons/mail/static/src/model/model_field.js b/addons/mail/static/src/model/model_field.js index 7aef11a7572..4850ec2f05f 100644 --- a/addons/mail/static/src/model/model_field.js +++ b/addons/mail/static/src/model/model_field.js @@ -124,7 +124,15 @@ export class ModelField { * the first operation that is able to determine an order for the two * records. */ - this.sort = sort; + this.sort; + /** + * Sorted fields define sorting rules that can depend on related fields. + * The path to a related field is defined by a period-separated sequence + * of the relations to follow. To avoid wasting performances splitting + * the path each time a relation must be followed, it is spllited at + * field instantiation and stored here. + */ + this.sortedFieldSplittedPaths; /** * Determines whether and how elements of this field should be summed * into a counter field (only makes sense for relational x2many). @@ -150,16 +158,25 @@ export class ModelField { this.required = true; } } - /** - * Automatically make relateds readonly. - */ if (this.related) { + // Automatically make relateds readonly. this.readonly = true; } if (this.compute) { // Automatically make computes readonly. this.readonly = true; } + if (sort) { + this.sort = []; + // Only keep unique paths, as they will be used to create listeners, + // and we do not want to create useless duplicate listeners. + const relatedPathSet = new Set(); + for (const [compareRule, pathAsString] of sort) { + relatedPathSet.add(pathAsString); + this.sort.push([compareRule, pathAsString.split('.')]); + } + this.sortedFieldSplittedPaths = [...relatedPathSet].map(pathAsString => pathAsString.split('.')); + } } /** diff --git a/addons/mail/static/src/model/model_field_relation_set.js b/addons/mail/static/src/model/model_field_relation_set.js index 3009ab8f072..0e6caf9c699 100644 --- a/addons/mail/static/src/model/model_field_relation_set.js +++ b/addons/mail/static/src/model/model_field_relation_set.js @@ -57,8 +57,7 @@ export class RelationSet { // access all useful values of current record (and relations) to mark them as dependencies this.record.modelManager.startListening(listener); const compareDefinition = this.field.sort; - const relatedPathSet = new Set(compareDefinition.map(operation => operation[1])); // only keep unique paths to avoid unnecessary listeners - for (const relatedPath of relatedPathSet) { + for (const relatedPath of this.field.sortedFieldSplittedPaths) { followRelations(value, relatedPath); } this.record.modelManager.stopListening(listener); diff --git a/addons/mail/static/src/model/model_manager.js b/addons/mail/static/src/model/model_manager.js index 3b8ccfe841c..f69add7cdd0 100644 --- a/addons/mail/static/src/model/model_manager.js +++ b/addons/mail/static/src/model/model_manager.js @@ -526,6 +526,35 @@ export class ModelManager { } } + /** + * @private + * @throws {Error} + */ + _checkOnChangesOnModels() { + for (const model of Object.values(this.models)) { + for (const { dependencies, methodName } of registry.get(model.name).get('onChanges')) { + for (const dependency of dependencies) { + let currentModel = model; + let currentField; + for (const fieldName of dependency) { + if (!currentModel) { + throw new Error(`OnChange '${methodName}' defines a dependency with path '${dependency.join('.')}', but this dependency does not resolve: ${currentField} is not a relational field, therefore there is no relation to follow.`); + } + currentField = currentModel.__fieldMap.get(fieldName); + if (!currentField) { + throw new Error(`OnChange '${methodName}' defines a dependency with path '${dependency.join('.')}', but this path does not resolve: ${currentModel}/${fieldName} does not exist.`); + } + if (currentField.to) { + currentModel = this.models[currentField.to]; + } else { + currentModel = undefined; + } + } + } + } + } + } + /** * @private * @throws {Error} in case some fields are not correct. @@ -580,6 +609,20 @@ export class ModelManager { if (![model.name, 'Record'].includes(inverseField.to)) { throw new Error(`${field} has its inverse ${inverseField} referring to an invalid model (${inverseField.to}).`); } + if (field.sort) { + for (const path of field.sortedFieldSplittedPaths) { + let currentField = field; + for (const fieldName of path) { + if (!currentField.to) { + throw new Error(`Field ${field} defines a sort with path '${path.join('.')}', but this path does not resolve: ${currentField} is not a relational field, therefore there is no relation to follow.`); + } + if (!this.models[currentField.to].__fieldMap.has(fieldName)) { + throw new Error(`Field ${field} defines a sort with path '${path.join('.')}', but this path does not resolve: ${this.models[currentField.to]}/${fieldName} does not exist.`); + } + currentField = this.models[currentField.to].__fieldMap.get(fieldName); + } + } + } } for (const identifyingField of model.__identifyingFieldNames) { const field = model.__fieldMap.get(identifyingField); @@ -921,6 +964,7 @@ export class ModelManager { * should have matching reversed relation. */ this._checkProcessedFieldsOnModels(); + this._checkOnChangesOnModels(); } /** diff --git a/addons/mail/static/src/model/model_utils.js b/addons/mail/static/src/model/model_utils.js index d7f375d7862..8f075f15299 100644 --- a/addons/mail/static/src/model/model_utils.js +++ b/addons/mail/static/src/model/model_utils.js @@ -6,15 +6,12 @@ * is undefined. * * @param {Record} record - * @param {string} relatedPath field names, dot separated to follow relations + * @param {string[]} relatedPath Array of field names. * @returns {any} */ export function followRelations(record, relatedPath) { let target = record; - for (const field of relatedPath.split('.')) { - if (!target.constructor.__fieldMap.has(field)) { - throw Error(`field(${field}) does not exist on ${target}`); - } + for (const field of relatedPath) { target = target[field]; if (!target) { break;