From 2c06b1dd68c3736cd2c52c784e45ebb383cc6bd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Lef=C3=A8vre?= Date: Tue, 6 Sep 2022 14:44:22 +0000 Subject: [PATCH] [IMP] spreadsheet[_account]: update o-spreadsheet to latest version This update contains the following commit: https://github.com/odoo/o-spreadsheet/commit/50e7c2006837f9c080d6d917920a04b1361b8b6a https://github.com/odoo/o-spreadsheet/commit/7bc2a8f7c6c6ef33f38f87126a46b6093424b61f https://github.com/odoo/o-spreadsheet/commit/90d64243a1ba13da402cf5b7611c762d5bba70f8 https://github.com/odoo/o-spreadsheet/commit/cb7534a9f6068bf9a1e008810ab3a3d83ae45d6c https://github.com/odoo/o-spreadsheet/commit/a5236ad4b8a7a83a62c4806f2c7aa69d92535b8f Part-of: odoo/odoo#97606 --- .../actions/spreadsheet_download_action.js | 2 +- .../static/src/o_spreadsheet/o_spreadsheet.js | 1369 ++++++++--------- .../src/o_spreadsheet/o_spreadsheet.xml | 2 +- .../static/tests/currency/currency_test.js | 13 +- .../global_filters_model_test.js | 10 +- .../static/tests/lists/list_plugin_test.js | 13 +- .../tests/pivots/model/pivot_plugin_test.js | 23 +- .../model/pivot_positional_formula_test.js | 18 +- addons/spreadsheet/static/tests/utils/list.js | 6 +- .../spreadsheet/static/tests/utils/model.js | 17 + .../spreadsheet/static/tests/utils/pivot.js | 3 +- .../tests/model/account_groups_tests.js | 12 +- .../static/tests/model/accounting_tests.js | 41 +- .../static/tests/model/fiscal_year_tests.js | 12 +- .../tests/ui/accounting_drilldown_tests.js | 8 +- 15 files changed, 755 insertions(+), 794 deletions(-) diff --git a/addons/spreadsheet/static/src/actions/spreadsheet_download_action.js b/addons/spreadsheet/static/src/actions/spreadsheet_download_action.js index 8376dd3e9c5..31c876271c2 100644 --- a/addons/spreadsheet/static/src/actions/spreadsheet_download_action.js +++ b/addons/spreadsheet/static/src/actions/spreadsheet_download_action.js @@ -32,7 +32,7 @@ async function downloadSpreadsheet(env, action) { * @returns {Promise} */ async function waitForDataLoaded(model) { - model.dispatch("EVALUATE_ALL_SHEETS"); + model.dispatch("EVALUATE_CELLS"); return new Promise((resolve, reject) => { let interval = undefined; interval = browser.setInterval(() => { diff --git a/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js b/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js index a0caf22c923..8cf44a8a441 100644 --- a/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js +++ b/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js @@ -216,6 +216,7 @@ } } + const CANVAS_SHIFT = 0.5; // Colors const BACKGROUND_GRAY_COLOR = "#f5f5f5"; const BACKGROUND_HEADER_COLOR = "#F8F9FA"; @@ -912,7 +913,7 @@ let memo; const lazyValue = () => { if (!isMemoized) { - memo = fn(); + memo = fn instanceof Function ? fn() : fn; isMemoized = true; } return memo; @@ -5761,502 +5762,6 @@ return { ...zone }; } - // HELPERS - function assert(condition, message) { - if (!condition()) { - throw new Error(message); - } - } - // ----------------------------------------------------------------------------- - // FORMAT FUNCTIONS - // ----------------------------------------------------------------------------- - const expectNumberValueError = (value) => _lt("The function [[FUNCTION_NAME]] expects a number value, but '%s' is a string, and cannot be coerced to a number.", value); - function toNumber(value) { - switch (typeof value) { - case "number": - return value; - case "boolean": - return value ? 1 : 0; - case "string": - if (isNumber(value) || value === "") { - return parseNumber(value); - } - const internalDate = parseDateTime(value); - if (internalDate) { - return internalDate.value; - } - throw new Error(expectNumberValueError(value)); - default: - return 0; - } - } - function strictToNumber(value) { - if (value === "") { - throw new Error(expectNumberValueError(value)); - } - return toNumber(value); - } - function toString(value) { - switch (typeof value) { - case "string": - return value; - case "number": - return value.toString(); - case "boolean": - return value ? "TRUE" : "FALSE"; - default: - return ""; - } - } - const expectBooleanValueError = (value) => _lt("The function [[FUNCTION_NAME]] expects a boolean value, but '%s' is a text, and cannot be coerced to a number.", value); - function toBoolean(value) { - switch (typeof value) { - case "boolean": - return value; - case "string": - if (value) { - let uppercaseVal = value.toUpperCase(); - if (uppercaseVal === "TRUE") { - return true; - } - if (uppercaseVal === "FALSE") { - return false; - } - throw new Error(expectBooleanValueError(value)); - } - else { - return false; - } - case "number": - return value ? true : false; - default: - return false; - } - } - function strictToBoolean(value) { - if (value === "") { - throw new Error(expectBooleanValueError(value)); - } - return toBoolean(value); - } - function toJsDate(value) { - return numberToJsDate(toNumber(value)); - } - // ----------------------------------------------------------------------------- - // VISIT FUNCTIONS - // ----------------------------------------------------------------------------- - function visitArgs(args, cellCb, dataCb) { - for (let arg of args) { - if (Array.isArray(arg)) { - // arg is ref to a Cell/Range - const lenRow = arg.length; - const lenCol = arg[0].length; - for (let y = 0; y < lenCol; y++) { - for (let x = 0; x < lenRow; x++) { - cellCb(arg[x][y]); - } - } - } - else { - // arg is set directly in the formula function - dataCb(arg); - } - } - } - function visitAny(args, cb) { - visitArgs(args, cb, cb); - } - function visitNumbers(args, cb) { - visitArgs(args, (cellValue) => { - if (typeof cellValue === "number") { - cb(cellValue); - } - }, (argValue) => { - cb(strictToNumber(argValue)); - }); - } - // ----------------------------------------------------------------------------- - // REDUCE FUNCTIONS - // ----------------------------------------------------------------------------- - function reduceArgs(args, cellCb, dataCb, initialValue) { - let val = initialValue; - for (let arg of args) { - if (Array.isArray(arg)) { - // arg is ref to a Cell/Range - const lenRow = arg.length; - const lenCol = arg[0].length; - for (let y = 0; y < lenCol; y++) { - for (let x = 0; x < lenRow; x++) { - val = cellCb(val, arg[x][y]); - } - } - } - else { - // arg is set directly in the formula function - val = dataCb(val, arg); - } - } - return val; - } - function reduceAny(args, cb, initialValue) { - return reduceArgs(args, cb, cb, initialValue); - } - function reduceNumbers(args, cb, initialValue) { - return reduceArgs(args, (acc, ArgValue) => { - if (typeof ArgValue === "number") { - return cb(acc, ArgValue); - } - return acc; - }, (acc, argValue) => { - return cb(acc, strictToNumber(argValue)); - }, initialValue); - } - function reduceNumbersTextAs0(args, cb, initialValue) { - return reduceArgs(args, (acc, ArgValue) => { - if (ArgValue !== undefined && ArgValue !== null) { - if (typeof ArgValue === "number") { - return cb(acc, ArgValue); - } - else if (typeof ArgValue === "boolean") { - return cb(acc, toNumber(ArgValue)); - } - else { - return cb(acc, 0); - } - } - return acc; - }, (acc, argValue) => { - return cb(acc, toNumber(argValue)); - }, initialValue); - } - // ----------------------------------------------------------------------------- - // CONDITIONAL EXPLORE FUNCTIONS - // ----------------------------------------------------------------------------- - /** - * This function allows to visit arguments and stop the visit if necessary. - * It is mainly used to bypass argument evaluation for functions like OR or AND. - */ - function conditionalVisitArgs(args, cellCb, dataCb) { - for (let arg of args) { - if (Array.isArray(arg)) { - // arg is ref to a Cell/Range - const lenRow = arg.length; - const lenCol = arg[0].length; - for (let y = 0; y < lenCol; y++) { - for (let x = 0; x < lenRow; x++) { - if (!cellCb(arg[x][y])) - return; - } - } - } - else { - // arg is set directly in the formula function - if (!dataCb(arg)) - return; - } - } - } - function conditionalVisitBoolean(args, cb) { - return conditionalVisitArgs(args, (ArgValue) => { - if (typeof ArgValue === "boolean") { - return cb(ArgValue); - } - if (typeof ArgValue === "number") { - return cb(ArgValue ? true : false); - } - return true; - }, (argValue) => { - if (argValue !== undefined && argValue !== null) { - return cb(strictToBoolean(argValue)); - } - return true; - }); - } - function getPredicate(descr, isQuery) { - let operator; - let operand; - let subString = descr.substring(0, 2); - if (subString === "<=" || subString === ">=" || subString === "<>") { - operator = subString; - operand = descr.substring(2); - } - else { - subString = descr.substring(0, 1); - if (subString === "<" || subString === ">" || subString === "=") { - operator = subString; - operand = descr.substring(1); - } - else { - operator = "="; - operand = descr; - } - } - if (isNumber(operand)) { - operand = toNumber(operand); - } - else if (operand === "TRUE" || operand === "FALSE") { - operand = toBoolean(operand); - } - const result = { operator, operand }; - if (typeof operand === "string") { - if (isQuery) { - operand += "*"; - } - result.regexp = operandToRegExp(operand); - } - return result; - } - function operandToRegExp(operand) { - let exp = ""; - let predecessor = ""; - for (let char of operand) { - if (char === "?" && predecessor !== "~") { - exp += "."; - } - else if (char === "*" && predecessor !== "~") { - exp += ".*"; - } - else { - if (char === "*" || char === "?") { - //remove "~" - exp = exp.slice(0, -1); - } - if (["^", ".", "[", "]", "$", "(", ")", "*", "+", "?", "|", "{", "}", "\\"].includes(char)) { - exp += "\\"; - } - exp += char; - } - predecessor = char; - } - return new RegExp("^" + exp + "$", "i"); - } - function evaluatePredicate(value, criterion) { - const { operator, operand } = criterion; - if (value === undefined || operand === undefined) { - return false; - } - if (typeof operand === "number" && operator === "=") { - return toString(value) === toString(operand); - } - if (operator === "<>" || operator === "=") { - let result; - if (typeof value === typeof operand) { - if (typeof value === "string" && criterion.regexp) { - result = criterion.regexp.test(value); - } - else { - result = value === operand; - } - } - else { - result = false; - } - return operator === "=" ? result : !result; - } - if (typeof value === typeof operand) { - switch (operator) { - case "<": - return value < operand; - case ">": - return value > operand; - case "<=": - return value <= operand; - case ">=": - return value >= operand; - } - } - return false; - } - /** - * Functions used especially for predicate evaluation on ranges. - * - * Take ranges with same dimensions and take predicates, one for each range. - * For (i, j) coordinates, if all elements with coordinates (i, j) of each - * range correspond to the associated predicate, then the function uses a callback - * function with the parameters "i" and "j". - * - * Syntax: - * visitMatchingRanges([range1, predicate1, range2, predicate2, ...], cb(i,j), likeSelection) - * - * - range1 (range): The range to check against predicate1. - * - predicate1 (string): The pattern or test to apply to range1. - * - range2: (range, repeatable) ranges to check. - * - predicate2 (string, repeatable): Additional pattern or test to apply to range2. - * - * - cb(i: number, j: number) => void: the callback function. - * - * - isQuery (boolean) indicates if the comparison with a string should be done as a SQL-like query. - * (Ex1 isQuery = true, predicate = "abc", element = "abcde": predicate match the element), - * (Ex2 isQuery = false, predicate = "abc", element = "abcde": predicate not match the element). - * (Ex3 isQuery = true, predicate = "abc", element = "abc": predicate match the element), - * (Ex4 isQuery = false, predicate = "abc", element = "abc": predicate match the element). - */ - function visitMatchingRanges(args, cb, isQuery = false) { - const countArg = args.length; - if (countArg % 2 === 1) { - throw new Error(_lt(`Function [[FUNCTION_NAME]] expects criteria_range and criterion to be in pairs.`)); - } - const dimRow = args[0].length; - const dimCol = args[0][0].length; - let predicates = []; - for (let i = 0; i < countArg - 1; i += 2) { - const criteriaRange = args[i]; - if (!Array.isArray(criteriaRange) || - criteriaRange.length !== dimRow || - criteriaRange[0].length !== dimCol) { - throw new Error(_lt(`Function [[FUNCTION_NAME]] expects criteria_range to have the same dimension`)); - } - const description = toString(args[i + 1]); - predicates.push(getPredicate(description, isQuery)); - } - for (let i = 0; i < dimRow; i++) { - for (let j = 0; j < dimCol; j++) { - let validatedPredicates = true; - for (let k = 0; k < countArg - 1; k += 2) { - const criteriaValue = args[k][i][j]; - const criterion = predicates[k / 2]; - validatedPredicates = evaluatePredicate(criteriaValue, criterion); - if (!validatedPredicates) { - break; - } - } - if (validatedPredicates) { - cb(i, j); - } - } - } - } - // ----------------------------------------------------------------------------- - // COMMON FUNCTIONS - // ----------------------------------------------------------------------------- - /** - * Perform a dichotomic search and return the index of the nearest match less than - * or equal to the target. If all values in the range are greater than the target, - * -1 is returned. - * If the range is not in sorted order, an incorrect value might be returned. - * - * Example: - * - [3, 6, 10], 3 => 0 - * - [3, 6, 10], 6 => 1 - * - [3, 6, 10], 9 => 1 - * - [3, 6, 10], 42 => 2 - * - [3, 6, 10], 2 => -1 - * - [3, undefined, 6, undefined, 10], 9 => 2 - * - [3, 6, undefined, undefined, undefined, 10], 2 => -1 - */ - function dichotomicPredecessorSearch(range, target) { - if (target === null) { - return -1; - } - const targetType = typeof target; - let valMin = undefined; - let valMinIndex = undefined; - let indexLeft = 0; - let indexRight = range.length - 1; - if (typeof range[indexLeft] === targetType && target < range[indexLeft]) { - return -1; - } - if (typeof range[indexRight] === targetType && range[indexRight] <= target) { - return indexRight; - } - let indexMedian; - let currentIndex; - let currentVal; - let currentType; - while (indexRight - indexLeft >= 0) { - indexMedian = Math.ceil((indexLeft + indexRight) / 2); - currentIndex = indexMedian; - currentVal = range[currentIndex]; - currentType = typeof currentVal; - // 1 - linear search to find value with the same type - while (indexLeft <= currentIndex && targetType !== currentType) { - currentIndex--; - currentVal = range[currentIndex]; - currentType = typeof currentVal; - } - // 2 - check if value match - if (currentType === targetType && currentVal <= target) { - if (valMin === undefined || - valMin < currentVal || - (valMin === currentVal && valMinIndex < currentIndex)) { - valMin = currentVal; - valMinIndex = currentIndex; - } - } - // 3 - give new indexs for the Binary search - if (currentType === targetType && currentVal > target) { - indexRight = currentIndex - 1; - } - else { - indexLeft = indexMedian + 1; - } - } - // note that valMinIndex could be 0 - return valMinIndex !== undefined ? valMinIndex : -1; - } - /** - * Perform a dichotomic search and return the index of the nearest match more than - * or equal to the target. If all values in the range are smaller than the target, - * -1 is returned. - * If the range is not in sorted order, an incorrect value might be returned. - * - * Example: - * - [10, 6, 3], 3 => 2 - * - [10, 6, 3], 6 => 1 - * - [10, 6, 3], 9 => 0 - * - [10, 6, 3], 42 => -1 - * - [10, 6, 3], 2 => 2 - * - [10, undefined, 6, undefined, 3], 9 => 0 - * - [10, 6, undefined, undefined, undefined, 3], 2 => 5 - */ - function dichotomicSuccessorSearch(range, target) { - const targetType = typeof target; - let valMax; - let valMaxIndex = undefined; - let indexLeft = 0; - let indexRight = range.length - 1; - if (typeof range[indexLeft] === targetType && target > range[indexLeft]) { - return -1; - } - if (typeof range[indexRight] === targetType && range[indexRight] > target) { - return indexRight; - } - let indexMedian; - let currentIndex; - let currentVal; - let currentType; - while (indexRight - indexLeft >= 0) { - indexMedian = Math.ceil((indexLeft + indexRight) / 2); - currentIndex = indexMedian; - currentVal = range[currentIndex]; - currentType = typeof currentVal; - // 1 - linear search to find value with the same type - while (indexLeft <= currentIndex && targetType !== currentType) { - currentIndex--; - currentVal = range[currentIndex]; - currentType = typeof currentVal; - } - // 2 - check if value match - if (currentType === targetType && currentVal >= target) { - if (valMax === undefined || - valMax > currentVal || - (valMax === currentVal && valMaxIndex > currentIndex)) { - valMax = currentVal; - valMaxIndex = currentIndex; - } - } - // 3 - give new indexs for the Binary search - if (currentType === targetType && currentVal <= target) { - indexRight = currentIndex - 1; - } - else { - indexLeft = indexMedian + 1; - } - } - // note that valMaxIndex could be 0 - return valMaxIndex !== undefined ? valMaxIndex : -1; - } - /** * This file contains helpers that are common to different charts (mainly * line, bar and pie charts) @@ -6527,33 +6032,37 @@ // Scorecard // --------------------------------------------------------------------------- function getBaselineText(baseline, keyValue, baselineMode) { - if (!baseline) { + const baselineEvaluated = baseline === null || baseline === void 0 ? void 0 : baseline.evaluated; + if (!baseline || baselineEvaluated === undefined) { return ""; } - else if (baselineMode === "text" || !isNumber(keyValue) || !isNumber(baseline.content)) { + else if (baselineMode === "text" || + (keyValue === null || keyValue === void 0 ? void 0 : keyValue.type) !== CellValueType.number || + baselineEvaluated.type !== CellValueType.number) { return baseline.formattedValue; } else { - let diff = toNumber(keyValue) - toNumber(baseline.content); + let diff = (keyValue === null || keyValue === void 0 ? void 0 : keyValue.value) - baselineEvaluated.value; if (baselineMode === "percentage") { - diff = (diff / toNumber(baseline.content)) * 100; + diff = (diff / baselineEvaluated.value) * 100; } - const baselineValue = Math.abs(parseFloat(diff.toFixed(2))); - let baselineStr = baselineValue.toLocaleString(); + let baselineStr = Math.abs(parseFloat(diff.toFixed(2))).toLocaleString(); if (baselineMode === "percentage") { baselineStr += "%"; } else if (baseline.format) { - baselineStr = formatValue(baselineValue, baseline.format); + baselineStr = formatValue(diff, baseline.format); } return baselineStr; } } function getBaselineColor(baseline, baselineMode, keyValue, colorUp, colorDown) { - if (baselineMode === "text" || !isNumber(baseline) || !isNumber(keyValue)) { + if (baselineMode === "text" || + (baseline === null || baseline === void 0 ? void 0 : baseline.type) !== CellValueType.number || + (keyValue === null || keyValue === void 0 ? void 0 : keyValue.type) !== CellValueType.number) { return undefined; } - const diff = toNumber(keyValue) - toNumber(baseline); + const diff = keyValue.value - baseline.value; if (diff > 0) { return colorUp; } @@ -6563,10 +6072,12 @@ return undefined; } function getBaselineArrowDirection(baseline, keyValue, baselineMode) { - if (baselineMode === "text" || !isNumber(baseline) || !isNumber(keyValue)) { + if (baselineMode === "text" || + (baseline === null || baseline === void 0 ? void 0 : baseline.type) !== CellValueType.number || + (keyValue === null || keyValue === void 0 ? void 0 : keyValue.type) !== CellValueType.number) { return "neutral"; } - const diff = toNumber(keyValue) - toNumber(baseline); + const diff = keyValue.value - baseline.value; if (diff > 0) { return "up"; } @@ -6813,22 +6324,6 @@ stackedBar: this.stackedBar, }; } - getSheetIdsUsedInChartRanges() { - const sheetIds = new Set(); - const ranges = []; - this.dataSets.map((ds) => ds.dataRange).map((range) => ranges.push(range)); - this.dataSets - .map((ds) => ds.labelCell) - .filter(isDefined$1) - .map((range) => ranges.push(range)); - if (this.labelRange) { - ranges.push(this.labelRange); - } - for (const range of ranges) { - sheetIds.add(range.sheetId); - } - return Array.from(sheetIds); - } updateRanges(applyChange) { const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange); if (!isStale) { @@ -7099,9 +6594,6 @@ const definition = this.getDefinitionWithSpecificRanges(this.dataRange, sheetId); return new GaugeChart(definition, sheetId, this.getters); } - getSheetIdsUsedInChartRanges() { - return this.dataRange ? [this.dataRange.sheetId] : []; - } getDefinition() { return this.getDefinitionWithSpecificRanges(this.dataRange); } @@ -7464,22 +6956,6 @@ const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId); return new LineChart(definition, sheetId, this.getters); } - getSheetIdsUsedInChartRanges() { - const sheetIds = new Set(); - const ranges = []; - this.dataSets.map((ds) => ds.dataRange).map((range) => ranges.push(range)); - this.dataSets - .map((ds) => ds.labelCell) - .filter(isDefined$1) - .map((range) => ranges.push(range)); - if (this.labelRange) { - ranges.push(this.labelRange); - } - for (const range of ranges) { - sheetIds.add(range.sheetId); - } - return Array.from(sheetIds); - } } function fixEmptyLabelsForDateCharts(labels, dataSetsValues) { if (labels.length === 0 || labels.every((label) => !label)) { @@ -7701,22 +7177,6 @@ dataSets, }; } - getSheetIdsUsedInChartRanges() { - const sheetIds = new Set(); - const ranges = []; - this.dataSets.map((ds) => ds.dataRange).map((range) => ranges.push(range)); - this.dataSets - .map((ds) => ds.labelCell) - .filter(isDefined$1) - .map((range) => ranges.push(range)); - if (this.labelRange) { - ranges.push(this.labelRange); - } - for (const range of ranges) { - sheetIds.add(range.sheetId); - } - return Array.from(sheetIds); - } updateRanges(applyChange) { const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange); if (!isStale) { @@ -7864,16 +7324,6 @@ : undefined, }; } - getSheetIdsUsedInChartRanges() { - const sheetIds = new Set(); - if (this.baseline) { - sheetIds.add(this.baseline.sheetId); - } - if (this.keyValue) { - sheetIds.add(this.keyValue.sheetId); - } - return Array.from(sheetIds); - } getDefinitionWithSpecificRanges(baseline, keyValue, targetSheetId) { return { baselineColorDown: this.baselineColorDown, @@ -7920,14 +7370,13 @@ const baselineZone = chart.baseline.zone; baselineCell = getters.getCell(chart.baseline.sheetId, baselineZone.left, baselineZone.top); } - const baselineValue = (baselineCell === null || baselineCell === void 0 ? void 0 : baselineCell.content) || ""; const background = getters.getBackgroundOfSingleCellChart(chart.background, chart.keyValue); return { title: chart.title, keyValue: formattedKeyValue || keyValue, - baselineDisplay: getBaselineText(baselineCell, keyValue, chart.baselineMode), - baselineArrow: getBaselineArrowDirection(baselineValue, keyValue, chart.baselineMode), - baselineColor: getBaselineColor(baselineValue, chart.baselineMode, keyValue, chart.baselineColorUp, chart.baselineColorDown), + baselineDisplay: getBaselineText(baselineCell, keyValueCell === null || keyValueCell === void 0 ? void 0 : keyValueCell.evaluated, chart.baselineMode), + baselineArrow: getBaselineArrowDirection(baselineCell === null || baselineCell === void 0 ? void 0 : baselineCell.evaluated, keyValueCell === null || keyValueCell === void 0 ? void 0 : keyValueCell.evaluated, chart.baselineMode), + baselineColor: getBaselineColor(baselineCell === null || baselineCell === void 0 ? void 0 : baselineCell.evaluated, chart.baselineMode, keyValueCell === null || keyValueCell === void 0 ? void 0 : keyValueCell.evaluated, chart.baselineColorUp, chart.baselineColorDown), baselineDescr: chart.baselineDescr, fontColor: chartFontColor(background), background, @@ -9941,15 +9390,6 @@ this.props.onFigureDeleted(); }, }); - registry.add("refresh", { - name: _lt("Refresh"), - sequence: 11, - action: () => { - this.env.model.dispatch("REFRESH_CHART", { - id: this.props.figure.id, - }); - }, - }); return registry; } get chartType() { @@ -10373,6 +9813,502 @@ } } + // HELPERS + function assert(condition, message) { + if (!condition()) { + throw new Error(message); + } + } + // ----------------------------------------------------------------------------- + // FORMAT FUNCTIONS + // ----------------------------------------------------------------------------- + const expectNumberValueError = (value) => _lt("The function [[FUNCTION_NAME]] expects a number value, but '%s' is a string, and cannot be coerced to a number.", value); + function toNumber(value) { + switch (typeof value) { + case "number": + return value; + case "boolean": + return value ? 1 : 0; + case "string": + if (isNumber(value) || value === "") { + return parseNumber(value); + } + const internalDate = parseDateTime(value); + if (internalDate) { + return internalDate.value; + } + throw new Error(expectNumberValueError(value)); + default: + return 0; + } + } + function strictToNumber(value) { + if (value === "") { + throw new Error(expectNumberValueError(value)); + } + return toNumber(value); + } + function toString(value) { + switch (typeof value) { + case "string": + return value; + case "number": + return value.toString(); + case "boolean": + return value ? "TRUE" : "FALSE"; + default: + return ""; + } + } + const expectBooleanValueError = (value) => _lt("The function [[FUNCTION_NAME]] expects a boolean value, but '%s' is a text, and cannot be coerced to a number.", value); + function toBoolean(value) { + switch (typeof value) { + case "boolean": + return value; + case "string": + if (value) { + let uppercaseVal = value.toUpperCase(); + if (uppercaseVal === "TRUE") { + return true; + } + if (uppercaseVal === "FALSE") { + return false; + } + throw new Error(expectBooleanValueError(value)); + } + else { + return false; + } + case "number": + return value ? true : false; + default: + return false; + } + } + function strictToBoolean(value) { + if (value === "") { + throw new Error(expectBooleanValueError(value)); + } + return toBoolean(value); + } + function toJsDate(value) { + return numberToJsDate(toNumber(value)); + } + // ----------------------------------------------------------------------------- + // VISIT FUNCTIONS + // ----------------------------------------------------------------------------- + function visitArgs(args, cellCb, dataCb) { + for (let arg of args) { + if (Array.isArray(arg)) { + // arg is ref to a Cell/Range + const lenRow = arg.length; + const lenCol = arg[0].length; + for (let y = 0; y < lenCol; y++) { + for (let x = 0; x < lenRow; x++) { + cellCb(arg[x][y]); + } + } + } + else { + // arg is set directly in the formula function + dataCb(arg); + } + } + } + function visitAny(args, cb) { + visitArgs(args, cb, cb); + } + function visitNumbers(args, cb) { + visitArgs(args, (cellValue) => { + if (typeof cellValue === "number") { + cb(cellValue); + } + }, (argValue) => { + cb(strictToNumber(argValue)); + }); + } + // ----------------------------------------------------------------------------- + // REDUCE FUNCTIONS + // ----------------------------------------------------------------------------- + function reduceArgs(args, cellCb, dataCb, initialValue) { + let val = initialValue; + for (let arg of args) { + if (Array.isArray(arg)) { + // arg is ref to a Cell/Range + const lenRow = arg.length; + const lenCol = arg[0].length; + for (let y = 0; y < lenCol; y++) { + for (let x = 0; x < lenRow; x++) { + val = cellCb(val, arg[x][y]); + } + } + } + else { + // arg is set directly in the formula function + val = dataCb(val, arg); + } + } + return val; + } + function reduceAny(args, cb, initialValue) { + return reduceArgs(args, cb, cb, initialValue); + } + function reduceNumbers(args, cb, initialValue) { + return reduceArgs(args, (acc, ArgValue) => { + if (typeof ArgValue === "number") { + return cb(acc, ArgValue); + } + return acc; + }, (acc, argValue) => { + return cb(acc, strictToNumber(argValue)); + }, initialValue); + } + function reduceNumbersTextAs0(args, cb, initialValue) { + return reduceArgs(args, (acc, ArgValue) => { + if (ArgValue !== undefined && ArgValue !== null) { + if (typeof ArgValue === "number") { + return cb(acc, ArgValue); + } + else if (typeof ArgValue === "boolean") { + return cb(acc, toNumber(ArgValue)); + } + else { + return cb(acc, 0); + } + } + return acc; + }, (acc, argValue) => { + return cb(acc, toNumber(argValue)); + }, initialValue); + } + // ----------------------------------------------------------------------------- + // CONDITIONAL EXPLORE FUNCTIONS + // ----------------------------------------------------------------------------- + /** + * This function allows to visit arguments and stop the visit if necessary. + * It is mainly used to bypass argument evaluation for functions like OR or AND. + */ + function conditionalVisitArgs(args, cellCb, dataCb) { + for (let arg of args) { + if (Array.isArray(arg)) { + // arg is ref to a Cell/Range + const lenRow = arg.length; + const lenCol = arg[0].length; + for (let y = 0; y < lenCol; y++) { + for (let x = 0; x < lenRow; x++) { + if (!cellCb(arg[x][y])) + return; + } + } + } + else { + // arg is set directly in the formula function + if (!dataCb(arg)) + return; + } + } + } + function conditionalVisitBoolean(args, cb) { + return conditionalVisitArgs(args, (ArgValue) => { + if (typeof ArgValue === "boolean") { + return cb(ArgValue); + } + if (typeof ArgValue === "number") { + return cb(ArgValue ? true : false); + } + return true; + }, (argValue) => { + if (argValue !== undefined && argValue !== null) { + return cb(strictToBoolean(argValue)); + } + return true; + }); + } + function getPredicate(descr, isQuery) { + let operator; + let operand; + let subString = descr.substring(0, 2); + if (subString === "<=" || subString === ">=" || subString === "<>") { + operator = subString; + operand = descr.substring(2); + } + else { + subString = descr.substring(0, 1); + if (subString === "<" || subString === ">" || subString === "=") { + operator = subString; + operand = descr.substring(1); + } + else { + operator = "="; + operand = descr; + } + } + if (isNumber(operand)) { + operand = toNumber(operand); + } + else if (operand === "TRUE" || operand === "FALSE") { + operand = toBoolean(operand); + } + const result = { operator, operand }; + if (typeof operand === "string") { + if (isQuery) { + operand += "*"; + } + result.regexp = operandToRegExp(operand); + } + return result; + } + function operandToRegExp(operand) { + let exp = ""; + let predecessor = ""; + for (let char of operand) { + if (char === "?" && predecessor !== "~") { + exp += "."; + } + else if (char === "*" && predecessor !== "~") { + exp += ".*"; + } + else { + if (char === "*" || char === "?") { + //remove "~" + exp = exp.slice(0, -1); + } + if (["^", ".", "[", "]", "$", "(", ")", "*", "+", "?", "|", "{", "}", "\\"].includes(char)) { + exp += "\\"; + } + exp += char; + } + predecessor = char; + } + return new RegExp("^" + exp + "$", "i"); + } + function evaluatePredicate(value, criterion) { + const { operator, operand } = criterion; + if (value === undefined || operand === undefined) { + return false; + } + if (typeof operand === "number" && operator === "=") { + return toString(value) === toString(operand); + } + if (operator === "<>" || operator === "=") { + let result; + if (typeof value === typeof operand) { + if (typeof value === "string" && criterion.regexp) { + result = criterion.regexp.test(value); + } + else { + result = value === operand; + } + } + else { + result = false; + } + return operator === "=" ? result : !result; + } + if (typeof value === typeof operand) { + switch (operator) { + case "<": + return value < operand; + case ">": + return value > operand; + case "<=": + return value <= operand; + case ">=": + return value >= operand; + } + } + return false; + } + /** + * Functions used especially for predicate evaluation on ranges. + * + * Take ranges with same dimensions and take predicates, one for each range. + * For (i, j) coordinates, if all elements with coordinates (i, j) of each + * range correspond to the associated predicate, then the function uses a callback + * function with the parameters "i" and "j". + * + * Syntax: + * visitMatchingRanges([range1, predicate1, range2, predicate2, ...], cb(i,j), likeSelection) + * + * - range1 (range): The range to check against predicate1. + * - predicate1 (string): The pattern or test to apply to range1. + * - range2: (range, repeatable) ranges to check. + * - predicate2 (string, repeatable): Additional pattern or test to apply to range2. + * + * - cb(i: number, j: number) => void: the callback function. + * + * - isQuery (boolean) indicates if the comparison with a string should be done as a SQL-like query. + * (Ex1 isQuery = true, predicate = "abc", element = "abcde": predicate match the element), + * (Ex2 isQuery = false, predicate = "abc", element = "abcde": predicate not match the element). + * (Ex3 isQuery = true, predicate = "abc", element = "abc": predicate match the element), + * (Ex4 isQuery = false, predicate = "abc", element = "abc": predicate match the element). + */ + function visitMatchingRanges(args, cb, isQuery = false) { + const countArg = args.length; + if (countArg % 2 === 1) { + throw new Error(_lt(`Function [[FUNCTION_NAME]] expects criteria_range and criterion to be in pairs.`)); + } + const dimRow = args[0].length; + const dimCol = args[0][0].length; + let predicates = []; + for (let i = 0; i < countArg - 1; i += 2) { + const criteriaRange = args[i]; + if (!Array.isArray(criteriaRange) || + criteriaRange.length !== dimRow || + criteriaRange[0].length !== dimCol) { + throw new Error(_lt(`Function [[FUNCTION_NAME]] expects criteria_range to have the same dimension`)); + } + const description = toString(args[i + 1]); + predicates.push(getPredicate(description, isQuery)); + } + for (let i = 0; i < dimRow; i++) { + for (let j = 0; j < dimCol; j++) { + let validatedPredicates = true; + for (let k = 0; k < countArg - 1; k += 2) { + const criteriaValue = args[k][i][j]; + const criterion = predicates[k / 2]; + validatedPredicates = evaluatePredicate(criteriaValue, criterion); + if (!validatedPredicates) { + break; + } + } + if (validatedPredicates) { + cb(i, j); + } + } + } + } + // ----------------------------------------------------------------------------- + // COMMON FUNCTIONS + // ----------------------------------------------------------------------------- + /** + * Perform a dichotomic search and return the index of the nearest match less than + * or equal to the target. If all values in the range are greater than the target, + * -1 is returned. + * If the range is not in sorted order, an incorrect value might be returned. + * + * Example: + * - [3, 6, 10], 3 => 0 + * - [3, 6, 10], 6 => 1 + * - [3, 6, 10], 9 => 1 + * - [3, 6, 10], 42 => 2 + * - [3, 6, 10], 2 => -1 + * - [3, undefined, 6, undefined, 10], 9 => 2 + * - [3, 6, undefined, undefined, undefined, 10], 2 => -1 + */ + function dichotomicPredecessorSearch(range, target) { + if (target === null) { + return -1; + } + const targetType = typeof target; + let valMin = undefined; + let valMinIndex = undefined; + let indexLeft = 0; + let indexRight = range.length - 1; + if (typeof range[indexLeft] === targetType && target < range[indexLeft]) { + return -1; + } + if (typeof range[indexRight] === targetType && range[indexRight] <= target) { + return indexRight; + } + let indexMedian; + let currentIndex; + let currentVal; + let currentType; + while (indexRight - indexLeft >= 0) { + indexMedian = Math.ceil((indexLeft + indexRight) / 2); + currentIndex = indexMedian; + currentVal = range[currentIndex]; + currentType = typeof currentVal; + // 1 - linear search to find value with the same type + while (indexLeft <= currentIndex && targetType !== currentType) { + currentIndex--; + currentVal = range[currentIndex]; + currentType = typeof currentVal; + } + // 2 - check if value match + if (currentType === targetType && currentVal <= target) { + if (valMin === undefined || + valMin < currentVal || + (valMin === currentVal && valMinIndex < currentIndex)) { + valMin = currentVal; + valMinIndex = currentIndex; + } + } + // 3 - give new indexs for the Binary search + if (currentType === targetType && currentVal > target) { + indexRight = currentIndex - 1; + } + else { + indexLeft = indexMedian + 1; + } + } + // note that valMinIndex could be 0 + return valMinIndex !== undefined ? valMinIndex : -1; + } + /** + * Perform a dichotomic search and return the index of the nearest match more than + * or equal to the target. If all values in the range are smaller than the target, + * -1 is returned. + * If the range is not in sorted order, an incorrect value might be returned. + * + * Example: + * - [10, 6, 3], 3 => 2 + * - [10, 6, 3], 6 => 1 + * - [10, 6, 3], 9 => 0 + * - [10, 6, 3], 42 => -1 + * - [10, 6, 3], 2 => 2 + * - [10, undefined, 6, undefined, 3], 9 => 0 + * - [10, 6, undefined, undefined, undefined, 3], 2 => 5 + */ + function dichotomicSuccessorSearch(range, target) { + const targetType = typeof target; + let valMax; + let valMaxIndex = undefined; + let indexLeft = 0; + let indexRight = range.length - 1; + if (typeof range[indexLeft] === targetType && target > range[indexLeft]) { + return -1; + } + if (typeof range[indexRight] === targetType && range[indexRight] > target) { + return indexRight; + } + let indexMedian; + let currentIndex; + let currentVal; + let currentType; + while (indexRight - indexLeft >= 0) { + indexMedian = Math.ceil((indexLeft + indexRight) / 2); + currentIndex = indexMedian; + currentVal = range[currentIndex]; + currentType = typeof currentVal; + // 1 - linear search to find value with the same type + while (indexLeft <= currentIndex && targetType !== currentType) { + currentIndex--; + currentVal = range[currentIndex]; + currentType = typeof currentVal; + } + // 2 - check if value match + if (currentType === targetType && currentVal >= target) { + if (valMax === undefined || + valMax > currentVal || + (valMax === currentVal && valMaxIndex > currentIndex)) { + valMax = currentVal; + valMaxIndex = currentIndex; + } + } + // 3 - give new indexs for the Binary search + if (currentType === targetType && currentVal <= target) { + indexRight = currentIndex - 1; + } + else { + indexLeft = indexMedian + 1; + } + } + // note that valMaxIndex could be 0 + return valMaxIndex !== undefined ? valMaxIndex : -1; + } + // ----------------------------------------------------------------------------- // FORMAT.LARGE.NUMBER // ----------------------------------------------------------------------------- @@ -19369,10 +19305,12 @@ } get vScrollbarStyle() { return ` + ${this.env.isDashboard() ? "width: 0px;" : ""} top: ${this.env.isDashboard() ? 0 : HEADER_HEIGHT}px;`; } get hScrollbarStyle() { return ` + ${this.env.isDashboard() ? "width: 0px;" : ""} left: ${this.env.isDashboard() ? 0 : HEADER_WIDTH}px;`; } get cellPopover() { @@ -19416,8 +19354,9 @@ return this.gridEl.getBoundingClientRect(); } resizeGrid() { - const currentHeight = this.gridEl.clientHeight - SCROLLBAR_WIDTH$1; - const currentWidth = this.gridEl.clientWidth - SCROLLBAR_WIDTH$1; + const scrollBarWidth = this.env.isDashboard() ? 0 : SCROLLBAR_WIDTH$1; + const currentHeight = this.gridEl.clientHeight - scrollBarWidth; + const currentWidth = this.gridEl.clientWidth - scrollBarWidth; const { height: viewportHeight, width: viewportWidth } = this.env.model.getters.getViewportDimensionWithHeaders(); if (currentHeight != viewportHeight || currentWidth !== viewportWidth) { this.env.model.dispatch("RESIZE_VIEWPORT", { @@ -19490,7 +19429,13 @@ canvas.width = width * dpr; canvas.height = height * dpr; canvas.setAttribute("style", `width:${width}px;height:${height}px;`); - ctx.translate(-0.5, -0.5); + // Imagine each pixel as a large square. The whole-number coordinates (0, 1, 2…) + // are the edges of the squares. If you draw a one-unit-wide line between whole-number + // coordinates, it will overlap opposite sides of the pixel square, and the resulting + // line will be drawn two pixels wide. To draw a line that is only one pixel wide, + // you need to shift the coordinates by 0.5 perpendicular to the line's direction. + // http://diveintohtml5.info/canvas.html#pixel-madness + ctx.translate(-CANVAS_SHIFT, -CANVAS_SHIFT); ctx.scale(dpr, dpr); this.env.model.drawGrid(renderingContext); } @@ -19822,11 +19767,14 @@ * whatever data they have (formula, string, ...). */ class AbstractCell { - constructor(id, evaluated, properties) { + constructor(id, lazyEvaluated, properties) { this.id = id; this.style = properties.style; this.format = properties.format; - this.evaluated = { ...evaluated, format: evaluated.format || properties.format }; + this.lazyEvaluated = lazyEvaluated.map((evaluated) => ({ + ...evaluated, + format: properties.format || evaluated.format, + })); } isFormula() { return false; @@ -19837,6 +19785,9 @@ isEmpty() { return false; } + get evaluated() { + return this.lazyEvaluated(); + } get formattedValue() { return formatValue(this.evaluated.value, this.evaluated.format); } @@ -19874,7 +19825,7 @@ } class EmptyCell extends AbstractCell { constructor(id, properties = {}) { - super(id, { value: "", type: CellValueType.empty }, properties); + super(id, lazy({ value: "", type: CellValueType.empty }), properties); this.content = ""; } isEmpty() { @@ -19883,7 +19834,7 @@ } class NumberCell extends AbstractCell { constructor(id, value, properties = {}) { - super(id, { value: value, type: CellValueType.number }, properties); + super(id, lazy({ value, type: CellValueType.number }), properties); this.content = formatValue(this.evaluated.value); } get composerContent() { @@ -19896,13 +19847,13 @@ } class BooleanCell extends AbstractCell { constructor(id, value, properties = {}) { - super(id, { value: value, type: CellValueType.boolean }, properties); + super(id, lazy({ value, type: CellValueType.boolean }), properties); this.content = this.evaluated.value ? "TRUE" : "FALSE"; } } class TextCell extends AbstractCell { constructor(id, value, properties = {}) { - super(id, { value: value, type: CellValueType.text }, properties); + super(id, lazy({ value, type: CellValueType.text }), properties); this.content = this.evaluated.value; } } @@ -19930,7 +19881,7 @@ textColor: ((_a = properties.style) === null || _a === void 0 ? void 0 : _a.textColor) || LINK_COLOR, }, }; - super(id, { value: link.label, type: CellValueType.text }, properties); + super(id, lazy({ value: link.label, type: CellValueType.text }), properties); this.link = link; this.content = content; } @@ -19985,7 +19936,7 @@ } class FormulaCell extends AbstractCell { constructor(buildFormulaString, id, compiledFormula, dependencies, properties) { - super(id, { value: LOADING, type: CellValueType.text }, properties); + super(id, lazy({ value: LOADING, type: CellValueType.text }), properties); this.buildFormulaString = buildFormulaString; this.compiledFormula = compiledFormula; this.dependencies = dependencies; @@ -19996,44 +19947,50 @@ isFormula() { return true; } - assignEvaluation(value, format) { - switch (typeof value) { - case "number": - this.evaluated = { - value, - format, - type: CellValueType.number, + assignEvaluation(lazyEvaluationResult) { + this.lazyEvaluated = lazyEvaluationResult.map((evaluationResult) => { + if (evaluationResult instanceof EvaluationError) { + return { + value: evaluationResult.errorType, + type: CellValueType.error, + error: evaluationResult, }; - break; - case "boolean": - this.evaluated = { - value, - format, - type: CellValueType.boolean, - }; - break; - case "string": - this.evaluated = { - value, - format, - type: CellValueType.text, - }; - break; - case "object": // null - this.evaluated = { - value: 0, - format, - type: CellValueType.number, - }; - break; - } - } - assignError(value, error) { - this.evaluated = { - value, - error, - type: CellValueType.error, - }; + } + const { value, format } = evaluationResult; + switch (typeof value) { + case "number": + return { + value, + format, + type: CellValueType.number, + }; + case "boolean": + return { + value, + format, + type: CellValueType.boolean, + }; + case "string": + return { + value, + format, + type: CellValueType.text, + }; + case "object": // null + return { + value: 0, + format, + type: CellValueType.number, + }; + default: + // cannot happen with Typescript compiler watching + // but possible in a vanilla javascript code base + return { + value: "", + type: CellValueType.empty, + }; + } + }); } } /** @@ -20048,11 +20005,11 @@ * @param properties */ constructor(id, content, error, properties) { - super(id, { + super(id, lazy({ value: CellErrorType.BadExpression, type: CellValueType.error, error, - }, properties); + }), properties); this.content = content; } } @@ -24608,10 +24565,7 @@ // TODO this plugin should not care about evaluation // and evaluation should not depend on implementation details here. // Task 2813749 - cell.assignEvaluation(before.evaluated.value, before.evaluated.format); - if (before.evaluated.type === CellValueType.error) { - cell.assignError(before.evaluated.value, before.evaluated.error); - } + cell.assignEvaluation(lazy(before.evaluated)); } this.history.update("cells", sheetId, cell.id, cell); this.dispatch("UPDATE_CELL_POSITION", { cellId: cell.id, col, row, sheetId }); @@ -24729,10 +24683,6 @@ } return definition; } - getSheetIdsUsedInChartRanges(figureId) { - var _a; - return (_a = this.charts[figureId]) === null || _a === void 0 ? void 0 : _a.getSheetIdsUsedInChartRanges(); - } // --------------------------------------------------------------------------- // Import/Export // --------------------------------------------------------------------------- @@ -24822,7 +24772,6 @@ "getChartIds", "getChart", "getContextCreationChart", - "getSheetIdsUsedInChartRanges", ]; // ----------------------------------------------------------------------------- @@ -25307,11 +25256,11 @@ const sizes = { COL: computedSizes.COL.map((size) => ({ manualSize: undefined, - computedSize: lazy(() => size), + computedSize: lazy(size), })), ROW: computedSizes.ROW.map((size) => ({ manualSize: undefined, - computedSize: lazy(() => size), + computedSize: lazy(size), })), }; this.history.update("sizes", cmd.sheetId, sizes); @@ -25369,13 +25318,13 @@ const size = height; this.history.update("sizes", cmd.sheetId, cmd.dimension, el, { manualSize: cmd.size || undefined, - computedSize: lazy(() => size), + computedSize: lazy(size), }); } else { this.history.update("sizes", cmd.sheetId, cmd.dimension, el, { manualSize: cmd.size || undefined, - computedSize: lazy(() => cmd.size || DEFAULT_CELL_WIDTH), + computedSize: lazy(cmd.size || DEFAULT_CELL_WIDTH), }); } } @@ -25392,7 +25341,7 @@ for (let row of range(target.top, target.bottom + 1)) { const rowHeight = this.getRowTallestCellSize(cmd.sheetId, row); if (rowHeight !== this.getRowSize(cmd.sheetId, row)) { - this.history.update("sizes", cmd.sheetId, "ROW", row, "computedSize", lazy(() => rowHeight)); + this.history.update("sizes", cmd.sheetId, "ROW", row, "computedSize", lazy(rowHeight)); } } } @@ -25484,11 +25433,11 @@ this.sizes[sheet.id] = { COL: computedSizes.COL.map((size, i) => ({ manualSize: manualSizes.COL[i], - computedSize: lazy(() => size), + computedSize: lazy(size), })), ROW: computedSizes.ROW.map((size, i) => ({ manualSize: manualSizes.ROW[i], - computedSize: lazy(() => size), + computedSize: lazy(size), })), }; } @@ -28580,7 +28529,7 @@ class EvaluationPlugin extends UIPlugin { constructor(getters, state, dispatch, config, selection) { super(getters, state, dispatch, config, selection); - this.isUpToDate = new Set(); // Set + this.isUpToDate = false; this.evalContext = config.evalContext; } // --------------------------------------------------------------------------- @@ -28588,33 +28537,22 @@ // --------------------------------------------------------------------------- handle(cmd) { if (invalidateEvaluationCommands.has(cmd.type)) { - this.isUpToDate.clear(); + this.isUpToDate = false; } switch (cmd.type) { case "UPDATE_CELL": if ("content" in cmd || "format" in cmd) { - this.isUpToDate.clear(); + this.isUpToDate = false; } break; - case "ACTIVATE_SHEET": { - this.evaluate(cmd.sheetIdTo); - this.isUpToDate.add(cmd.sheetIdTo); - break; - } case "EVALUATE_CELLS": - this.evaluate(cmd.sheetId); - this.isUpToDate.add(cmd.sheetId); - break; - case "EVALUATE_ALL_SHEETS": - this.evaluateAllSheets(); + this.evaluate(); break; } } finalize() { - const sheetId = this.getters.getActiveSheetId(); - if (!this.isUpToDate.has(sheetId)) { - this.evaluate(sheetId); - this.isUpToDate.add(sheetId); + if (!this.isUpToDate) { + this.evaluate(); } } // --------------------------------------------------------------------------- @@ -28652,51 +28590,64 @@ // --------------------------------------------------------------------------- // Evaluator // --------------------------------------------------------------------------- - evaluate(sheetId) { - const cells = this.getters.getCells(sheetId); + *getAllCells() { + // use a generator function to avoid re-building a new object + for (const sheetId of this.getters.getSheetIds()) { + const cells = this.getters.getCells(sheetId); + for (const cellId in cells) { + yield cells[cellId]; + } + } + } + evaluate() { const compilationParameters = this.getCompilationParameters(computeCell); const visited = {}; - for (let cell of Object.values(cells)) { + for (const cell of this.getAllCells()) { computeCell(cell); } - function handleError(e, cell) { + this.isUpToDate = true; + function handleError(e) { if (!(e instanceof Error)) { e = new Error(e); } const msg = (e === null || e === void 0 ? void 0 : e.errorType) || CellErrorType.GenericError; // apply function name const __lastFnCalled = compilationParameters[2].__lastFnCalled || ""; - cell.assignError(msg, new EvaluationError(msg, e.message.replace("[[FUNCTION_NAME]]", __lastFnCalled), e.logLevel !== undefined ? e.logLevel : CellErrorLevel.error)); + return new EvaluationError(msg, e.message.replace("[[FUNCTION_NAME]]", __lastFnCalled), e.logLevel !== undefined ? e.logLevel : CellErrorLevel.error); } function computeCell(cell) { if (!cell.isFormula()) { return; } const cellId = cell.id; - if (cellId in visited) { - if (visited[cellId] === null) { - cell.assignError(CellErrorType.CircularDependency, new CircularDependencyError()); - } - return; - } - visited[cellId] = null; - try { + const computedCell = lazy(() => { compilationParameters[2].__originCellXC = () => { // compute the value lazily for performance reasons const position = compilationParameters[2].getters.getCellPosition(cellId); return toXC(position.col, position.row); }; - const computedCell = cell.compiledFormula.execute(cell.dependencies, ...compilationParameters); - cell.assignEvaluation(computedCell.value, cell.format || computedCell.format); - if (Array.isArray(cell.evaluated.value)) { - // if a value returns an array (like =A1:A3) - throw new Error(_lt("This formula depends on invalid values")); + if (cellId in visited) { + if (visited[cellId] === null) { + return new CircularDependencyError(); + } + return cell.evaluated; } - } - catch (e) { - handleError(e, cell); - } - visited[cellId] = true; + visited[cellId] = null; + try { + const computedCell = cell.compiledFormula.execute(cell.dependencies, ...compilationParameters); + visited[cellId] = true; + if (Array.isArray(computedCell.value)) { + // if a value returns an array (like =A1:A3) + throw new Error(_lt("This formula depends on invalid values")); + } + return { value: computedCell.value, format: cell.format || computedCell.format }; + } + catch (error) { + visited[cellId] = true; + return handleError(error); + } + }); + cell.assignEvaluation(computedCell); } } /** @@ -28725,7 +28676,6 @@ return getEvaluatedCell(cell); } function getEvaluatedCell(cell) { - computeCell(cell); if (cell.evaluated.type === CellValueType.error) { throw new EvaluationError(cell.evaluated.value, cell.evaluated.error.message, cell.evaluated.error.logLevel); } @@ -28793,15 +28743,6 @@ } return [refFn, range, evalContext]; } - /** - * Triggers an evaluation of all cells on all sheets. - */ - evaluateAllSheets() { - for (const sheetId of this.getters.getSheetIds()) { - this.evaluate(sheetId); - this.isUpToDate.add(sheetId); - } - } } EvaluationPlugin.getters = ["evaluateFormula", "getRangeFormattedValues", "getRangeValues"]; @@ -28826,14 +28767,6 @@ case "DELETE_FIGURE": this.charts[cmd.id] = undefined; break; - case "REFRESH_CHART": - this.charts[cmd.id] = undefined; - this.evaluateUsedSheets([cmd.id]); //TODO Lazy evaluation for the win - break; - case "ACTIVATE_SHEET": - const chartsIds = this.getters.getChartIds(cmd.sheetIdTo); - this.evaluateUsedSheets(chartsIds); //TODO Lazy evaluation for the win - break; case "DELETE_SHEET": for (let chartId in this.charts) { if (!this.getters.isChartDefined(chartId)) { @@ -28853,20 +28786,6 @@ } return this.charts[figureId]; } - evaluateUsedSheets(chartsIds) { - const usedSheetsId = new Set(); - for (let chartId of chartsIds) { - const sheetIds = this.getters.getSheetIdsUsedInChartRanges(chartId) || []; - sheetIds.forEach((sheetId) => { - if (sheetId !== this.getters.getActiveSheetId()) { - usedSheetsId.add(sheetId); - } - }); - } - for (let sheetId of usedSheetsId) { - this.dispatch("EVALUATE_CELLS", { sheetId }); - } - } /** * Get the background color of a chart based on the color of the first cell of the main range * of the chart. In order of priority, it will return : @@ -29850,7 +29769,7 @@ const sheetId = this.getters.getActiveSheetId(); // white background ctx.fillStyle = "#ffffff"; - ctx.fillRect(0, 0, width, height); + ctx.fillRect(0, 0, width + CANVAS_SHIFT, height + CANVAS_SHIFT); // background grid const { right, left, top, bottom } = viewport; if (!this.getters.getGridLinesVisibility(sheetId) || this.getters.isDashboard()) { @@ -37507,7 +37426,7 @@ * (e.g. open a document with several sheet and click on download before visiting each sheet) */ exportXLSX() { - this.dispatch("EVALUATE_ALL_SHEETS"); + this.dispatch("EVALUATE_CELLS"); let data = createEmptyExcelWorkbookData(); for (let handler of this.handlers) { if (handler instanceof BasePlugin) { @@ -37637,8 +37556,8 @@ Object.defineProperty(exports, '__esModule', { value: true }); exports.__info__.version = '2.0.0'; - exports.__info__.date = '2022-09-06T12:31:27.223Z'; - exports.__info__.hash = '5a48875'; + exports.__info__.date = '2022-09-09T12:45:57.973Z'; + exports.__info__.hash = 'a5236ad'; })(this.o_spreadsheet = this.o_spreadsheet || {}, owl); -//# sourceMappingURL=o_spreadsheet.js.map +//# sourceMappingURL=o_spreadsheet.js.map \ No newline at end of file diff --git a/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.xml b/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.xml index 68e1cbcff5c..c11d9bd7e20 100644 --- a/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.xml +++ b/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.xml @@ -2948,4 +2948,4 @@ - + \ No newline at end of file diff --git a/addons/spreadsheet/static/tests/currency/currency_test.js b/addons/spreadsheet/static/tests/currency/currency_test.js index 377aca265ac..4024d45fb09 100644 --- a/addons/spreadsheet/static/tests/currency/currency_test.js +++ b/addons/spreadsheet/static/tests/currency/currency_test.js @@ -2,8 +2,7 @@ import { setCellContent } from "@spreadsheet/../tests/utils/commands"; import { getCell, getCellValue } from "@spreadsheet/../tests/utils/getters"; -import { nextTick } from "@web/../tests/helpers/utils"; -import { createModelWithDataSource } from "@spreadsheet/../tests/utils/model"; +import { createModelWithDataSource, waitForDataSourcesLoaded } from "@spreadsheet/../tests/utils/model"; QUnit.module("spreadsheet > Currency"); @@ -18,7 +17,7 @@ QUnit.test("Basic exchange formula", async (assert) => { }); setCellContent(model, "A1", `=ODOO.CURRENCY.RATE("EUR","USD")`); assert.strictEqual(getCellValue(model, "A1"), "Loading..."); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.strictEqual(getCellValue(model, "A1"), 0.9); }); @@ -32,7 +31,7 @@ QUnit.test("Currency rate throw with unknown currency", async (assert) => { }, }); setCellContent(model, "A1", `=ODOO.CURRENCY.RATE("INVALID","USD")`); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.strictEqual(getCell(model, "A1").evaluated.error.message, "Currency rate unavailable."); }); @@ -47,10 +46,10 @@ QUnit.test("Currency rates are only loaded once", async (assert) => { }, }); setCellContent(model, "A1", `=ODOO.CURRENCY.RATE("EUR","USD")`); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.verifySteps(["FETCH"]); setCellContent(model, "A2", `=ODOO.CURRENCY.RATE("EUR","USD")`); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.verifySteps([]); }); @@ -70,6 +69,6 @@ QUnit.test("Currency rates are loaded once by clock", async (assert) => { }); setCellContent(model, "A1", `=ODOO.CURRENCY.RATE("EUR","USD")`); setCellContent(model, "A2", `=ODOO.CURRENCY.RATE("EUR","SEK")`); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.verifySteps(["FETCH:2"]); }); diff --git a/addons/spreadsheet/static/tests/global_filters/global_filters_model_test.js b/addons/spreadsheet/static/tests/global_filters/global_filters_model_test.js index 48df859bab9..632e5ed47f3 100644 --- a/addons/spreadsheet/static/tests/global_filters/global_filters_model_test.js +++ b/addons/spreadsheet/static/tests/global_filters/global_filters_model_test.js @@ -6,6 +6,7 @@ import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended"; import { createModelWithDataSource, setupDataSourceEvaluation, + waitForDataSourcesLoaded, } from "@spreadsheet/../tests/utils/model"; import { createSpreadsheetWithPivotAndList } from "@spreadsheet/../tests/utils/pivot_list"; @@ -891,6 +892,7 @@ QUnit.module("spreadsheet > Global filters model", {}, () => { } }, }); + await waitForDataSourcesLoaded(model); assert.verifySteps([ "partner/read_group", "partner/read_group", @@ -949,6 +951,7 @@ QUnit.module("spreadsheet > Global filters model", {}, () => { } }, }); + await waitForDataSourcesLoaded(model); assert.verifySteps(["partner/read_group"]); assert.equal(getCellValue(model, "A1"), ""); } @@ -984,12 +987,14 @@ QUnit.module("spreadsheet > Global filters model", {}, () => { } }, }); + await waitForDataSourcesLoaded(model); assert.verifySteps([ "partner/read_group", "partner/read_group", "partner/read_group", "partner/read_group", ]); + assert.strictEqual(getCellValue(model, "A1"), 131); model.dispatch("ADD_GLOBAL_FILTER", { filter: { id: "42", @@ -1002,6 +1007,7 @@ QUnit.module("spreadsheet > Global filters model", {}, () => { defaultValue: {}, // no default value! }, }); + assert.strictEqual(getCellValue(model, "A1"), 131); assert.verifySteps([]); }); @@ -1165,7 +1171,9 @@ QUnit.module("spreadsheet > Global filters model", {}, () => { const { model } = await createSpreadsheetWithPivotAndList(); await addGlobalFilter(model, THIS_YEAR_FILTER); const [filter] = model.getters.getGlobalFilters(); - const filterPlugin = model["handlers"].find((handler) => handler instanceof FiltersEvaluationPlugin); + const filterPlugin = model["handlers"].find( + (handler) => handler instanceof FiltersEvaluationPlugin + ); const exportData = { styles: [], sheets: [] }; filterPlugin.exportForExcel(exportData); const filterSheet = exportData.sheets[0]; diff --git a/addons/spreadsheet/static/tests/lists/list_plugin_test.js b/addons/spreadsheet/static/tests/lists/list_plugin_test.js index b3c4485d9f4..3b06079b6dd 100644 --- a/addons/spreadsheet/static/tests/lists/list_plugin_test.js +++ b/addons/spreadsheet/static/tests/lists/list_plugin_test.js @@ -51,7 +51,7 @@ QUnit.module("spreadsheet > list plugin", {}, () => { const { model } = await createSpreadsheetWithList(); setCellContent(model, "A1", `=ODOO.LIST(1,1,"active")`); assert.strictEqual(getCellValue(model, "A1"), "Loading..."); - await nextTick(); // Await for batching collection of missing fields + await waitForDataSourcesLoaded(model); // Await for batching collection of missing fields assert.strictEqual(getCellValue(model, "A1"), true); }); @@ -291,7 +291,7 @@ QUnit.module("spreadsheet > list plugin", {}, () => { uid: 4, }; patchWithCleanup(session, testSession); - await createModelWithDataSource({ + const model = await createModelWithDataSource({ spreadsheetData, mockRPC: function (route, { model, method, kwargs }) { if (model !== "partner") { @@ -309,6 +309,7 @@ QUnit.module("spreadsheet > list plugin", {}, () => { } }, }); + await waitForDataSourcesLoaded(model); assert.verifySteps(["search_read"]); }); @@ -377,16 +378,16 @@ QUnit.module("spreadsheet > list plugin", {}, () => { domain: [["foo", "in", [55]]], }); assert.deepEqual(model.getters.getListDefinition(listId).domain, [["foo", "in", [55]]]); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.strictEqual(getCellValue(model, "B2"), ""); model.dispatch("REQUEST_UNDO"); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.deepEqual(model.getters.getListDefinition(listId).domain, []); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.strictEqual(getCellValue(model, "B2"), "TRUE"); model.dispatch("REQUEST_REDO"); assert.deepEqual(model.getters.getListDefinition(listId).domain, [["foo", "in", [55]]]); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.strictEqual(getCellValue(model, "B2"), ""); }); diff --git a/addons/spreadsheet/static/tests/pivots/model/pivot_plugin_test.js b/addons/spreadsheet/static/tests/pivots/model/pivot_plugin_test.js index d8ab8dc8524..d628dcd322b 100644 --- a/addons/spreadsheet/static/tests/pivots/model/pivot_plugin_test.js +++ b/addons/spreadsheet/static/tests/pivots/model/pivot_plugin_test.js @@ -9,7 +9,10 @@ import { import { createSpreadsheetWithPivot } from "@spreadsheet/../tests/utils/pivot"; import CommandResult from "@spreadsheet/o_spreadsheet/cancelled_reason"; import { setCellContent } from "@spreadsheet/../tests/utils/commands"; -import { createModelWithDataSource } from "@spreadsheet/../tests/utils/model"; +import { + createModelWithDataSource, + waitForDataSourcesLoaded, +} from "@spreadsheet/../tests/utils/model"; import { makeDeferred, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils"; import { session } from "@web/session"; @@ -279,7 +282,7 @@ QUnit.module("spreadsheet > pivot plugin", {}, () => { uid: 4, }; patchWithCleanup(session, testSession); - await createModelWithDataSource({ + const model = await createModelWithDataSource({ spreadsheetData, mockRPC: function (route, { model, method, kwargs }) { if (model !== "partner") { @@ -293,6 +296,7 @@ QUnit.module("spreadsheet > pivot plugin", {}, () => { } }, }); + await waitForDataSourcesLoaded(model); assert.verifySteps(["read_group", "read_group", "read_group", "read_group"]); } ); @@ -329,7 +333,7 @@ QUnit.module("spreadsheet > pivot plugin", {}, () => { }, }, }; - await createModelWithDataSource({ + const model = await createModelWithDataSource({ spreadsheetData, mockRPC: function (route, { model, method, kwargs }) { if (model === "partner" && method === "fields_get") { @@ -339,6 +343,7 @@ QUnit.module("spreadsheet > pivot plugin", {}, () => { } }, }); + await waitForDataSourcesLoaded(model); assert.verifySteps(["partner/fields_get"]); }); @@ -481,7 +486,7 @@ QUnit.module("spreadsheet > pivot plugin", {}, () => { content: `=ODOO.PIVOT.HEADER("1", "product_id", "1111111")`, sheetId, }); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.equal( getCell(model, "E10").evaluated.error.message, "Unable to fetch the label of 1111111 of model product" @@ -501,7 +506,7 @@ QUnit.module("spreadsheet > pivot plugin", {}, () => { }); setCellContent(model, "F10", `=ODOO.PIVOT.HEADER("1", "product_id", A25)`); assert.equal(getCell(model, "A25"), null, "the cell should be empty"); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.equal(getCellValue(model, "F10"), "None"); }); @@ -650,16 +655,16 @@ QUnit.module("spreadsheet > pivot plugin", {}, () => { domain: [["foo", "in", [55]]], }); assert.deepEqual(model.getters.getPivotDefinition(pivotId).domain, [["foo", "in", [55]]]); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.strictEqual(getCellValue(model, "B4"), ""); model.dispatch("REQUEST_UNDO"); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.deepEqual(model.getters.getPivotDefinition(pivotId).domain, []); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.strictEqual(getCellValue(model, "B4"), 11); model.dispatch("REQUEST_REDO"); assert.deepEqual(model.getters.getPivotDefinition(pivotId).domain, [["foo", "in", [55]]]); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.strictEqual(getCellValue(model, "B4"), ""); }); diff --git a/addons/spreadsheet/static/tests/pivots/model/pivot_positional_formula_test.js b/addons/spreadsheet/static/tests/pivots/model/pivot_positional_formula_test.js index 5c8d5a28e5f..29d53d9e7ba 100644 --- a/addons/spreadsheet/static/tests/pivots/model/pivot_positional_formula_test.js +++ b/addons/spreadsheet/static/tests/pivots/model/pivot_positional_formula_test.js @@ -1,10 +1,12 @@ /** @odoo-module */ -import { nextTick } from "@web/../tests/helpers/utils"; import { setCellContent } from "@spreadsheet/../tests/utils/commands"; import { getCell, getCellValue } from "@spreadsheet/../tests/utils/getters"; import { createSpreadsheetWithPivot } from "@spreadsheet/../tests/utils/pivot"; -import { createModelWithDataSource } from "@spreadsheet/../tests/utils/model"; +import { + createModelWithDataSource, + waitForDataSourcesLoaded, +} from "@spreadsheet/../tests/utils/model"; QUnit.module("spreadsheet > positional pivot formula", {}, () => { QUnit.test("Can have positional args in pivot formula", async function (assert) { @@ -132,7 +134,7 @@ QUnit.module("spreadsheet > positional pivot formula", {}, () => { setCellContent(model, "C2", `=ODOO.PIVOT(1,"probability","#bar",2,"#foo",2)`); setCellContent(model, "D1", `=ODOO.PIVOT(1,"probability","#bar",1)`); setCellContent(model, "D2", `=ODOO.PIVOT(1,"probability","#bar",2)`); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.strictEqual(getCellValue(model, "A1"), "No"); assert.strictEqual(getCellValue(model, "A2"), "Yes"); assert.strictEqual(getCellValue(model, "B1"), ""); @@ -170,7 +172,7 @@ QUnit.module("spreadsheet > positional pivot formula", {}, () => { setCellContent(model, "C2", `=ODOO.PIVOT(1,"probability","#bar",2,"#foo",2)`); setCellContent(model, "D1", `=ODOO.PIVOT(1,"probability","#bar",1)`); setCellContent(model, "D2", `=ODOO.PIVOT(1,"probability","#bar",2)`); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.strictEqual(getCellValue(model, "A1"), "Yes"); assert.strictEqual(getCellValue(model, "A2"), "No"); assert.strictEqual(getCellValue(model, "B1"), 11); @@ -209,7 +211,7 @@ QUnit.module("spreadsheet > positional pivot formula", {}, () => { setCellContent(model, "C2", `=ODOO.PIVOT(1,"probability","#bar",2,"#foo",2)`); setCellContent(model, "D1", `=ODOO.PIVOT(1,"probability","#bar",1)`); setCellContent(model, "D2", `=ODOO.PIVOT(1,"probability","#bar",2)`); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.strictEqual(getCellValue(model, "A1"), "Yes"); assert.strictEqual(getCellValue(model, "A2"), "No"); assert.strictEqual(getCellValue(model, "B1"), 11); @@ -248,7 +250,7 @@ QUnit.module("spreadsheet > positional pivot formula", {}, () => { setCellContent(model, "C2", `=ODOO.PIVOT(1,"probability","#bar",2,"#foo",2)`); setCellContent(model, "D1", `=ODOO.PIVOT(1,"probability","#bar",1)`); setCellContent(model, "D2", `=ODOO.PIVOT(1,"probability","#bar",2)`); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.strictEqual(getCellValue(model, "A1"), "No"); assert.strictEqual(getCellValue(model, "A2"), "Yes"); assert.strictEqual(getCellValue(model, "B1"), ""); @@ -284,7 +286,7 @@ QUnit.module("spreadsheet > positional pivot formula", {}, () => { setCellContent(model, "B11", `=ODOO.PIVOT(1,"probability","#product_id",2)`); setCellContent(model, "C10", `=ODOO.PIVOT(1,"foo","#product_id",1)`); setCellContent(model, "C11", `=ODOO.PIVOT(1,"foo","#product_id",2)`); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.strictEqual(getCellValue(model, "A10"), "xphone"); assert.strictEqual(getCellValue(model, "A11"), "xpad"); assert.strictEqual(getCellValue(model, "B10"), 10); @@ -318,7 +320,7 @@ QUnit.module("spreadsheet > positional pivot formula", {}, () => { setCellContent(model, "B11", `=ODOO.PIVOT(1,"probability","#product_id",2)`); setCellContent(model, "C10", `=ODOO.PIVOT(1,"foo","#product_id",1)`); setCellContent(model, "C11", `=ODOO.PIVOT(1,"foo","#product_id",2)`); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.strictEqual(getCellValue(model, "A10"), "xpad"); assert.strictEqual(getCellValue(model, "A11"), "xphone"); assert.strictEqual(getCellValue(model, "B10"), 121); diff --git a/addons/spreadsheet/static/tests/utils/list.js b/addons/spreadsheet/static/tests/utils/list.js index dffb50faf3b..5ea6502ef0b 100644 --- a/addons/spreadsheet/static/tests/utils/list.js +++ b/addons/spreadsheet/static/tests/utils/list.js @@ -1,10 +1,8 @@ /** @odoo-module */ -import { nextTick } from "@web/../tests/helpers/utils"; - import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended"; import { generateListDefinition } from "./data"; -import { createModelWithDataSource } from "./model"; +import { createModelWithDataSource, waitForDataSourcesLoaded } from "./model"; const uuidGenerator = new spreadsheet.helpers.UuidGenerator(); @@ -66,6 +64,6 @@ export async function createSpreadsheetWithList(params = {}) { const env = model.config.evalContext.env; env.model = model; - await nextTick(); + await waitForDataSourcesLoaded(model); return { model, env }; } diff --git a/addons/spreadsheet/static/tests/utils/model.js b/addons/spreadsheet/static/tests/utils/model.js index a19c767cdc5..1f0ff0b6839 100644 --- a/addons/spreadsheet/static/tests/utils/model.js +++ b/addons/spreadsheet/static/tests/utils/model.js @@ -50,6 +50,23 @@ export async function createModelWithDataSource(params = {}) { * @param {Model} model */ export async function waitForDataSourcesLoaded(model) { + function readAllCellsValue() { + for (const sheetId of model.getters.getSheetIds()) { + const cells = model.getters.getCells(sheetId); + for (const cellId in cells) { + cells[cellId].evaluated.value; + } + } + } + // Read a first time in order to trigger the RPC + readAllCellsValue(); //@ts-ignore await model.config.dataSources.waitForAllLoaded(); + await nextTick(); + // Read a second time to trigger the compute format (which could trigger a RPC for currency, in list) + readAllCellsValue(); + await nextTick(); + // Read a third time to trigger the RPC to get the correct currency + readAllCellsValue(); + await nextTick(); } diff --git a/addons/spreadsheet/static/tests/utils/pivot.js b/addons/spreadsheet/static/tests/utils/pivot.js index 15958320c84..1ff20fdf088 100644 --- a/addons/spreadsheet/static/tests/utils/pivot.js +++ b/addons/spreadsheet/static/tests/utils/pivot.js @@ -5,7 +5,7 @@ import { nextTick } from "@web/../tests/helpers/utils"; import PivotDataSource from "@spreadsheet/pivot/pivot_data_source"; import { getBasicServerData } from "./data"; -import { createModelWithDataSource } from "./model"; +import { createModelWithDataSource, waitForDataSourcesLoaded } from "./model"; /** @typedef {import("@spreadsheet/o_spreadsheet/o_spreadsheet").Model} Model */ @@ -70,5 +70,6 @@ export async function createSpreadsheetWithPivot(params = {}) { await insertPivotInSpreadsheet(model, { arch }); const env = model.config.evalContext.env; env.model = model; + await waitForDataSourcesLoaded(model); return { model, env }; } diff --git a/addons/spreadsheet_account/static/tests/model/account_groups_tests.js b/addons/spreadsheet_account/static/tests/model/account_groups_tests.js index fff24e5a220..f551c56b370 100644 --- a/addons/spreadsheet_account/static/tests/model/account_groups_tests.js +++ b/addons/spreadsheet_account/static/tests/model/account_groups_tests.js @@ -1,8 +1,10 @@ /** @odoo-module */ -import { nextTick } from "@web/../tests/helpers/utils"; import { setCellContent } from "@spreadsheet/../tests/utils/commands"; -import { createModelWithDataSource } from "@spreadsheet/../tests/utils/model"; +import { + createModelWithDataSource, + waitForDataSourcesLoaded, +} from "@spreadsheet/../tests/utils/model"; import { getCellValue } from "@spreadsheet/../tests/utils/getters"; import { getAccountingData } from "../accounting_test_data"; @@ -16,21 +18,21 @@ QUnit.module("spreadsheet_account > account groups", { beforeEach }, () => { QUnit.test("get no account", async (assert) => { const model = await createModelWithDataSource({ serverData }); setCellContent(model, "A1", `=ODOO.ACCOUNT.GROUP("test")`); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.equal(getCellValue(model, "A1"), ""); }); QUnit.test("get one account", async (assert) => { const model = await createModelWithDataSource({ serverData }); setCellContent(model, "A1", `=ODOO.ACCOUNT.GROUP("income_other")`); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.equal(getCellValue(model, "A1"), "100105"); }); QUnit.test("get multiple accounts", async (assert) => { const model = await createModelWithDataSource({ serverData }); setCellContent(model, "A1", `=ODOO.ACCOUNT.GROUP("income")`); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.equal(getCellValue(model, "A1"), "100104,200104"); }); }); diff --git a/addons/spreadsheet_account/static/tests/model/accounting_tests.js b/addons/spreadsheet_account/static/tests/model/accounting_tests.js index efa96bf2da4..be770b49ef6 100644 --- a/addons/spreadsheet_account/static/tests/model/accounting_tests.js +++ b/addons/spreadsheet_account/static/tests/model/accounting_tests.js @@ -1,8 +1,10 @@ /** @odoo-module */ -import { nextTick } from "@web/../tests/helpers/utils"; import { setCellContent } from "@spreadsheet/../tests/utils/commands"; -import { createModelWithDataSource } from "@spreadsheet/../tests/utils/model"; +import { + createModelWithDataSource, + waitForDataSourcesLoaded, +} from "@spreadsheet/../tests/utils/model"; import { parseAccountingDate } from "../../src/accounting_functions"; import { getCellValue, getCell } from "@spreadsheet/../tests/utils/getters"; import { getAccountingData } from "../accounting_test_data"; @@ -29,7 +31,7 @@ QUnit.module("spreadsheet_account > Accounting", { beforeEach }, () => { setCellContent(model, "A1", `=ODOO.CREDIT("100", "2022")`); setCellContent(model, "A2", `=ODOO.DEBIT("100", "2022")`); setCellContent(model, "A3", `=ODOO.BALANCE("100", "2022")`); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.equal(getCellValue(model, "A1"), 16); assert.equal(getCellValue(model, "A2"), 42); assert.equal(getCellValue(model, "A3"), 26); @@ -41,7 +43,7 @@ QUnit.module("spreadsheet_account > Accounting", { beforeEach }, () => { setCellContent(model, "A1", `=ODOO.CREDIT("100", "2022")`); setCellContent(model, "A2", `=ODOO.DEBIT("100", "2022")`); setCellContent(model, "A3", `=ODOO.BALANCE("100", "2022")`); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.strictEqual(getCell(model, "A1").evaluated.format, "#,##0.00[$€]"); assert.strictEqual(getCell(model, "A2").evaluated.format, "#,##0.00[$€]"); assert.strictEqual(getCell(model, "A3").evaluated.format, "#,##0.00[$€]"); @@ -56,7 +58,7 @@ QUnit.module("spreadsheet_account > Accounting", { beforeEach }, () => { }, }); setCellContent(model, "A1", `=ODOO.CREDIT("100", "2022", 0, 123456)`); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.strictEqual( getCell(model, "A1").evaluated.error.message, "Currency not available for this company." @@ -72,12 +74,15 @@ QUnit.module("spreadsheet_account > Accounting", { beforeEach }, () => { setCellContent(model, "A5", `=ODOO.BALANCE("100", 1900)`); // this should be ok setCellContent(model, "A6", `=ODOO.BALANCE("100", 1900, -1)`); setCellContent(model, "A7", `=ODOO.DEBIT("100", 1899)`); - await nextTick(); - const errorMessage = `'%s' is not a valid period. Supported formats are "21/12/2022", "Q1/2022", "12/2022", and "2022".` + await waitForDataSourcesLoaded(model); + const errorMessage = `'%s' is not a valid period. Supported formats are "21/12/2022", "Q1/2022", "12/2022", and "2022".`; assert.equal(getCell(model, "A1").evaluated.error.message, "0 is not a valid year."); assert.equal(getCell(model, "A2").evaluated.error.message, "0 is not a valid year."); assert.equal(getCell(model, "A3").evaluated.error.message, "-1 is not a valid year."); - assert.equal(getCell(model, "A4").evaluated.error.message, sprintf(errorMessage, "not a valid period")); + assert.equal( + getCell(model, "A4").evaluated.error.message, + sprintf(errorMessage, "not a valid period") + ); assert.equal(getCell(model, "A5").evaluated.value, 0); assert.equal(getCell(model, "A6").evaluated.error.message, "1899 is not a valid year."); assert.equal(getCell(model, "A7").evaluated.error.message, "1899 is not a valid year."); @@ -98,7 +103,7 @@ QUnit.module("spreadsheet_account > Accounting", { beforeEach }, () => { setCellContent(model, "A1", `=ODOO.CREDIT("100,200", "2022")`); setCellContent(model, "A2", `=ODOO.DEBIT("100,200", "2022")`); setCellContent(model, "A3", `=ODOO.BALANCE("100,200", "2022")`); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.equal(getCellValue(model, "A1"), 26); assert.equal(getCellValue(model, "A2"), 142); assert.equal(getCellValue(model, "A3"), 116); @@ -114,7 +119,7 @@ QUnit.module("spreadsheet_account > Accounting", { beforeEach }, () => { }, }); setCellContent(model, "A1", `=ODOO.CREDIT("100", "2022")`); - await nextTick(); + await waitForDataSourcesLoaded(model); const cell = getCell(model, "A1"); assert.equal(cell.evaluated.value, "#ERROR"); assert.equal(cell.evaluated.error.message, "a nasty error"); @@ -125,7 +130,7 @@ QUnit.module("spreadsheet_account > Accounting", { beforeEach }, () => { mockRPC: async function (route, args) { if (args.method === "spreadsheet_fetch_debit_credit") { const blobs = args.args[0]; - for (let blob of blobs) { + for (const blob of blobs) { assert.step(JSON.stringify(blob)); } } @@ -141,7 +146,7 @@ QUnit.module("spreadsheet_account > Accounting", { beforeEach }, () => { setCellContent(model, "A7", `=ODOO.DEBIT("5", "05/04/2021", 1)`); setCellContent(model, "A8", `=ODOO.BALANCE("5", "2022",,,FALSE)`); setCellContent(model, "A9", `=ODOO.BALANCE("100", "05/05/2022",,,TRUE)`); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.verifySteps([ JSON.stringify( @@ -217,7 +222,7 @@ QUnit.module("spreadsheet_account > Accounting", { beforeEach }, () => { if (args.method === "spreadsheet_fetch_debit_credit") { assert.step("spreadsheet_fetch_debit_credit"); const blobs = args.args[0]; - for (let blob of blobs) { + for (const blob of blobs) { assert.step(JSON.stringify(blob)); } } @@ -226,7 +231,7 @@ QUnit.module("spreadsheet_account > Accounting", { beforeEach }, () => { setCellContent(model, "A1", `=ODOO.BALANCE("100,200", "2022")`); setCellContent(model, "A2", `=ODOO.CREDIT("100,200", "2022")`); setCellContent(model, "A3", `=ODOO.DEBIT("100,200","2022")`); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.verifySteps([ "spreadsheet_fetch_debit_credit", @@ -256,7 +261,7 @@ QUnit.module("spreadsheet_account > Accounting", { beforeEach }, () => { if (args.method === "spreadsheet_fetch_debit_credit") { assert.step("spreadsheet_fetch_debit_credit"); const blobs = args.args[0]; - for (let blob of blobs) { + for (const blob of blobs) { assert.step(JSON.stringify(blob)); } } @@ -266,7 +271,7 @@ QUnit.module("spreadsheet_account > Accounting", { beforeEach }, () => { setCellContent(model, "A2", `=ODOO.BALANCE(A1, 2022)`); assert.equal(getCellValue(model, "A1"), "Loading..."); assert.equal(getCellValue(model, "A2"), "Loading..."); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.equal(getCellValue(model, "A1"), "100104,200104"); assert.equal(getCellValue(model, "A2"), 0); assert.verifySteps([ @@ -297,7 +302,7 @@ QUnit.module("spreadsheet_account > Accounting", { beforeEach }, () => { if (args.method === "spreadsheet_fetch_debit_credit") { assert.step("spreadsheet_fetch_debit_credit"); const blobs = args.args[0]; - for (let blob of blobs) { + for (const blob of blobs) { assert.step(JSON.stringify(blob)); } } @@ -313,7 +318,7 @@ QUnit.module("spreadsheet_account > Accounting", { beforeEach }, () => { // Because cells are evaluated given their order in the sheet, // A1's request is done first, meaning it's also resolved first, which add A2 to the next batch (synchronously) // Only then A3 is resolved. => A2 is batched while A3 is pending - await nextTick(); + await waitForDataSourcesLoaded(model); assert.equal(getCellValue(model, "A1"), "100104,200104"); assert.equal(getCellValue(model, "A2"), 0); assert.equal(getCellValue(model, "A3"), 0); diff --git a/addons/spreadsheet_account/static/tests/model/fiscal_year_tests.js b/addons/spreadsheet_account/static/tests/model/fiscal_year_tests.js index 6ed8d9c3c98..9d644946f61 100644 --- a/addons/spreadsheet_account/static/tests/model/fiscal_year_tests.js +++ b/addons/spreadsheet_account/static/tests/model/fiscal_year_tests.js @@ -1,7 +1,9 @@ /** @odoo-module */ -import { nextTick } from "@web/../tests/helpers/utils"; import { setCellContent } from "@spreadsheet/../tests/utils/commands"; -import { createModelWithDataSource } from "@spreadsheet/../tests/utils/model"; +import { + createModelWithDataSource, + waitForDataSourcesLoaded, +} from "@spreadsheet/../tests/utils/model"; import { getCell } from "@spreadsheet/../tests/utils/getters"; import "@spreadsheet_account/index"; @@ -25,7 +27,7 @@ QUnit.module("spreadsheet_account > fiscal year", {}, () => { }); setCellContent(model, "A1", `=ODOO.FISCALYEAR.START("11/11/2020")`); setCellContent(model, "A2", `=ODOO.FISCALYEAR.END("11/11/2020")`); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.verifySteps(["get_fiscal_dates"]); assert.equal(getCell(model, "A1").formattedValue, "1/1/2020"); assert.equal(getCell(model, "A2").formattedValue, "12/31/2020"); @@ -50,7 +52,7 @@ QUnit.module("spreadsheet_account > fiscal year", {}, () => { }); setCellContent(model, "A1", `=ODOO.FISCALYEAR.START("11/11/2020", 1)`); setCellContent(model, "A2", `=ODOO.FISCALYEAR.END("11/11/2020", 1)`); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.verifySteps(["get_fiscal_dates"]); assert.equal(getCell(model, "A1").formattedValue, "1/1/2020"); assert.equal(getCell(model, "A2").formattedValue, "12/31/2020"); @@ -75,7 +77,7 @@ QUnit.module("spreadsheet_account > fiscal year", {}, () => { }); setCellContent(model, "A1", `=ODOO.FISCALYEAR.START("11/11/2020", 100)`); setCellContent(model, "A2", `=ODOO.FISCALYEAR.END("11/11/2020", 100)`); - await nextTick(); + await waitForDataSourcesLoaded(model); assert.verifySteps(["get_fiscal_dates"]); assert.equal( getCell(model, "A1").evaluated.error.message, diff --git a/addons/spreadsheet_account/static/tests/ui/accounting_drilldown_tests.js b/addons/spreadsheet_account/static/tests/ui/accounting_drilldown_tests.js index d9d9d0f513e..b3a44dbc77d 100644 --- a/addons/spreadsheet_account/static/tests/ui/accounting_drilldown_tests.js +++ b/addons/spreadsheet_account/static/tests/ui/accounting_drilldown_tests.js @@ -1,10 +1,12 @@ /** @odoo-module */ -import { nextTick } from "@web/../tests/helpers/utils"; import { selectCell, setCellContent } from "@spreadsheet/../tests/utils/commands"; import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended"; import { getAccountingData } from "../accounting_test_data"; -import { createModelWithDataSource } from "@spreadsheet/../tests/utils/model"; +import { + createModelWithDataSource, + waitForDataSourcesLoaded, +} from "@spreadsheet/../tests/utils/model"; import { registry } from "@web/core/registry"; const { cellMenuRegistry } = spreadsheet.registries; @@ -64,7 +66,7 @@ QUnit.module("spreadsheet_account > Accounting Drill down", { beforeEach }, () = env.model = model; setCellContent(model, "A1", `=ODOO.BALANCE("100", 2020)`); setCellContent(model, "A2", `=ODOO.BALANCE("100", 0)`); - await nextTick(); + await waitForDataSourcesLoaded(model); selectCell(model, "A1"); const root = cellMenuRegistry.getAll().find((item) => item.id === "move_lines_see_records"); assert.equal(root.isVisible(env), true);