[FIX] web: fix concurrency issue in pivot
Let's assume the following scenario in a pivot view - toggle a filter on a slow network s.t. it takes a while to load - while it's loading, toggle a measure Before this commit, the view was kind of locked after those two steps, i.e. toggling measures, expanding groups, flipping axis did not work anymore (it could be unlocked by interacting again with the search view though). The reason is that the new filter triggered a re-rendering of the withSearch component (the parent of the pivot view), thus calling willUpdateProps on the pivot view, which reloads the data. Toggling a measure cancels the current load operation (via keepLast), and the promise returned by willUpdateProps is thus pending forever. From that point, only another re-rendering from the parent (withSearch) could unlock the situation by cancelling the previous rendering. X-original-commit: b9278e07b9ff01e608ccba6f4602eb9069213a46 Part-of: odoo/odoo#76576
This commit is contained in:
@@ -26,12 +26,12 @@ export class Model extends EventBus {
|
||||
* @param {Object} params
|
||||
* @param {Object} services
|
||||
*/
|
||||
setup(params, services) {}
|
||||
setup(/* params, services */) {}
|
||||
|
||||
/**
|
||||
* @param {SearchParams} searchParams
|
||||
*/
|
||||
load(searchParams) {}
|
||||
load(/* searchParams */) {}
|
||||
|
||||
notify() {
|
||||
this.trigger("update");
|
||||
@@ -78,7 +78,7 @@ export function useModel(ModelClass, params, options = {}) {
|
||||
|
||||
onWillUpdateProps((nextProps) => {
|
||||
const searchParams = getSearchParams(nextProps);
|
||||
searchParams.useSampleModel = false; // not sure it is good --> we'll know when implementing dashboard
|
||||
searchParams.useSampleModel = false;
|
||||
return model.load(searchParams);
|
||||
});
|
||||
return model;
|
||||
|
||||
@@ -281,7 +281,6 @@ import { computeReportMeasures, processMeasure } from "@web/views/helpers/utils"
|
||||
|
||||
/**
|
||||
* @typedef Meta
|
||||
* @property {Object} meta
|
||||
* @property {string[]} activeMeasures
|
||||
* @property {string[]} colGroupBys
|
||||
* @property {boolean} disableLinking
|
||||
@@ -296,6 +295,8 @@ import { computeReportMeasures, processMeasure } from "@web/views/helpers/utils"
|
||||
* @property {string[]} expandedRowGroupBys
|
||||
* @property {string[]} expandedColGroupBys
|
||||
* @property {Object} sortedColumn
|
||||
* @property {Array[]} domains
|
||||
* @property {string[]} origins
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -316,7 +317,6 @@ import { computeReportMeasures, processMeasure } from "@web/views/helpers/utils"
|
||||
* @typedef Config
|
||||
* @property {Meta} meta
|
||||
* @property {Data} data
|
||||
* @property {SearchParams} searchParams
|
||||
*/
|
||||
|
||||
export class PivotModel extends Model {
|
||||
@@ -360,8 +360,13 @@ export class PivotModel extends Model {
|
||||
expandedColGroupBys: params.meta.expandedColGroupBys || [],
|
||||
sortedColumn,
|
||||
});
|
||||
this.meta = this._buildMeta(meta);
|
||||
|
||||
this.searchParams = {
|
||||
context: {},
|
||||
domain: [],
|
||||
domains: [],
|
||||
groupBy: [],
|
||||
};
|
||||
this.data = params.data || {
|
||||
colGroupTree: null,
|
||||
rowGroupTree: null,
|
||||
@@ -370,16 +375,12 @@ export class PivotModel extends Model {
|
||||
counts: {},
|
||||
numbering: {},
|
||||
};
|
||||
this.meta = this._buildMeta(meta);
|
||||
|
||||
this.searchParams = {
|
||||
context: {},
|
||||
domain: [],
|
||||
groupBy: [],
|
||||
|
||||
domains: null, // not the same as searchParams.domains from cp
|
||||
origins: null, // generated from searchParams.domains from cp
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -421,7 +422,7 @@ export class PivotModel extends Model {
|
||||
} else {
|
||||
meta.expandedColGroupBys.push(groupBy);
|
||||
}
|
||||
const config = { meta, data: this.data, searchParams: this.searchParams };
|
||||
const config = { meta, data: this.data };
|
||||
await this._expandGroup(groupId, type, config);
|
||||
this.meta = meta;
|
||||
this.notify();
|
||||
@@ -493,8 +494,8 @@ export class PivotModel extends Model {
|
||||
* This is the easiest way to expand all the groups that are not expanded
|
||||
*/
|
||||
async expandAll() {
|
||||
const config = { meta: this.meta, data: this.data, searchParams: this.searchParams };
|
||||
this.data = await this._loadData(config);
|
||||
const config = { meta: this.meta, data: this.data };
|
||||
await this._loadData(config, false);
|
||||
this.notify();
|
||||
}
|
||||
/**
|
||||
@@ -504,7 +505,11 @@ export class PivotModel extends Model {
|
||||
* @param {'row'|'col'} type
|
||||
*/
|
||||
async expandGroup(groupId, type) {
|
||||
const config = { meta: this.meta, data: this.data, searchParams: this.searchParams };
|
||||
if (this.loadProm) {
|
||||
return; // we are currently reloaded the table
|
||||
}
|
||||
|
||||
const config = { meta: this.meta, data: this.data };
|
||||
await this._expandGroup(groupId, type, config);
|
||||
this.notify();
|
||||
}
|
||||
@@ -516,7 +521,7 @@ export class PivotModel extends Model {
|
||||
*/
|
||||
exportData() {
|
||||
const measureCount = this.meta.activeMeasures.length;
|
||||
const originCount = this.searchParams.origins.length;
|
||||
const originCount = this.meta.origins.length;
|
||||
|
||||
const table = this.getTable();
|
||||
|
||||
@@ -633,7 +638,7 @@ export class PivotModel extends Model {
|
||||
* @returns {Array[]}
|
||||
*/
|
||||
getGroupDomain(group) {
|
||||
const config = { meta: this.meta, data: this.data, searchParams: this.searchParams };
|
||||
const config = { meta: this.meta, data: this.data };
|
||||
return this._getGroupDomain(group, config);
|
||||
}
|
||||
/**
|
||||
@@ -661,8 +666,7 @@ export class PivotModel extends Model {
|
||||
* @returns {boolean} true iff there's no data in the table
|
||||
*/
|
||||
hasData() {
|
||||
const config = { meta: this.meta, data: this.data, searchParams: this.searchParams };
|
||||
return this._hasData(config);
|
||||
return this._hasData(this.data);
|
||||
}
|
||||
/**
|
||||
* @override
|
||||
@@ -670,7 +674,7 @@ export class PivotModel extends Model {
|
||||
*/
|
||||
async load(searchParams) {
|
||||
this.orm2Use = this.realORM;
|
||||
searchParams = JSON.parse(JSON.stringify(searchParams)); //This to prevent errors in the Dashboard
|
||||
this.searchParams = JSON.parse(JSON.stringify(searchParams));
|
||||
|
||||
const activeMeasures =
|
||||
processMeasure(searchParams.context.pivot_measures) || this.meta.activeMeasures;
|
||||
@@ -687,10 +691,6 @@ export class PivotModel extends Model {
|
||||
}
|
||||
meta.colGroupBys = searchParams.context.pivot_column_groupby || this.meta.colGroupBys;
|
||||
|
||||
const { domains, origins } = this._computeDerivedParams(searchParams);
|
||||
searchParams.domains = domains;
|
||||
searchParams.origins = origins;
|
||||
|
||||
if (JSON.stringify(meta.rowGroupBys) !== JSON.stringify(this.meta.rowGroupBys)) {
|
||||
meta.expandedRowGroupBys = [];
|
||||
}
|
||||
@@ -704,20 +704,8 @@ export class PivotModel extends Model {
|
||||
meta.activeMeasures,
|
||||
meta.additionalMeasures
|
||||
);
|
||||
const config = { meta, data: this.data, searchParams };
|
||||
const data = await this._loadData(config);
|
||||
const oldConfig = { meta: this.meta, data: this.data, searchParams: this.searchParams };
|
||||
if (this._hasData(oldConfig)) {
|
||||
if (symmetricalDifference(meta.rowGroupBys, this.meta.rowGroupBys).length === 0) {
|
||||
this._pruneTree(data.rowGroupTree, this.data.rowGroupTree);
|
||||
}
|
||||
if (symmetricalDifference(meta.colGroupBys, this.meta.colGroupBys).length === 0) {
|
||||
this._pruneTree(data.colGroupTree, this.data.colGroupTree);
|
||||
}
|
||||
}
|
||||
this.data = data;
|
||||
this.meta = meta;
|
||||
this.searchParams = searchParams;
|
||||
const config = { meta, data: this.data };
|
||||
return this._loadData(config);
|
||||
}
|
||||
/**
|
||||
* Sort the rows, depending on the values of a given column. This is an
|
||||
@@ -729,7 +717,7 @@ export class PivotModel extends Model {
|
||||
sortRows(sortedColumn) {
|
||||
this._cancelPreviousOperation();
|
||||
|
||||
const config = { meta: this.meta, data: this.data, searchParams: this.searchParams };
|
||||
const config = { meta: this.meta, data: this.data };
|
||||
this._sortRows(sortedColumn, config);
|
||||
|
||||
this.notify();
|
||||
@@ -752,13 +740,13 @@ export class PivotModel extends Model {
|
||||
// to wait in case there is a pending load)
|
||||
meta.activeMeasures.splice(index, 1);
|
||||
await Promise.resolve(this.loadProm);
|
||||
this.meta = meta;
|
||||
} else {
|
||||
meta.activeMeasures.push(fieldName);
|
||||
const config = { meta, data: this.data, searchParams: this.searchParams };
|
||||
this.data = await this._loadData(config);
|
||||
let config = { meta, data: this.data };
|
||||
await this._loadData(config);
|
||||
}
|
||||
this.nextActiveMeasures = null;
|
||||
this.meta = meta;
|
||||
this.notify();
|
||||
}
|
||||
|
||||
@@ -810,6 +798,14 @@ export class PivotModel extends Model {
|
||||
meta.customGroupBys = new Map([...meta.customGroupBys]);
|
||||
// shallow copy sortedColumn because we never modify groupId in place
|
||||
meta.sortedColumn = meta.sortedColumn ? { ...meta.sortedColumn } : null;
|
||||
if (this.searchParams.comparison) {
|
||||
const domains = this.searchParams.comparison.domains.slice().reverse();
|
||||
meta.domains = domains.map((d) => d.arrayRepr);
|
||||
meta.origins = domains.map((d) => d.description);
|
||||
} else {
|
||||
meta.domains = [this.searchParams.domain];
|
||||
meta.origins = [""];
|
||||
}
|
||||
Object.defineProperty(meta, "fullColGroupBys", {
|
||||
get() {
|
||||
return meta.colGroupBys.concat(meta.expandedColGroupBys);
|
||||
@@ -961,7 +957,7 @@ export class PivotModel extends Model {
|
||||
measureSpecs,
|
||||
groupBy,
|
||||
options,
|
||||
config.searchParams.context
|
||||
this.searchParams.context
|
||||
);
|
||||
return {
|
||||
group: group,
|
||||
@@ -1016,7 +1012,7 @@ export class PivotModel extends Model {
|
||||
* @returns {Array}
|
||||
*/
|
||||
_getMeasurements(group, config) {
|
||||
const { meta, searchParams } = config;
|
||||
const { meta } = config;
|
||||
return meta.activeMeasures.reduce((measurements, measureName) => {
|
||||
var measurement = group[measureName];
|
||||
if (measurement instanceof Array) {
|
||||
@@ -1026,7 +1022,7 @@ export class PivotModel extends Model {
|
||||
if (meta.measures[measureName].type === "boolean" && measurement instanceof Boolean) {
|
||||
measurement = measurement ? 1 : 0;
|
||||
}
|
||||
if (searchParams.origins.length > 1 && !measurement) {
|
||||
if (meta.origins.length > 1 && !measurement) {
|
||||
measurement = 0;
|
||||
}
|
||||
measurements[measureName] = measurement;
|
||||
@@ -1051,7 +1047,7 @@ export class PivotModel extends Model {
|
||||
height: 1,
|
||||
measure: measureName,
|
||||
title: this.meta.measures[measureName].string,
|
||||
width: 2 * this.searchParams.origins.length - 1,
|
||||
width: 2 * this.meta.origins.length - 1,
|
||||
};
|
||||
if (
|
||||
sortedColumn.measure === measureName &&
|
||||
@@ -1135,7 +1131,7 @@ export class PivotModel extends Model {
|
||||
const isSortedByOrigin = isSorted && !sortedColumn.originIndexes[1];
|
||||
const isSortedByVariation = isSorted && sortedColumn.originIndexes[1];
|
||||
|
||||
this.searchParams.origins.forEach((origin, originIndex) => {
|
||||
this.meta.origins.forEach((origin, originIndex) => {
|
||||
const originCell = {
|
||||
groupId: groupId,
|
||||
height: 1,
|
||||
@@ -1181,7 +1177,7 @@ export class PivotModel extends Model {
|
||||
const colGroupBys = this.meta.fullColGroupBys;
|
||||
const height = colGroupBys.length + 1;
|
||||
const measureCount = this.meta.activeMeasures.length;
|
||||
const originCount = this.searchParams.origins.length;
|
||||
const originCount = this.meta.origins.length;
|
||||
const leafCounts = this._getLeafCounts(this.data.colGroupTree);
|
||||
let headers = [];
|
||||
const measureColumns = []; // used to generate the measure cells
|
||||
@@ -1330,11 +1326,10 @@ export class PivotModel extends Model {
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {Config} config
|
||||
* @param {Data} data
|
||||
* @returns {boolean} true iff there's no data in the table
|
||||
*/
|
||||
_hasData(config) {
|
||||
const { data } = config;
|
||||
_hasData(data) {
|
||||
return (data.counts[JSON.stringify([[], []])] || []).some((count) => {
|
||||
return count > 0;
|
||||
});
|
||||
@@ -1350,9 +1345,18 @@ export class PivotModel extends Model {
|
||||
* @private
|
||||
* @param {Config} config
|
||||
*/
|
||||
async _loadData(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, meta, searchParams } = config;
|
||||
const { data, meta } = config;
|
||||
data.rowGroupTree = { root: { labels: [], values: [] }, directSubTrees: new Map() };
|
||||
data.colGroupTree = { root: { labels: [], values: [] }, directSubTrees: new Map() };
|
||||
data.measurements = {};
|
||||
@@ -1360,24 +1364,46 @@ export class PivotModel extends Model {
|
||||
data.groupDomains = {};
|
||||
data.numbering = {};
|
||||
const key = JSON.stringify([[], []]);
|
||||
data.groupDomains[key] = searchParams.domains.slice(0);
|
||||
data.groupDomains[key] = meta.domains.slice(0);
|
||||
|
||||
const group = { rowValues: [], colValues: [] };
|
||||
const leftDivisors = sections(meta.fullRowGroupBys);
|
||||
const rightDivisors = sections(meta.fullColGroupBys);
|
||||
const divisors = cartesian(leftDivisors, rightDivisors);
|
||||
|
||||
await this._subdivideGroup(group, divisors.slice(0, 1), config);
|
||||
await this._subdivideGroup(group, divisors.slice(1), config);
|
||||
this._subdivideGroup(group, divisors.slice(0, 1), config)
|
||||
.then(() => {
|
||||
return this._subdivideGroup(group, divisors.slice(1), config);
|
||||
})
|
||||
.then(async () => {
|
||||
if (meta.useSampleModel && !this._hasData(config.data)) {
|
||||
const fakeORM = buildSampleORM(meta.resModel, meta.fields, this.user);
|
||||
this.orm2Use = fakeORM;
|
||||
await this._loadData(config);
|
||||
} else if (this.orm2Use.rpc.name !== "fakeRPC") {
|
||||
meta.useSampleModel = false;
|
||||
}
|
||||
|
||||
if (meta.useSampleModel && !this._hasData(config)) {
|
||||
const fakeORM = buildSampleORM(meta.resModel, meta.fields, this.user);
|
||||
this.orm2Use = fakeORM;
|
||||
return this._loadData(config);
|
||||
} else if (this.orm2Use.rpc.name !== "fakeRPC") {
|
||||
meta.useSampleModel = false;
|
||||
}
|
||||
return config.data;
|
||||
// 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(meta.rowGroupBys, this.meta.rowGroupBys).length === 0
|
||||
) {
|
||||
this._pruneTree(data.rowGroupTree, this.data.rowGroupTree);
|
||||
}
|
||||
if (
|
||||
symmetricalDifference(meta.colGroupBys, this.meta.colGroupBys).length === 0
|
||||
) {
|
||||
this._pruneTree(data.colGroupTree, this.data.colGroupTree);
|
||||
}
|
||||
}
|
||||
|
||||
this.data = config.data;
|
||||
this.meta = config.meta;
|
||||
this.resolveLoadProm();
|
||||
});
|
||||
|
||||
return this.loadProm;
|
||||
}
|
||||
/**
|
||||
* Extract the information in the read_group results (groupSubdivisions)
|
||||
@@ -1392,7 +1418,7 @@ export class PivotModel extends Model {
|
||||
* @param {Config} config
|
||||
*/
|
||||
_prepareData(group, groupSubdivisions, config) {
|
||||
const { data, meta, searchParams } = config;
|
||||
const { data, meta } = config;
|
||||
const groupRowValues = group.rowValues;
|
||||
let groupRowLabels = [];
|
||||
let rowSubTree = data.rowGroupTree;
|
||||
@@ -1438,21 +1464,21 @@ export class PivotModel extends Model {
|
||||
const originIndex = groupSubdivision.group.originIndex;
|
||||
|
||||
if (!(key in data.measurements)) {
|
||||
data.measurements[key] = searchParams.origins.map(() => {
|
||||
data.measurements[key] = meta.origins.map(() => {
|
||||
return this._getMeasurements({}, config);
|
||||
});
|
||||
}
|
||||
data.measurements[key][originIndex] = this._getMeasurements(subGroup, config);
|
||||
|
||||
if (!(key in data.counts)) {
|
||||
data.counts[key] = searchParams.origins.map(function () {
|
||||
data.counts[key] = meta.origins.map(function () {
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
data.counts[key][originIndex] = subGroup.__count;
|
||||
|
||||
if (!(key in data.groupDomains)) {
|
||||
data.groupDomains[key] = searchParams.origins.map(function () {
|
||||
data.groupDomains[key] = meta.origins.map(function () {
|
||||
return Domain.FALSE.toList();
|
||||
});
|
||||
}
|
||||
@@ -1469,25 +1495,6 @@ export class PivotModel extends Model {
|
||||
this._sortRows(meta.sortedColumn, config);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @todo review comment
|
||||
* Determine this.searchParams.domains and this.searchParams.origins from
|
||||
* this.searchParams.domains.
|
||||
*
|
||||
* @private
|
||||
* @param {SearchParams} searchParams
|
||||
*/
|
||||
_computeDerivedParams(searchParams) {
|
||||
const { comparison, domain } = searchParams;
|
||||
if (comparison) {
|
||||
const domains = searchParams.comparison.domains.slice().reverse();
|
||||
return {
|
||||
domains: domains.map((d) => d.arrayRepr),
|
||||
origins: domains.map((d) => d.description),
|
||||
};
|
||||
}
|
||||
return { domains: [domain], origins: [""] };
|
||||
}
|
||||
/**
|
||||
* Make any group in tree a leaf if it was a leaf in oldTree.
|
||||
*
|
||||
@@ -1561,10 +1568,10 @@ export class PivotModel extends Model {
|
||||
* @param {Config} config
|
||||
*/
|
||||
async _subdivideGroup(group, divisors, config) {
|
||||
const { data, searchParams } = config;
|
||||
const { data, meta } = config;
|
||||
const key = JSON.stringify([group.rowValues, group.colValues]);
|
||||
|
||||
const proms = searchParams.origins.reduce((acc, origin, originIndex) => {
|
||||
const proms = meta.origins.reduce((acc, origin, originIndex) => {
|
||||
// if no information on group content is available, we fetch data.
|
||||
// if group is known to be empty for the given origin,
|
||||
// we don't need to fetch data for that origin.
|
||||
|
||||
@@ -4882,6 +4882,52 @@ QUnit.module("Views", (hooks) => {
|
||||
assert.strictEqual(getCurrentValues(pivot), ["2", "4", "1", "1", "1", "3"].join(","));
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"concurrent reloads: add a filter, and directly toggle a measure",
|
||||
async function (assert) {
|
||||
let def;
|
||||
const pivot = await makeView({
|
||||
type: "pivot",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<pivot>
|
||||
<field name="foo" type="measure"/>
|
||||
<field name="product_id" type="row"/>
|
||||
</pivot>`,
|
||||
searchViewArch: `
|
||||
<search>
|
||||
<filter name="my_filter" string="My Filter" domain="[('product_id', '=', 37)]"/>
|
||||
</search>`,
|
||||
mockRPC(route, args) {
|
||||
if (args.method === "read_group") {
|
||||
return Promise.resolve(def);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(getCurrentValues(pivot), ["32", "12", "20"].join(","));
|
||||
|
||||
// Set a domain (this reload is delayed)
|
||||
def = makeDeferred();
|
||||
await toggleFilterMenu(pivot);
|
||||
await toggleMenuItem(pivot, "My Filter");
|
||||
|
||||
assert.strictEqual(getCurrentValues(pivot), ["32", "12", "20"].join(","));
|
||||
|
||||
// Toggle a measure
|
||||
await toggleMenu(pivot, "Measures");
|
||||
await toggleMenuItem(pivot, "Count");
|
||||
|
||||
assert.strictEqual(getCurrentValues(pivot), ["32", "12", "20"].join(","));
|
||||
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(getCurrentValues(pivot), ["12", "1", "12", "1"].join(","));
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"if no measure is set in arch, 'Count' is used as measure initially",
|
||||
async function (assert) {
|
||||
|
||||
Reference in New Issue
Block a user