diff --git a/addons/web/static/src/views/graph/graph_model.js b/addons/web/static/src/views/graph/graph_model.js index 909fce47610..a784104520d 100644 --- a/addons/web/static/src/views/graph/graph_model.js +++ b/addons/web/static/src/views/graph/graph_model.js @@ -1,7 +1,7 @@ /** @odoo-module **/ import { sortBy } from "@web/core/utils/arrays"; -import { KeepLast } from "@web/core/utils/concurrency"; +import { KeepLast, Race } from "@web/core/utils/concurrency"; import { rankInterval } from "@web/search/utils/dates"; import { getGroupBy } from "@web/search/utils/group_by"; import { GROUPABLE_TYPES } from "@web/search/utils/misc"; @@ -85,7 +85,13 @@ export class GraphModel extends Model { * @override */ setup(params) { + // concurrency management this.keepLast = new KeepLast(); + this.race = new Race(); + const _fetchDataPoints = this._fetchDataPoints.bind(this); + this._fetchDataPoints = (...args) => { + return this.race.add(_fetchDataPoints(...args)); + }; this.initialGroupBy = null; @@ -126,9 +132,7 @@ export class GraphModel extends Model { metaData.additionalMeasures ); - await this._fetchDataPoints(metaData); - - this._prepareData(); + return this._fetchDataPoints(metaData); } /** @@ -144,17 +148,13 @@ export class GraphModel extends Model { * @param {Object} params */ async updateMetaData(params) { - const metaData = Object.assign({}, this.metaData, params); - if ("measure" in params) { - await this._fetchDataPoints(metaData); + await this._fetchDataPoints(Object.assign({}, this.metaData, params)); } else { - await this.keepLast.add(Promise.resolve()); - this.metaData = metaData; + await this.race.getCurrentProm(); + this.metaData = Object.assign({}, this.metaData, params); + this._prepareData(); } - - this._prepareData(); - this.notify(); } @@ -171,6 +171,7 @@ export class GraphModel extends Model { async _fetchDataPoints(metaData) { this.dataPoints = await this.keepLast.add(this._loadDataPoints(metaData)); this.metaData = metaData; + this._prepareData(); } /** diff --git a/addons/web/static/src/views/pivot/pivot_model.js b/addons/web/static/src/views/pivot/pivot_model.js index 68a4ef04b6a..fe30557458b 100644 --- a/addons/web/static/src/views/pivot/pivot_model.js +++ b/addons/web/static/src/views/pivot/pivot_model.js @@ -2,7 +2,7 @@ import { Domain } from "@web/core/domain"; import { cartesian, sections, sortBy, symmetricalDifference } from "@web/core/utils/arrays"; -import { KeepLast } from "@web/core/utils/concurrency"; +import { KeepLast, Race } from "@web/core/utils/concurrency"; import { computeVariation } from "@web/core/utils/numbers"; import { DEFAULT_INTERVAL } from "@web/search/utils/dates"; import { Model } from "@web/views/helpers/model"; @@ -338,7 +338,13 @@ export class PivotModel extends Model { * @param {Object} [params.data] previously exported data */ setup(params) { + // concurrency management this.keepLast = new KeepLast(); + this.race = new Race(); + const _loadData = this._loadData.bind(this); + this._loadData = (...args) => { + return this.race.add(_loadData(...args)); + }; let sortedColumn = params.metaData.sortedColumn || null; if (!sortedColumn && params.metaData.defaultOrder) { @@ -373,9 +379,6 @@ export class PivotModel extends Model { this.metaData = this._buildMetaData(metaData); this.reload = false; // used to discriminate between the first load and subsequent reloads - // loadProm ensures that loadData always returns a promise that resolves when - // the final data is loaded (in case several load requests arrive consecutively) - this.loadProm = null; this.nextActiveMeasures = null; // allows to toggle several measures consecutively } @@ -394,7 +397,7 @@ export class PivotModel extends Model { * @param {string} [params.interval] */ async addGroupBy(params) { - if (this.loadProm) { + if (this.race.getCurrentProm()) { return; // we are currently reloaded the table } @@ -435,7 +438,7 @@ export class PivotModel extends Model { * @param {'row'|'col'} type */ closeGroup(groupId, type) { - if (this.loadProm) { + if (this.race.getCurrentProm()) { return; // we are currently reloading the table } @@ -507,7 +510,7 @@ export class PivotModel extends Model { * @param {'row'|'col'} type */ async expandGroup(groupId, type) { - if (this.loadProm) { + if (this.race.getCurrentProm()) { return; // we are currently reloaded the table } @@ -598,7 +601,7 @@ export class PivotModel extends Model { * flipping the axes. This method is thus async. */ async flip() { - await Promise.resolve(this.loadProm); + await this.race.getCurrentProm(); // swap the data: the main column and the main row let temp = this.data.rowGroupTree; @@ -719,7 +722,7 @@ export class PivotModel extends Model { * @param {number[]} sortedColumn.groupId */ sortRows(sortedColumn) { - if (this.loadProm) { + if (this.race.getCurrentProm()) { return; // we are currently reloaded the table } @@ -745,7 +748,7 @@ export class PivotModel extends Model { // actually reload a lesser amount of information (but still, we need // to wait in case there is a pending load) metaData.activeMeasures.splice(index, 1); - await Promise.resolve(this.loadProm); + await Promise.resolve(this.race.getCurrentProm()); this.metaData = metaData; } else { metaData.activeMeasures.push(fieldName); @@ -1347,15 +1350,6 @@ export class PivotModel extends Model { * @param {Config} config */ async _loadData(config, prune = true) { - if (!this.loadProm) { - this.loadProm = new Promise((resolve) => { - this.resolveLoadProm = () => { - this.loadProm = null; - resolve(); - }; - }); - } - config.data = {}; // data will be completely recomputed const { data, metaData } = config; data.rowGroupTree = { root: { labels: [], values: [] }, directSubTrees: new Map() }; @@ -1372,33 +1366,25 @@ export class PivotModel extends Model { const rightDivisors = sections(metaData.fullColGroupBys); const divisors = cartesian(leftDivisors, rightDivisors); - this._subdivideGroup(group, divisors.slice(0, 1), config) - .then(() => { - return this._subdivideGroup(group, divisors.slice(1), config); - }) - .then(async () => { - // keep folded groups folded after the reload if the structure of the table is the same - if (prune && this._hasData(data) && this._hasData(this.data)) { - if ( - symmetricalDifference(metaData.rowGroupBys, this.metaData.rowGroupBys) - .length === 0 - ) { - this._pruneTree(data.rowGroupTree, this.data.rowGroupTree); - } - if ( - symmetricalDifference(metaData.colGroupBys, this.metaData.colGroupBys) - .length === 0 - ) { - this._pruneTree(data.colGroupTree, this.data.colGroupTree); - } - } + await this._subdivideGroup(group, divisors.slice(0, 1), config); + await this._subdivideGroup(group, divisors.slice(1), config); - this.data = config.data; - this.metaData = config.metaData; - this.resolveLoadProm(); - }); + // keep folded groups folded after the reload if the structure of the table is the same + if (prune && this._hasData(data) && this._hasData(this.data)) { + if ( + symmetricalDifference(metaData.rowGroupBys, this.metaData.rowGroupBys).length === 0 + ) { + this._pruneTree(data.rowGroupTree, this.data.rowGroupTree); + } + if ( + symmetricalDifference(metaData.colGroupBys, this.metaData.colGroupBys).length === 0 + ) { + this._pruneTree(data.colGroupTree, this.data.colGroupTree); + } + } - return this.loadProm; + this.data = config.data; + this.metaData = config.metaData; } /** * Extract the information in the read_group results (groupSubdivisions) diff --git a/addons/web/static/tests/views/graph_view_tests.js b/addons/web/static/tests/views/graph_view_tests.js index 619c99c4edd..0c853c97356 100644 --- a/addons/web/static/tests/views/graph_view_tests.js +++ b/addons/web/static/tests/views/graph_view_tests.js @@ -1,7 +1,7 @@ /** @odoo-module **/ import { BORDER_WHITE, DEFAULT_BG } from "@web/views/graph/colors"; -import { click, triggerEvent } from "@web/../tests/helpers/utils"; +import { click, makeDeferred, nextTick, triggerEvent } from "@web/../tests/helpers/utils"; import { createWebClient, doAction } from "@web/../tests/webclient/helpers"; import { dialogService } from "@web/core/dialog/dialog_service"; import { GraphArchParser } from "@web/views/graph/graph_arch_parser"; @@ -3308,4 +3308,118 @@ QUnit.module("Views", (hooks) => { ]); } ); + + QUnit.test( + "concurrent reloads: add a filter, and directly toggle a measure", + async function (assert) { + let def; + const graph = await makeView({ + serverData, + type: "graph", + resModel: "foo", + arch: ` + + + `, + searchViewArch: ` + + + `, + mockRPC: function (route, args) { + if (args.method === "web_read_group") { + return Promise.resolve(def); + } + }, + }); + + checkDatasets(assert, graph, ["data", "label"], { + data: [4, 4], + label: "Count", + }); + + // Set a domain (this reload is delayed) + def = makeDeferred(); + await toggleFilterMenu(graph); + await toggleMenuItem(graph, "My Filter"); + + checkDatasets(assert, graph, ["data", "label"], { + data: [4, 4], + label: "Count", + }); + + // Toggle a measure + await toggleMenu(graph, "Measures"); + await toggleMenuItem(graph, "Foo"); + + checkDatasets(assert, graph, ["data", "label"], { + data: [4, 4], + label: "Count", + }); + + def.resolve(); + await nextTick(); + + checkDatasets(assert, graph, ["data", "label"], { + data: [82, 157], + label: "Foo", + }); + } + ); + + QUnit.test("change graph mode while loading a filter", async function (assert) { + let def; + const graph = await makeView({ + serverData, + type: "graph", + resModel: "foo", + arch: ` + + + `, + searchViewArch: ` + + + `, + mockRPC: function (route, args) { + if (args.method === "web_read_group") { + return Promise.resolve(def); + } + }, + }); + + checkDatasets(assert, graph, ["data", "label"], { + data: [4, 4], + label: "Count", + }); + checkModeIs(assert, graph, "line"); + + // Set a domain (this reload is delayed) + def = makeDeferred(); + await toggleFilterMenu(graph); + await toggleMenuItem(graph, "My Filter"); + + checkDatasets(assert, graph, ["data", "label"], { + data: [4, 4], + label: "Count", + }); + checkModeIs(assert, graph, "line"); + + // Change graph mode + await selectMode(graph, "bar"); + + checkDatasets(assert, graph, ["data", "label"], { + data: [4, 4], + label: "Count", + }); + checkModeIs(assert, graph, "line"); + + def.resolve(); + await nextTick(); + + checkDatasets(assert, graph, ["data", "label"], { + data: [1], + label: "Count", + }); + checkModeIs(assert, graph, "bar"); + }); });