[FIX] spreadsheet: update o_spreadsheet to latest version

https://github.com/odoo/o-spreadsheet/commit/8bd0fb2b4 [REL] 17.0.3
https://github.com/odoo/o-spreadsheet/commit/fe3ba2a88 [FIX] text_helper: strikethrough wrong positioning
https://github.com/odoo/o-spreadsheet/commit/742581e80 [IMP] demo: add demo data for data validation
https://github.com/odoo/o-spreadsheet/commit/0323dd37a [FIX] FiltersPlugin: removing row with data filter header Task: 3546012
https://github.com/odoo/o-spreadsheet/commit/fc2a17482 [FIX] FilterEvaluationPlugin: insert/remove rows on data filter header Task: 3546012
https://github.com/odoo/o-spreadsheet/commit/3c3bbe9a2 [MOV] tests: fix typo in test file name of context menu
https://github.com/odoo/o-spreadsheet/commit/c53528f7c [FIX] F&R: Fix css rule
https://github.com/odoo/o-spreadsheet/commit/b6fa9634f [FIX] Menus: Show Insert col/row menu when full sheet selected Task: 3450188
https://github.com/odoo/o-spreadsheet/commit/80bd3b30f [REV] Charts: Do not rely on history for the chartRuntimes Task: 3578417
https://github.com/odoo/o-spreadsheet/commit/3d6f4f954 [IMP] evaluation: improve perfs of `getCorrespondingFormulaCell` Task: 3584306
https://github.com/odoo/o-spreadsheet/commit/17ba12211 [FIX] cell: update cell tokens and dependencies Task: 3584306
https://github.com/odoo/o-spreadsheet/commit/c0cb4cf86 [FIX] find_and_replace: take array formula result into account Task: 3413999

[IMP] spreadsheet: remove useless tokenize calls

The pivot/list helpers were tokenizing the cell's content when trying
to find list/pivot formulas. But we can skip the tokenize call as we
already have the tokens in the cell's compiledFormula.

closes odoo/odoo#142323

Task: 3584306
Related: odoo/enterprise#50888
Signed-off-by: Adrien Minne (adrm) <adrm@odoo.com>
Co-authored-by: Adrien Minne <adrm@odoo.com>
This commit is contained in:
Lucas Lefèvre (lul)
2023-11-19 23:55:11 +00:00
co-authored by Adrien Minne
parent 22eb8e7b6a
commit 037e5b823b
14 changed files with 264 additions and 168 deletions
@@ -2,33 +2,37 @@
import * as spreadsheet from "@odoo/o-spreadsheet";
const { parse, iterateAstNodes } = spreadsheet;
const { parseTokens, iterateAstNodes } = spreadsheet;
/**
* @typedef {Object} OdooFunctionDescription
* @property {string} functionName Name of the function
* @property {Array<string>} args Arguments of the function
*
* @typedef {Object} Token
* @property {string} type
* @property {string} value
*/
/**
* This function is used to search for the functions which match the given matcher
* from the given formula
*
* @param {string} formula
* @param {Token[]} tokens
* @param {string[]} functionNames e.g. ["ODOO.LIST", "ODOO.LIST.HEADER"]
* @private
* @returns {Array<OdooFunctionDescription>}
*/
export function getOdooFunctions(formula, functionNames) {
const formulaUpperCased = formula.toUpperCase();
export function getOdooFunctions(tokens, functionNames) {
// Parsing is an expensive operation, so we first check if the
// formula contains one of the function names
if (!functionNames.some((fn) => formulaUpperCased.includes(fn.toUpperCase()))) {
if (!tokens.some((t) => t.type === "SYMBOL" && functionNames.includes(t.value.toUpperCase()))) {
return [];
}
let ast;
try {
ast = parse(formula);
ast = parseTokens(tokens);
} catch {
return [];
}
@@ -9,7 +9,7 @@ export const SEE_RECORD_LIST = async (position, env) => {
if (!cell) {
return;
}
const { args } = getFirstListFunction(cell.content);
const { args } = getFirstListFunction(cell.compiledFormula.tokens);
const evaluatedArgs = args
.map(astToFormula)
.map((arg) => env.model.getters.evaluateFormula(sheetId, arg));
@@ -36,7 +36,8 @@ export const SEE_RECORD_LIST_VISIBLE = (position, env) => {
evaluatedCell.type !== "empty" &&
evaluatedCell.type !== "error" &&
cell &&
getNumberOfListFormulas(cell.content) === 1 &&
getFirstListFunction(cell.content).functionName === "ODOO.LIST"
cell.isFormula &&
getNumberOfListFormulas(cell.compiledFormula.tokens) === 1 &&
getFirstListFunction(cell.compiledFormula.tokens).functionName === "ODOO.LIST"
);
};
@@ -2,25 +2,27 @@
import { getOdooFunctions } from "../helpers/odoo_functions_helpers";
/** @typedef {import("@spreadsheet/helpers/odoo_functions_helpers").Token} Token */
/**
* Parse a spreadsheet formula and detect the number of LIST functions that are
* present in the given formula.
*
* @param {string} formula
* @param {Token[]} tokens
*
* @returns {number}
*/
export function getNumberOfListFormulas(formula) {
return getOdooFunctions(formula, ["ODOO.LIST", "ODOO.LIST.HEADER"]).length;
export function getNumberOfListFormulas(tokens) {
return getOdooFunctions(tokens, ["ODOO.LIST", "ODOO.LIST.HEADER"]).length;
}
/**
* Get the first List function description of the given formula.
*
* @param {string} formula
* @param {Token[]} tokens
*
* @returns {import("../helpers/odoo_functions_helpers").OdooFunctionDescription|undefined}
*/
export function getFirstListFunction(formula) {
return getOdooFunctions(formula, ["ODOO.LIST", "ODOO.LIST.HEADER"])[0];
export function getFirstListFunction(tokens) {
return getOdooFunctions(tokens, ["ODOO.LIST", "ODOO.LIST.HEADER"])[0];
}
@@ -53,7 +53,7 @@ export class ListUIPlugin extends spreadsheet.UIPlugin {
const cells = this.getters.getCells(sheetId);
for (const cell of Object.values(cells)) {
if (cell.isFormula) {
this._addListPositionToDataSource(cell.content);
this._addListPositionToDataSource(cell);
}
}
}
@@ -87,7 +87,11 @@ export class ListUIPlugin extends spreadsheet.UIPlugin {
}
case "UPDATE_CELL":
if (cmd.content) {
this._addListPositionToDataSource(cmd.content);
const position = { sheetId: cmd.sheetId, col: cmd.col, row: cmd.row };
const cell = this.getters.getCell(position);
if (cell && cell.isFormula) {
this._addListPositionToDataSource(cell);
}
}
break;
case "UNDO":
@@ -204,11 +208,11 @@ export class ListUIPlugin extends spreadsheet.UIPlugin {
*
* @param {string} content Odoo list formula
*/
_addListPositionToDataSource(content) {
if (getNumberOfListFormulas(content) !== 1) {
_addListPositionToDataSource(cell) {
if (getNumberOfListFormulas(cell.compiledFormula.tokens) !== 1) {
return;
}
const { functionName, args } = getFirstListFunction(content);
const { functionName, args } = getFirstListFunction(cell.compiledFormula.tokens);
if (functionName !== "ODOO.LIST") {
return;
}
@@ -254,7 +258,7 @@ export class ListUIPlugin extends spreadsheet.UIPlugin {
const cell = this.getters.getCell(position);
const sheetId = position.sheetId;
if (cell && cell.isFormula) {
const listFunction = getFirstListFunction(cell.content);
const listFunction = getFirstListFunction(cell.compiledFormula.tokens);
if (listFunction) {
const content = astToFormula(listFunction.args[0]);
return this.getters.evaluateFormula(sheetId, content).toString();
@@ -2,9 +2,9 @@
/**
* This file is generated by o-spreadsheet build tools. Do not edit it.
* @see https://github.com/odoo/o-spreadsheet
* @version 17.0.2
* @date 2023-11-03T12:32:51.249Z
* @hash 3184728ea
* @version 17.0.3
* @date 2023-11-16T13:27:10.083Z
* @hash 8bd0fb2b4
*/
(function (exports, owl) {
@@ -4541,14 +4541,14 @@
switch (context.textBaseline) {
case "top":
underlineY += boxHeight - 2 * strokeWidth;
strikeY += boxHeight - textHeight;
strikeY += boxHeight / 2 - strokeWidth;
break;
case "middle":
underlineY += boxHeight / 2 - strokeWidth;
break;
case "alphabetic":
underlineY += 2 * strokeWidth;
strikeY -= textHeight / 2 - strokeWidth / 2;
strikeY -= 3 * strokeWidth;
break;
case "bottom":
underlineY = y;
@@ -5967,7 +5967,7 @@
return { cellData: {} };
}
const sheetId = data.sheetId;
const content = getters.getTranslatedCellFormula(sheetId, x, y, cell.compiledFormula, cell.dependencies);
const content = getters.getTranslatedCellFormula(sheetId, x, y, cell.compiledFormula);
return {
cellData: {
border: data.border,
@@ -10694,6 +10694,17 @@
const IS_ONLY_ONE_RANGE = (env) => {
return env.model.getters.getSelectedZones().length === 1;
};
const CAN_INSERT_HEADER = (env, dimension) => {
if (!IS_ONLY_ONE_RANGE(env)) {
return false;
}
const activeHeaders = dimension === "COL" ? env.model.getters.getActiveCols() : env.model.getters.getActiveRows();
const ortogonalActiveHeaders = dimension === "COL" ? env.model.getters.getActiveRows() : env.model.getters.getActiveCols();
const sheetId = env.model.getters.getActiveSheetId();
const zone = env.model.getters.getSelectedZone();
const allSheetSelected = isEqual(zone, env.model.getters.getSheetZone(sheetId));
return isConsecutive(activeHeaders) && (ortogonalActiveHeaders.size === 0 || allSheetSelected);
};
const undo = {
name: _t("Undo"),
@@ -19371,9 +19382,7 @@
const number = getRowsNumber(env);
return number === 1 ? _t("Insert row") : _t("Insert %s rows", number.toString());
},
isVisible: (env) => isConsecutive(env.model.getters.getActiveRows()) &&
IS_ONLY_ONE_RANGE(env) &&
env.model.getters.getActiveCols().size === 0,
isVisible: (env) => CAN_INSERT_HEADER(env, "ROW"),
icon: "o-spreadsheet-Icon.INSERT_ROW",
};
const rowInsertRowBefore = {
@@ -19382,9 +19391,7 @@
return number === 1 ? _t("Insert row above") : _t("Insert %s rows above", number.toString());
},
execute: INSERT_ROWS_BEFORE_ACTION,
isVisible: (env) => isConsecutive(env.model.getters.getActiveRows()) &&
IS_ONLY_ONE_RANGE(env) &&
env.model.getters.getActiveCols().size === 0,
isVisible: (env) => CAN_INSERT_HEADER(env, "ROW"),
icon: "o-spreadsheet-Icon.INSERT_ROW_BEFORE",
};
const topBarInsertRowsBefore = {
@@ -19415,9 +19422,7 @@
const number = getRowsNumber(env);
return number === 1 ? _t("Insert row below") : _t("Insert %s rows below", number.toString());
},
isVisible: (env) => isConsecutive(env.model.getters.getActiveRows()) &&
IS_ONLY_ONE_RANGE(env) &&
env.model.getters.getActiveCols().size === 0,
isVisible: (env) => CAN_INSERT_HEADER(env, "ROW"),
icon: "o-spreadsheet-Icon.INSERT_ROW_AFTER",
};
const topBarInsertRowsAfter = {
@@ -19435,9 +19440,7 @@
const number = getColumnsNumber(env);
return number === 1 ? _t("Insert column") : _t("Insert %s columns", number.toString());
},
isVisible: (env) => isConsecutive(env.model.getters.getActiveCols()) &&
IS_ONLY_ONE_RANGE(env) &&
env.model.getters.getActiveRows().size === 0,
isVisible: (env) => CAN_INSERT_HEADER(env, "COL"),
icon: "o-spreadsheet-Icon.INSERT_COL",
};
const colInsertColsBefore = {
@@ -19448,9 +19451,7 @@
: _t("Insert %s columns left", number.toString());
},
execute: INSERT_COLUMNS_BEFORE_ACTION,
isVisible: (env) => isConsecutive(env.model.getters.getActiveCols()) &&
IS_ONLY_ONE_RANGE(env) &&
env.model.getters.getActiveRows().size === 0,
isVisible: (env) => CAN_INSERT_HEADER(env, "COL"),
icon: "o-spreadsheet-Icon.INSERT_COL_BEFORE",
};
const topBarInsertColsBefore = {
@@ -19483,9 +19484,7 @@
: _t("Insert %s columns right", number.toString());
},
execute: INSERT_COLUMNS_AFTER_ACTION,
isVisible: (env) => isConsecutive(env.model.getters.getActiveCols()) &&
IS_ONLY_ONE_RANGE(env) &&
env.model.getters.getActiveRows().size === 0,
isVisible: (env) => CAN_INSERT_HEADER(env, "COL"),
icon: "o-spreadsheet-Icon.INSERT_COL_AFTER",
};
const topBarInsertColsAfter = {
@@ -23826,7 +23825,7 @@
}
.o-input-count {
width: fit-content;
padding: 4 0 4 4;
padding: 4px 0 4px 4px;
}
}
}
@@ -34947,6 +34946,9 @@
// -----------------------------------------------------------------------------
function compile(formula) {
const tokens = rangeTokenize(formula);
return compileTokens(tokens);
}
function compileTokens(tokens) {
const { dependencies, constantValues } = formulaArguments(tokens);
const cacheKey = compilationCacheKey(tokens, dependencies, constantValues);
if (!functionCache[cacheKey]) {
@@ -35313,11 +35315,11 @@
for (const sheet of Object.keys(this.cells)) {
for (const cell of Object.values(this.cells[sheet] || {})) {
if (cell.isFormula) {
for (const range of cell.dependencies) {
for (const range of cell.compiledFormula.dependencies) {
if (!sheetId || range.sheetId === sheetId) {
const change = applyChange(range);
if (change.changeType !== "NONE") {
this.history.update("cells", sheet, cell.id, "dependencies", cell.dependencies.indexOf(range), change.range);
this.history.update("cells", sheet, cell.id, "compiledFormula", "dependencies", cell.compiledFormula.dependencies.indexOf(range), change.range);
}
}
}
@@ -35513,10 +35515,10 @@
/*
* Reconstructs the original formula string based on a normalized form and its dependencies
*/
getFormulaCellContent(sheetId, cell, dependencies) {
const ranges = dependencies || cell.dependencies;
getFormulaCellContent(sheetId, compiledFormula, dependencies) {
const ranges = dependencies || compiledFormula.dependencies;
let rangeIndex = 0;
return concat(cell.compiledFormula.tokens.map((token) => {
return concat(compiledFormula.tokens.map((token) => {
if (token.type === "REFERENCE") {
const range = ranges[rangeIndex++];
return this.getters.getRangeString(range, sheetId);
@@ -35527,12 +35529,9 @@
/*
* Constructs a formula string based on an initial formula and a translation vector
*/
getTranslatedCellFormula(sheetId, offsetX, offsetY, compiledFormula, dependencies) {
const adaptedDependencies = this.getters.createAdaptedRanges(dependencies, offsetX, offsetY, sheetId);
return this.getFormulaCellContent(sheetId, {
compiledFormula,
dependencies: adaptedDependencies,
});
getTranslatedCellFormula(sheetId, offsetX, offsetY, compiledFormula) {
const adaptedDependencies = this.getters.createAdaptedRanges(compiledFormula.dependencies, offsetX, offsetY, sheetId);
return this.getFormulaCellContent(sheetId, compiledFormula, adaptedDependencies);
}
getCellStyle(position) {
return this.getters.getCell(position)?.style || {};
@@ -35712,8 +35711,10 @@
style,
format,
isFormula: true,
compiledFormula,
dependencies: [],
compiledFormula: {
...compiledFormula,
dependencies: [],
},
};
}
/**
@@ -35722,7 +35723,7 @@
*/
createFormulaCellWithDependencies(id, compiledFormula, format, style, sheetId) {
const dependencies = compiledFormula.dependencies.map((xc) => this.getters.getRangeFromSheetXC(sheetId, xc));
return new FormulaCellWithDependencies(id, compiledFormula, format, style, dependencies, sheetId, this.getFormulaCellContent.bind(this));
return new FormulaCellWithDependencies(id, compiledFormula, format, style, dependencies, sheetId, this.getters.getRangeString.bind(this));
}
createErrorFormula(id, content, format, style, error) {
return {
@@ -35732,13 +35733,12 @@
format,
isFormula: true,
compiledFormula: {
dependencies: [],
tokens: tokenize(content),
dependencies: [],
execute: function () {
throw error;
},
},
dependencies: [],
};
}
checkCellOutOfSheet(sheetId, col, row) {
@@ -35751,27 +35751,44 @@
}
class FormulaCellWithDependencies {
id;
compiledFormula;
format;
style;
dependencies;
sheetId;
getFormulaCellContent;
getRangeString;
isFormula = true;
constructor(id, compiledFormula, format, style, dependencies, sheetId, getFormulaCellContent) {
compiledFormula;
constructor(id, compiledFormula, format, style, dependencies, sheetId, getRangeString) {
this.id = id;
this.compiledFormula = compiledFormula;
this.format = format;
this.style = style;
this.dependencies = dependencies;
this.sheetId = sheetId;
this.getFormulaCellContent = getFormulaCellContent;
this.getRangeString = getRangeString;
let rangeIndex = 0;
const tokens = compiledFormula.tokens.map((token) => {
if (token.type === "REFERENCE") {
const index = rangeIndex++;
return new RangeReferenceToken(() => this.getRangeString(dependencies[index], this.sheetId));
}
return token;
});
this.compiledFormula = {
...compiledFormula,
dependencies,
tokens,
};
}
get content() {
return this.getFormulaCellContent(this.sheetId, {
dependencies: this.dependencies,
compiledFormula: this.compiledFormula,
});
return concat(this.compiledFormula.tokens.map((token) => token.value));
}
}
class RangeReferenceToken {
getValue;
type = "REFERENCE";
constructor(getValue) {
this.getValue = getValue;
}
get value() {
return this.getValue();
}
}
@@ -36916,6 +36933,13 @@
}
onDeleteColumnsRows(cmd) {
for (const table of this.getFilterTables(cmd.sheetId)) {
// Remove the filter tables whose data filter headers are in the removed rows.
if (cmd.dimension === "ROW" && cmd.elements.includes(table.zone.top)) {
const tables = { ...this.tables[cmd.sheetId] };
delete tables[table.id];
this.history.update("tables", cmd.sheetId, tables);
continue;
}
const zone = reduceZoneOnDeletion(table.zone, cmd.dimension === "COL" ? "left" : "top", cmd.elements);
if (!zone) {
const tables = { ...this.tables[cmd.sheetId] };
@@ -39964,6 +39988,13 @@
return (this.evaluatedCells.get(this.encodePosition(position)) ||
createEvaluatedCell("", { locale: this.getters.getLocale() }));
}
getSpreadPositionsOf(position) {
const positionId = this.encodePosition(position);
if (!this.spreadingRelations.isArrayFormula(positionId)) {
return [];
}
return Array.from(this.spreadingRelations.getArrayResultPositionIds(positionId)).map(this.decodePosition.bind(this));
}
getArrayFormulaSpreadingOn(position) {
const positionId = this.encodePosition(position);
const formulaPosition = this.getArrayFormulaSpreadingOnId(positionId);
@@ -40122,7 +40153,7 @@
const position = this.compilationParams[2].getters.getCellPosition(cellId);
return toXC(position.col, position.row);
};
const formulaReturn = cellData.compiledFormula.execute(cellData.dependencies, ...this.compilationParams);
const formulaReturn = cellData.compiledFormula.execute(cellData.compiledFormula.dependencies, ...this.compilationParams);
if (!isMatrix(formulaReturn)) {
return createEvaluatedCell(formulaReturn.value, {
format: cellData.format || formulaReturn.format,
@@ -40222,7 +40253,7 @@
return [];
}
const dependencies = [];
for (const range of cell.dependencies) {
for (const range of cell.compiledFormula.dependencies) {
if (range.invalidSheetName || range.invalidXc) {
continue;
}
@@ -40428,6 +40459,7 @@
"getEvaluatedCell",
"getEvaluatedCells",
"getEvaluatedCellsInZone",
"getSpreadPositionsOf",
];
shouldRebuildDependenciesGraph = true;
evaluator;
@@ -40537,6 +40569,9 @@
getEvaluatedCellsInZone(sheetId, zone) {
return positions(zone).map(({ col, row }) => this.getters.getEvaluatedCell({ sheetId, col, row }));
}
getSpreadPositionsOf(position) {
return this.evaluator.getSpreadPositionsOf(position);
}
// ---------------------------------------------------------------------------
// Export
// ---------------------------------------------------------------------------
@@ -40574,10 +40609,10 @@
*/
getCorrespondingFormulaCell(position) {
const cell = this.getters.getCell(position);
if (cell && cell.content) {
if (cell.isFormula && !isBadExpression(cell.content)) {
return cell;
}
if (cell && cell.isFormula) {
return isBadExpression(cell.compiledFormula.tokens) ? undefined : cell;
}
else if (cell && cell.content) {
return undefined;
}
const spreadingFormulaPosition = this.evaluator.getArrayFormulaSpreadingOn(position);
@@ -40594,9 +40629,9 @@
return buildCompilationParameters(this.config.custom, this.getters, (position) => this.evaluator.getEvaluatedCell(position));
}
}
function isBadExpression(formula) {
function isBadExpression(tokens) {
try {
compile(formula);
compileTokens(tokens);
return false;
}
catch (error) {
@@ -40765,22 +40800,20 @@
invalidateCFEvaluationCommands.has(cmd.type) ||
cmd.type === "EVALUATE_CELLS" ||
cmd.type === "UPDATE_CELL") {
if (cmd.type !== "UNDO" && cmd.type !== "REDO") {
for (const chartId in this.charts) {
this.history.update("charts", chartId, undefined);
}
for (const chartId in this.charts) {
this.charts[chartId] = undefined;
}
}
switch (cmd.type) {
case "UPDATE_CHART":
case "CREATE_CHART":
case "DELETE_FIGURE":
this.history.update("charts", cmd.id, undefined);
this.charts[cmd.id] = undefined;
break;
case "DELETE_SHEET":
for (let chartId in this.charts) {
if (!this.getters.isChartDefined(chartId)) {
this.history.update("charts", chartId, undefined);
this.charts[chartId] = undefined;
}
}
break;
@@ -40792,8 +40825,7 @@
if (!chart) {
throw new Error(`No chart for the given id: ${figureId}`);
}
const runtime = this.createRuntimeChart(chart);
this.history.update("charts", figureId, runtime);
this.charts[figureId] = this.createRuntimeChart(chart);
}
return this.charts[figureId];
}
@@ -40945,7 +40977,10 @@
const values = cf.rule.values.map((value, i) => {
const compiledFormula = formulas[i];
if (compiledFormula) {
return this.getters.getTranslatedCellFormula(sheetId, col - zone.left, row - zone.top, compiledFormula, compiledFormula.dependencies.map((d) => this.getters.getRangeFromSheetXC(sheetId, d)));
return this.getters.getTranslatedCellFormula(sheetId, col - zone.left, row - zone.top, {
...compiledFormula,
dependencies: compiledFormula.dependencies.map((d) => this.getters.getRangeFromSheetXC(sheetId, d)),
});
}
return value;
});
@@ -41317,7 +41352,10 @@
}
try {
const formula = compile(value);
const translatedFormula = this.getters.getTranslatedCellFormula(sheetId, offset.col, offset.row, formula, formula.dependencies.map((d) => this.getters.getRangeFromSheetXC(sheetId, d)));
const translatedFormula = this.getters.getTranslatedCellFormula(sheetId, offset.col, offset.row, {
...formula,
dependencies: formula.dependencies.map((d) => this.getters.getRangeFromSheetXC(sheetId, d)),
});
const evaluated = this.getters.evaluateFormula(sheetId, translatedFormula);
return evaluated && !isMatrix(evaluated) ? evaluated.toString() : "";
}
@@ -43569,7 +43607,7 @@
}
let content = origin.cell.content;
if (origin.cell.isFormula && operation === "COPY") {
content = this.getters.getTranslatedCellFormula(sheetId, col - origin.position.col, row - origin.position.row, origin.cell.compiledFormula, origin.cell.dependencies);
content = this.getters.getTranslatedCellFormula(sheetId, col - origin.position.col, row - origin.position.row, origin.cell.compiledFormula);
}
this.dispatch("UPDATE_CELL", {
...target,
@@ -44101,20 +44139,29 @@
const sheetId = this.getters.getActiveSheetId();
const cells = this.getters.getCells(sheetId);
const matches = [];
if (this.toSearch) {
if (this.toSearch && this.currentSearchRegex) {
for (const cell of Object.values(cells)) {
const { col, row } = this.getters.getCellPosition(cell.id);
const cellPosition = { sheetId, col, row };
const isColHidden = this.getters.isColHidden(sheetId, col);
const isRowHidden = this.getters.isRowHidden(sheetId, row);
if (isColHidden || isRowHidden) {
continue;
}
if (cell &&
this.currentSearchRegex &&
this.currentSearchRegex.test(this.getSearchableString({ sheetId, col, row }))) {
if (this.currentSearchRegex.test(this.getSearchableString(cellPosition))) {
const match = { col, row, selected: false };
matches.push(match);
}
for (const spreadPosition of this.getters.getSpreadPositionsOf(cellPosition)) {
if (this.currentSearchRegex.test(this.getSearchableString(spreadPosition))) {
const match = {
col: spreadPosition.col,
row: spreadPosition.row,
selected: false,
};
matches.push(match);
}
}
}
}
return matches.sort(this.sortByRowThenColumn);
@@ -44173,16 +44220,19 @@
return;
}
const sheetId = this.getters.getActiveSheetId();
const cell = this.getters.getCell({ sheetId, ...selectedMatch });
const { col, row } = selectedMatch;
const position = { sheetId, ...selectedMatch };
const cell = this.getters.getCell(position);
if (!cell?.content) {
return;
}
if (cell?.isFormula && !this.searchOptions.searchFormulas) {
return;
}
const replaceRegex = new RegExp(this.currentSearchRegex.source, this.currentSearchRegex.flags + "g");
const toReplace = this.getSearchableString({ sheetId, col, row });
const toReplace = this.getSearchableString(position);
const content = toReplace.replace(replaceRegex, replaceWith);
const canonicalContent = canonicalizeNumberContent(content, this.getters.getLocale());
this.dispatch("UPDATE_CELL", { sheetId, col, row, content: canonicalContent });
this.dispatch("UPDATE_CELL", { ...position, content: canonicalContent });
}
/**
* Replace the value of the currently selected match
@@ -45757,7 +45807,7 @@
if (cell.isFormula) {
const position = this.getters.getCellPosition(cell.id);
// we only have a vertical offset
content = this.getters.getTranslatedCellFormula(sheetId, 0, newRow - position.row, cell.compiledFormula, cell.dependencies);
content = this.getters.getTranslatedCellFormula(sheetId, 0, newRow - position.row, cell.compiledFormula);
}
newCellValues.style = cell.style;
newCellValues.content = content;
@@ -47820,6 +47870,8 @@
case "EVALUATE_CELLS":
case "ACTIVATE_SHEET":
case "REMOVE_FILTER_TABLE":
case "ADD_COLUMNS_ROWS":
case "REMOVE_COLUMNS_ROWS":
this.isEvaluationDirty = true;
break;
case "START":
@@ -55812,7 +55864,6 @@
GaugeChartDesignPanel,
ScorecardChartConfigPanel,
ScorecardChartDesignPanel,
SelectionInput,
FigureComponent,
Menu,
SelectionInput,
@@ -55844,6 +55895,7 @@
exports.addFunction = addFunction;
exports.astToFormula = astToFormula;
exports.compile = compile;
exports.compileTokens = compileTokens;
exports.components = components;
exports.constants = constants;
exports.convertAstNodes = convertAstNodes;
@@ -55859,6 +55911,7 @@
exports.links = links;
exports.load = load;
exports.parse = parse;
exports.parseTokens = parseTokens;
exports.readonlyAllowedCommands = readonlyAllowedCommands;
exports.registries = registries;
exports.setDefaultSheetViewSize = setDefaultSheetViewSize;
@@ -55866,9 +55919,9 @@
exports.tokenize = tokenize;
__info__.version = "17.0.2";
__info__.date = "2023-11-03T12:32:51.249Z";
__info__.hash = "3184728ea";
__info__.version = "17.0.3";
__info__.date = "2023-11-16T13:27:10.083Z";
__info__.hash = "8bd0fb2b4";
})(this.o_spreadsheet = this.o_spreadsheet || {}, owl);
@@ -1,9 +1,9 @@
<!--
This file is generated by o-spreadsheet build tools. Do not edit it.
@see https://github.com/odoo/o-spreadsheet
@version 17.0.2
@date 2023-11-03T12:32:55.274Z
@hash 3184728ea
@version 17.0.3
@date 2023-11-16T13:27:13.577Z
@hash 8bd0fb2b4
-->
<odoo>
<t t-name="o-spreadsheet-ActionButton">
@@ -32,7 +32,8 @@ export const SEE_RECORDS_PIVOT_VISIBLE = (position, env) => {
evaluatedCell.type !== "error" &&
argsDomain !== undefined &&
cell &&
getNumberOfPivotFormulas(cell.content) === 1
cell.isFormula &&
getNumberOfPivotFormulas(cell.compiledFormula.tokens) === 1
);
};
@@ -56,7 +57,7 @@ export function SET_FILTER_MATCHING_CONDITION(position, env) {
return false;
}
const matchingFilters = env.model.getters.getFiltersMatchingPivotArgs(pivotId, domainArgs);
const pivotFunction = getFirstPivotFunction(cell.content).functionName;
const pivotFunction = getFirstPivotFunction(cell.compiledFormula.tokens).functionName;
return (
SEE_RECORDS_PIVOT_VISIBLE(position, env) &&
(pivotFunction === "ODOO.PIVOT" ||
@@ -3,6 +3,8 @@
import { _t } from "@web/core/l10n/translation";
import { getOdooFunctions } from "../helpers/odoo_functions_helpers";
/** @typedef {import("@spreadsheet/helpers/odoo_functions_helpers").Token} Token */
export const pivotFormulaRegex = /^=.*PIVOT/;
//--------------------------------------------------------------------------
@@ -13,12 +15,12 @@ export const pivotFormulaRegex = /^=.*PIVOT/;
* Parse a spreadsheet formula and detect the number of PIVOT functions that are
* present in the given formula.
*
* @param {string} formula
* @param {Token[]} tokens
*
* @returns {number}
*/
export function getNumberOfPivotFormulas(formula) {
return getOdooFunctions(formula, [
export function getNumberOfPivotFormulas(tokens) {
return getOdooFunctions(tokens, [
"ODOO.PIVOT",
"ODOO.PIVOT.HEADER",
"ODOO.PIVOT.POSITION",
@@ -29,12 +31,12 @@ export function getNumberOfPivotFormulas(formula) {
/**
* Get the first Pivot function description of the given formula.
*
* @param {string} formula
* @param {Token[]} tokens
*
* @returns {import("../helpers/odoo_functions_helpers").OdooFunctionDescription|undefined}
*/
export function getFirstPivotFunction(formula) {
return getOdooFunctions(formula, [
export function getFirstPivotFunction(tokens) {
return getOdooFunctions(tokens, [
"ODOO.PIVOT",
"ODOO.PIVOT.HEADER",
"ODOO.PIVOT.POSITION",
@@ -15,6 +15,7 @@ const { DateTime } = luxon;
/**
* @typedef {import("./pivot_core_plugin").PivotDefinition} PivotDefinition
* @typedef {import("@spreadsheet/global_filters/plugins/global_filters_core_plugin").FieldMatching} FieldMatching
* @typedef {import("@spreadsheet/helpers/odoo_functions_helpers").Token} Token
*/
/**
@@ -77,7 +78,7 @@ export class PivotUIPlugin extends spreadsheet.UIPlugin {
const { col, row } = event.anchor.cell;
const cell = this.getters.getCell({ sheetId, col, row });
if (cell !== undefined && cell.content.startsWith("=ODOO.PIVOT.HEADER(")) {
const filters = this.getFiltersMatchingPivot(cell.content);
const filters = this._getFiltersMatchingPivot(cell.compiledFormula.tokens);
this.dispatch("SET_MANY_GLOBAL_FILTER_VALUE", { filters });
}
break;
@@ -187,7 +188,7 @@ export class PivotUIPlugin extends spreadsheet.UIPlugin {
getPivotIdFromPosition(position) {
const cell = this.getters.getCorrespondingFormulaCell(position);
if (cell && cell.isFormula) {
const pivotFunction = this.getters.getFirstPivotFunction(cell.content);
const pivotFunction = this.getters.getFirstPivotFunction(cell.compiledFormula.tokens);
if (pivotFunction) {
return pivotFunction.args[0].toString();
}
@@ -195,8 +196,9 @@ export class PivotUIPlugin extends spreadsheet.UIPlugin {
return undefined;
}
getFirstPivotFunction(formula) {
const pivotFunction = getFirstPivotFunction(formula);
getFirstPivotFunction(tokens) {
console.log("getFirstPivotFunction", tokens);
const pivotFunction = getFirstPivotFunction(tokens);
if (!pivotFunction) {
return undefined;
}
@@ -204,6 +206,12 @@ export class PivotUIPlugin extends spreadsheet.UIPlugin {
const evaluatedArgs = args.map((argAst) => {
if (argAst.type == "EMPTY") {
return undefined;
} else if (
argAst.type === "STRING" ||
argAst.type === "BOOLEAN" ||
argAst.type === "NUMBER"
) {
return argAst.value;
}
const argsString = astToFormula(argAst);
return this.getters.evaluateFormula(this.getters.getActiveSheetId(), argsString);
@@ -229,11 +237,17 @@ export class PivotUIPlugin extends spreadsheet.UIPlugin {
*/
getPivotDomainArgsFromPosition(position) {
const cell = this.getters.getCorrespondingFormulaCell(position);
if (!cell || !cell.isFormula || getNumberOfPivotFormulas(cell.content) === 0) {
if (
!cell ||
!cell.isFormula ||
getNumberOfPivotFormulas(cell.compiledFormula.tokens) === 0
) {
return undefined;
}
const mainPosition = this.getters.getCellPosition(cell.id);
const { args, functionName } = this.getters.getFirstPivotFunction(cell.content);
const { args, functionName } = this.getters.getFirstPivotFunction(
cell.compiledFormula.tokens
);
if (functionName === "ODOO.PIVOT.TABLE") {
const pivotId = args[0];
const dataSource = this.getPivotDataSource(pivotId);
@@ -321,12 +335,12 @@ export class PivotUIPlugin extends spreadsheet.UIPlugin {
/**
* Get the filter impacted by a pivot formula's argument
* @param {string} formula Formula of the pivot cell
* @param {Token[]} tokens Formula of the pivot cell
*
* @returns {Array<Object>}
*/
getFiltersMatchingPivot(formula) {
const functionDescription = this.getters.getFirstPivotFunction(formula);
_getFiltersMatchingPivot(tokens) {
const functionDescription = this.getters.getFirstPivotFunction(tokens);
if (!functionDescription) {
return [];
}
@@ -503,7 +517,6 @@ PivotUIPlugin.getters = [
"getPivotIdFromPosition",
"getPivotCellValue",
"getPivotGroupByValues",
"getFiltersMatchingPivot",
"getFiltersMatchingPivotArgs",
"getPivotDataSourceId",
"getPivotTableStructure",
@@ -2,7 +2,7 @@
import { nextTick, patchDate } from "@web/../tests/helpers/utils";
import { CommandResult } from "@spreadsheet/o_spreadsheet/cancelled_reason";
import { Model, DispatchResult, helpers } from "@odoo/o-spreadsheet";
import { Model, DispatchResult, helpers, tokenize } from "@odoo/o-spreadsheet";
import {
createModelWithDataSource,
waitForDataSourcesLoaded,
@@ -47,6 +47,7 @@ import {
import { GlobalFiltersUIPlugin } from "@spreadsheet/global_filters/plugins/global_filters_ui_plugin";
import { migrate } from "@spreadsheet/o_spreadsheet/migration";
import { toRangeData } from "../utils/zones";
import { PivotUIPlugin } from "@spreadsheet/pivot/index";
const { DateTime } = luxon;
const { toZone } = helpers;
@@ -68,6 +69,11 @@ const DEFAULT_FIELD_MATCHINGS = {
1: { chain: "date", type: "date" },
};
function getFiltersMatchingPivot(model, formula) {
const pivotUIPlugin = model["handlers"].find((handler) => handler instanceof PivotUIPlugin);
return pivotUIPlugin._getFiltersMatchingPivot(tokenize(formula));
}
QUnit.module("spreadsheet > Global filters model", {}, () => {
QUnit.test("Can add a global filter", async function (assert) {
assert.expect(4);
@@ -1772,21 +1778,25 @@ QUnit.module("spreadsheet > Global filters model", {}, () => {
pivot: { 1: { chain: "date", type: "date" } },
}
);
const relationalFilters1 = model.getters.getFiltersMatchingPivot(
const relationalFilters1 = getFiltersMatchingPivot(
model,
'=ODOO.PIVOT.HEADER(1,"product_id",37)'
);
assert.deepEqual(relationalFilters1, [{ filterId: "42", value: [37] }]);
const relationalFilters2 = model.getters.getFiltersMatchingPivot(
const relationalFilters2 = getFiltersMatchingPivot(
model,
'=ODOO.PIVOT.HEADER(1,"product_id","41")'
);
assert.deepEqual(relationalFilters2, [{ filterId: "42", value: [41] }]);
const dateFilters1 = model.getters.getFiltersMatchingPivot(
const dateFilters1 = getFiltersMatchingPivot(
model,
'=ODOO.PIVOT.HEADER(1,"date:month","08/2016")'
);
assert.deepEqual(dateFilters1, [
{ filterId: "43", value: { yearOffset: -6, period: "august" } },
]);
const dateFilters2 = model.getters.getFiltersMatchingPivot(
const dateFilters2 = getFiltersMatchingPivot(
model,
'=ODOO.PIVOT.HEADER(1,"date:year","2016")'
);
assert.deepEqual(dateFilters2, [{ filterId: "43", value: { yearOffset: -6 } }]);
@@ -1797,7 +1807,7 @@ QUnit.module("spreadsheet > Global filters model", {}, () => {
"getFiltersMatchingPivot return an empty array if there is no pivot formula",
async function (assert) {
const model = await createModelWithDataSource();
const result = model.getters.getFiltersMatchingPivot("=1");
const result = getFiltersMatchingPivot(model, "=1");
assert.deepEqual(result, []);
}
);
@@ -1837,7 +1847,8 @@ QUnit.module("spreadsheet > Global filters model", {}, () => {
pivot: { 1: { chain: "product_id", type: "many2one" } },
}
);
const filters = model.getters.getFiltersMatchingPivot(
const filters = getFiltersMatchingPivot(
model,
'=ODOO.PIVOT.HEADER(1,"date:month","08/2016","product_id","41")'
);
assert.deepEqual(filters, [{ filterId: "42", value: [41] }]);
@@ -1864,7 +1875,7 @@ QUnit.module("spreadsheet > Global filters model", {}, () => {
},
{ pivot: { 1: { chain: "product_id", type: "many2one" } } }
);
const filters = model.getters.getFiltersMatchingPivot(getCellFormula(model, "B3"));
const filters = getFiltersMatchingPivot(model, getCellFormula(model, "B3"));
assert.deepEqual(filters, [
{
filterId: "42",
@@ -1894,7 +1905,7 @@ QUnit.module("spreadsheet > Global filters model", {}, () => {
},
{ pivot: { 1: { chain: "product_id", type: "many2one" } } }
);
const filters = model.getters.getFiltersMatchingPivot(getCellFormula(model, "B3"));
const filters = getFiltersMatchingPivot(model, getCellFormula(model, "B3"));
assert.deepEqual(filters, [
{
filterId: "42",
@@ -1919,7 +1930,7 @@ QUnit.module("spreadsheet > Global filters model", {}, () => {
type: "relation",
defaultValue: [],
});
const filters = model.getters.getFiltersMatchingPivot(getCellFormula(model, "B3"));
const filters = getFiltersMatchingPivot(model, getCellFormula(model, "B3"));
assert.deepEqual(filters, []);
}
);
@@ -1944,7 +1955,7 @@ QUnit.module("spreadsheet > Global filters model", {}, () => {
},
{ pivot: { 1: { chain: "product_id", type: "many2one" } } }
);
const filters = model.getters.getFiltersMatchingPivot(getCellFormula(model, "B3"));
const filters = getFiltersMatchingPivot(model, getCellFormula(model, "B3"));
assert.deepEqual(filters, []);
}
);
@@ -1966,7 +1977,7 @@ QUnit.module("spreadsheet > Global filters model", {}, () => {
defaultValue: [1],
pivotFields: { 1: { field: "product_id", type: "many2one" } },
});
const filters = model.getters.getFiltersMatchingPivot(getCellFormula(model, "B3"));
const filters = getFiltersMatchingPivot(model, getCellFormula(model, "B3"));
assert.deepEqual(filters, []);
}
);
@@ -2062,7 +2073,7 @@ QUnit.module("spreadsheet > Global filters model", {}, () => {
type: "relation",
defaultValue: [],
});
const filters = model.getters.getFiltersMatchingPivot(getCellFormula(model, "B2"));
const filters = getFiltersMatchingPivot(model, getCellFormula(model, "B2"));
assert.deepEqual(filters, []);
}
);
@@ -3,7 +3,7 @@ import { getFirstPivotFunction, getNumberOfPivotFormulas } from "@spreadsheet/pi
import { getFirstListFunction, getNumberOfListFormulas } from "@spreadsheet/list/list_helpers";
import { toNormalizedPivotValue } from "@spreadsheet/pivot/pivot_model";
import { pivotTimeAdapter } from "@spreadsheet/pivot/pivot_time_adapters";
import { constants } from "@odoo/o-spreadsheet";
import { constants, tokenize } from "@odoo/o-spreadsheet";
const { DEFAULT_LOCALE } = constants;
function stringArg(value) {
@@ -13,14 +13,15 @@ function stringArg(value) {
QUnit.module("spreadsheet > pivot_helpers", {}, () => {
QUnit.test("Basic formula extractor", async function (assert) {
const formula = `=ODOO.PIVOT("1", "test") + ODOO.LIST("2", "hello", "bla")`;
const tokens = tokenize(formula);
let functionName;
let args;
({ functionName, args } = getFirstPivotFunction(formula));
({ functionName, args } = getFirstPivotFunction(tokens));
assert.strictEqual(functionName, "ODOO.PIVOT");
assert.strictEqual(args.length, 2);
assert.deepEqual(args[0], stringArg("1"));
assert.deepEqual(args[1], stringArg("test"));
({ functionName, args } = getFirstListFunction(formula));
({ functionName, args } = getFirstListFunction(tokens));
assert.strictEqual(functionName, "ODOO.LIST");
assert.strictEqual(args.length, 3);
assert.deepEqual(args[0], stringArg("2"));
@@ -30,33 +31,34 @@ QUnit.module("spreadsheet > pivot_helpers", {}, () => {
QUnit.test("Extraction with two PIVOT formulas", async function (assert) {
const formula = `=ODOO.PIVOT("1", "test") + ODOO.PIVOT("2", "hello", "bla")`;
const { functionName, args } = getFirstPivotFunction(formula);
const tokens = tokenize(formula);
const { functionName, args } = getFirstPivotFunction(tokens);
assert.strictEqual(functionName, "ODOO.PIVOT");
assert.strictEqual(args.length, 2);
assert.deepEqual(args[0], stringArg("1"));
assert.deepEqual(args[1], stringArg("test"));
assert.strictEqual(getFirstListFunction(formula), undefined);
assert.strictEqual(getFirstListFunction(tokens), undefined);
});
QUnit.test("Number of formulas", async function (assert) {
const formula = `=ODOO.PIVOT("1", "test") + ODOO.PIVOT("2", "hello", "bla") + ODOO.LIST("1", "bla")`;
assert.strictEqual(getNumberOfPivotFormulas(formula), 2);
assert.strictEqual(getNumberOfListFormulas(formula), 1);
assert.strictEqual(getNumberOfPivotFormulas("=1+1"), 0);
assert.strictEqual(getNumberOfListFormulas("=1+1"), 0);
assert.strictEqual(getNumberOfPivotFormulas("=bla"), 0);
assert.strictEqual(getNumberOfListFormulas("=bla"), 0);
assert.strictEqual(getNumberOfPivotFormulas(tokenize(formula)), 2);
assert.strictEqual(getNumberOfListFormulas(tokenize(formula)), 1);
assert.strictEqual(getNumberOfPivotFormulas(tokenize("=1+1")), 0);
assert.strictEqual(getNumberOfListFormulas(tokenize("=1+1")), 0);
assert.strictEqual(getNumberOfPivotFormulas(tokenize("=bla")), 0);
assert.strictEqual(getNumberOfListFormulas(tokenize("=bla")), 0);
});
QUnit.test("getFirstPivotFunction does not crash when given crap", async function (assert) {
assert.strictEqual(getFirstListFunction("=SUM(A1)"), undefined);
assert.strictEqual(getFirstPivotFunction("=SUM(A1)"), undefined);
assert.strictEqual(getFirstListFunction("=1+1"), undefined);
assert.strictEqual(getFirstPivotFunction("=1+1"), undefined);
assert.strictEqual(getFirstListFunction("=bla"), undefined);
assert.strictEqual(getFirstPivotFunction("=bla"), undefined);
assert.strictEqual(getFirstListFunction("bla"), undefined);
assert.strictEqual(getFirstPivotFunction("bla"), undefined);
assert.strictEqual(getFirstListFunction(tokenize("=SUM(A1)")), undefined);
assert.strictEqual(getFirstPivotFunction(tokenize("=SUM(A1)")), undefined);
assert.strictEqual(getFirstListFunction(tokenize("=1+1")), undefined);
assert.strictEqual(getFirstPivotFunction(tokenize("=1+1")), undefined);
assert.strictEqual(getFirstListFunction(tokenize("=bla")), undefined);
assert.strictEqual(getFirstPivotFunction(tokenize("=bla")), undefined);
assert.strictEqual(getFirstListFunction(tokenize("bla")), undefined);
assert.strictEqual(getFirstPivotFunction(tokenize("bla")), undefined);
});
});
@@ -65,7 +65,7 @@ export function getCells(model, sheetId = model.getters.getActiveSheetId()) {
*/
export function getCellFormula(model, xc, sheetId = model.getters.getActiveSheetId()) {
const cell = getCell(model, xc, sheetId);
return cell && cell.isFormula ? model.getters.getFormulaCellContent(sheetId, cell) : "";
return cell && cell.isFormula ? cell.content : "";
}
/**
@@ -20,7 +20,7 @@ cellMenuRegistry.add("move_lines_see_records", {
const position = env.model.getters.getActivePosition();
const sheetId = position.sheetId;
const cell = env.model.getters.getCell(position);
const { args } = getFirstAccountFunction(cell.content);
const { args } = getFirstAccountFunction(cell.compiledFormula.tokens);
let [codes, date_range, offset, companyId, includeUnposted] = args
.map(astToFormula)
.map((arg) => env.model.getters.evaluateFormula(sheetId, arg));
@@ -46,7 +46,7 @@ cellMenuRegistry.add("move_lines_see_records", {
!evaluatedCell.error &&
evaluatedCell.value !== "" &&
cell &&
getNumberOfAccountFormulas(cell.content) === 1
getNumberOfAccountFormulas(cell.compiledFormula.tokens) === 1
);
},
});
+10 -7
View File
@@ -1,22 +1,25 @@
/** @odoo-module **/
import { getOdooFunctions } from "@spreadsheet/helpers/odoo_functions_helpers";
/** @typedef {import("@spreadsheet/helpers/odoo_functions_helpers").OdooFunctionDescription} OdooFunctionDescription*/
/**
* @typedef {import("@spreadsheet/helpers/odoo_functions_helpers").Token} Token
* @typedef {import("@spreadsheet/helpers/odoo_functions_helpers").OdooFunctionDescription} OdooFunctionDescription
*/
/**
* @param {string} formula
* @param {Token[]} tokens
* @returns {number}
*/
export function getNumberOfAccountFormulas(formula) {
return getOdooFunctions(formula, ["ODOO.BALANCE", "ODOO.CREDIT", "ODOO.DEBIT"]).length;
export function getNumberOfAccountFormulas(tokens) {
return getOdooFunctions(tokens, ["ODOO.BALANCE", "ODOO.CREDIT", "ODOO.DEBIT"]).length;
}
/**
* Get the first Account function description of the given formula.
*
* @param {string} formula
* @param {Token[]} tokens
* @returns {OdooFunctionDescription | undefined}
*/
export function getFirstAccountFunction(formula) {
return getOdooFunctions(formula, ["ODOO.BALANCE", "ODOO.CREDIT", "ODOO.DEBIT"])[0];
export function getFirstAccountFunction(tokens) {
return getOdooFunctions(tokens, ["ODOO.BALANCE", "ODOO.CREDIT", "ODOO.DEBIT"])[0];
}