[REF] spreadsheet: refactor insert pivot methods

The goal here is to simplify the code of the insertion of a pivot in the
pivot core plugin. Now the domain/style for each cell of the pivot is computed
in the `SpreadsheetPivotTable` class, which simplifies the plugin a lot.

Also take the opportunity to convert all `anchor` array arguments
to `{ col, row }` objects since it's the direction we've taken every
where else: `position.col` is much more readable than `anchor[0]`

closes odoo/odoo#128981

Task: 3318865
Related: odoo/enterprise#44326
Signed-off-by: Lucas Lefèvre (lul) <lul@odoo.com>
This commit is contained in:
Adrien Minne (adrm)
2023-08-08 22:41:04 +02:00
committed by Lucas Lefèvre
parent 77c758fe8b
commit 17d54eff72
2 changed files with 157 additions and 177 deletions
@@ -1,16 +1,25 @@
/** @odoo-module */
import { HEADER_STYLE, TOP_LEVEL_STYLE, MEASURE_STYLE } from "@spreadsheet/helpers/constants";
/**
* @typedef {Object} Column
* @property {string[]} fields
* @property {string[]} values
* @property {number} width
* @property {number} offset
*
* @typedef {Object} Row
* @property {string[]} fields
* @property {string[]} values
* @property {number} intend
*
* @typedef {Object} PivotCell
* @property {boolean} isHeader
* @property {string[]} [domain] Domain of the pivot formula. Undefined for constant cells or empty cells
* @property {string} [content] Content of constant cells in the pivot.
* @property {Object} [style]
* @property {string} [measure] Measure for the pivot formula. Undefined for header cells.
*
* @typedef {Object} SpreadsheetTableData
* @property {Column[][]} cols
* @property {Row[]} rows
@@ -68,17 +77,21 @@ export class SpreadsheetPivotTable {
* @param {string} rowTitle
*/
constructor(cols, rows, measures, rowTitle = "") {
this._cols = cols;
/** @type {Column[][]} */
this._cols = cols.map((row) => {
// offset in the pivot table
// starts at 1 because the first column is the row title
let offset = 1;
return row.map((col) => {
col = { ...col, offset };
offset += col.width;
return col;
});
});
this._rows = rows;
this._measures = measures;
this._rowTitle = rowTitle;
}
/**
* @returns {number}
*/
getNumberOfMeasures() {
return this._measures.length;
this.pivotCells = this._getPivotCells();
}
/**
@@ -93,22 +106,22 @@ export class SpreadsheetPivotTable {
* @returns {Column[]}
*/
getMeasureHeaders() {
return this._cols[this._cols.length - 1];
return this._cols[this.getNumberOfHeaderRows() - 1];
}
/**
* Get the number of columns leafs (i.e. the number of the last row of columns)
* @returns {number}
*/
getColWidth() {
return this._cols[this._cols.length - 1].length;
getNumberOfDataColumns() {
return this._cols[this.getNumberOfHeaderRows() - 1].length;
}
/**
* Get the number of row in each columns
* Get the number of row in each column header
* @return {number}
*/
getColHeight() {
getNumberOfHeaderRows() {
return this._cols.length;
}
@@ -124,7 +137,7 @@ export class SpreadsheetPivotTable {
*
* @returns {number}
*/
getRowHeight() {
getNumberOfDataRows() {
return this._rows.length;
}
@@ -171,8 +184,80 @@ export class SpreadsheetPivotTable {
return this._rows[index];
}
getRowTitle() {
return this._rowTitle;
/**
* @private
* @returns {PivotCell[][]}
*/
_getPivotCells() {
const pivotHeight = this.getNumberOfHeaderRows() + this.getNumberOfDataRows();
const pivotWidth = this.getNumberOfDataColumns() + 1;
const domainArray = [];
for (let col = 0; col < pivotWidth; col++) {
domainArray.push([]);
for (let row = 0; row < pivotHeight; row++) {
domainArray[col].push(this._getPivotCell(col, row));
}
}
return domainArray;
}
/**
* @returns {PivotCell}
*/
_getPivotCell(col, row) {
const colHeadersHeight = this.getNumberOfHeaderRows();
if (col === 0 && row === colHeadersHeight - 1) {
return { content: this._rowTitle, isHeader: true, style: HEADER_STYLE };
} else if (row <= colHeadersHeight - 1) {
const domain = this._getColHeaderDomain(col, row);
const style = row === colHeadersHeight - 1 ? MEASURE_STYLE : TOP_LEVEL_STYLE;
return { domain, isHeader: true, style };
} else if (col === 0) {
const rowIndex = row - colHeadersHeight;
const domain = this._getRowDomain(rowIndex);
const indent = this._rows[rowIndex].indent;
const style = indent <= 1 ? TOP_LEVEL_STYLE : indent === 2 ? HEADER_STYLE : undefined;
return { domain, isHeader: true, style };
} else {
const rowIndex = row - colHeadersHeight;
const domain = [...this._getRowDomain(rowIndex), ...this._getColDomain(col)];
const measure = this._getColMeasure(col);
return { domain, isHeader: false, measure };
}
}
_getColHeaderDomain(col, row) {
if (col === 0) {
return undefined;
}
const domain = [];
const pivotCol = this._cols[row].find((pivotCol) => pivotCol.offset === col);
if (!pivotCol) {
return undefined;
}
for (let i = 0; i < pivotCol.fields.length; i++) {
domain.push(pivotCol.fields[i]);
domain.push(pivotCol.values[i]);
}
return domain;
}
_getColDomain(col) {
return this._getColHeaderDomain(col, this.getNumberOfHeaderRows() - 1).slice(0, -2); // slice: remove measure and value
}
_getColMeasure(col) {
return this._getColHeaderDomain(col, this.getNumberOfHeaderRows() - 1).at(-1);
}
_getRowDomain(row) {
const domain = [];
for (let i = 0; i < this._rows[row].fields.length; i++) {
domain.push(this._rows[row].fields[i]);
domain.push(this._rows[row].values[i]);
}
return domain;
}
/**
@@ -20,18 +20,19 @@
* @property {Object} fieldMatching
*
* @typedef {import("@spreadsheet/global_filters/plugins/global_filters_core_plugin").FieldMatching} FieldMatching
* @typedef {import("../pivot_table.js").PivotCell} PivotCell
*/
import { CorePlugin } from "@odoo/o-spreadsheet";
import { CorePlugin, helpers } from "@odoo/o-spreadsheet";
import { makePivotFormula } from "../pivot_helpers";
import { getMaxObjectId } from "@spreadsheet/helpers/helpers";
import { HEADER_STYLE, TOP_LEVEL_STYLE, MEASURE_STYLE } from "@spreadsheet/helpers/constants";
import { SpreadsheetPivotTable } from "../pivot_table";
import CommandResult from "../../o_spreadsheet/cancelled_reason";
import { _t } from "@web/core/l10n/translation";
import { globalFiltersFieldMatchers } from "@spreadsheet/global_filters/plugins/global_filters_core_plugin";
import { sprintf } from "@web/core/utils/strings";
import { checkFilterFieldMatching } from "@spreadsheet/global_filters/helpers";
const { isDefined } = helpers;
export default class PivotCorePlugin extends CorePlugin {
constructor(config) {
@@ -82,22 +83,22 @@ export default class PivotCorePlugin extends CorePlugin {
switch (cmd.type) {
case "INSERT_PIVOT": {
const { sheetId, col, row, id, definition } = cmd;
/** @type [number,number] */
const anchor = [col, row];
/** @type { col: number, row: number } */
const position = { col, row };
const { cols, rows, measures, rowTitle } = cmd.table;
const table = new SpreadsheetPivotTable(cols, rows, measures, rowTitle);
this._addPivot(id, definition);
this._insertPivot(sheetId, anchor, id, table);
this._insertPivot(sheetId, position, id, table);
this.history.update("nextId", parseInt(id, 10) + 1);
break;
}
case "RE_INSERT_PIVOT": {
const { sheetId, col, row, id } = cmd;
/** @type [number,number] */
const anchor = [col, row];
/** @type { col: number, row: number } */
const position = { col, row };
const { cols, rows, measures, rowTitle } = cmd.table;
const table = new SpreadsheetPivotTable(cols, rows, measures, rowTitle);
this._insertPivot(sheetId, anchor, id, table);
this._insertPivot(sheetId, position, id, table);
break;
}
case "RENAME_ODOO_PIVOT": {
@@ -267,93 +268,39 @@ export default class PivotCorePlugin extends CorePlugin {
/**
* @param {string} sheetId
* @param {[number, number]} anchor
* @param {{ col: number, row: number }} position
* @param {string} id
* @param {SpreadsheetPivotTable} table
*/
_insertPivot(sheetId, anchor, id, table) {
this._resizeSheet(sheetId, anchor, table);
this._insertColumns(sheetId, anchor, id, table);
this._insertRows(sheetId, anchor, id, table);
this._insertBody(sheetId, anchor, id, table);
this._addBorders(sheetId, anchor, table);
}
/**
* @param {string} sheetId
* @param {[number, number]} anchor
* @param {string} id
* @param {SpreadsheetPivotTable} table
*/
_insertColumns(sheetId, anchor, id, table) {
let anchorLeft = anchor[0] + 1;
let anchorTop = anchor[1];
for (const _row of table.getColHeaders()) {
anchorLeft = anchor[0] + 1;
for (const cell of _row) {
const args = [id];
for (let i = 0; i < cell.fields.length; i++) {
args.push(cell.fields[i]);
args.push(cell.values[i]);
}
if (cell.width > 1) {
this._merge(sheetId, {
top: anchorTop,
bottom: anchorTop,
left: anchorLeft,
right: anchorLeft + cell.width - 1,
});
}
this._addPivotFormula(sheetId, anchorLeft, anchorTop, "ODOO.PIVOT.HEADER", args);
anchorLeft += cell.width;
_insertPivot(sheetId, position, id, table) {
this._resizeSheet(sheetId, position, table);
const pivotCells = table.pivotCells;
for (let col = 0; col < pivotCells.length; col++) {
for (let row = 0; row < pivotCells[col].length; row++) {
const pivotCell = pivotCells[col][row];
const functionCol = position.col + col;
const functionRow = position.row + row;
this._addPivotFormula(
sheetId,
id,
{ col: functionCol, row: functionRow },
pivotCell
);
}
anchorTop++;
}
const colHeight = table.getColHeight();
const colWidth = table.getColWidth();
const lastRowBeforeMeasureRow = anchor[1] + colHeight - 2;
const right = anchor[0] + colWidth;
const left = right - table.getNumberOfMeasures() + 1;
for (let anchorTop = anchor[1]; anchorTop < lastRowBeforeMeasureRow; anchorTop++) {
this._merge(sheetId, { top: anchorTop, bottom: anchorTop, left, right });
}
const headersZone = {
top: anchor[1],
bottom: lastRowBeforeMeasureRow,
left: anchor[0],
right: anchor[0] + colWidth,
};
const measuresZone = {
top: anchor[1] + colHeight - 1,
bottom: anchor[1] + colHeight - 1,
left: anchor[0],
right: anchor[0] + colWidth,
};
this.dispatch("SET_FORMATTING", { sheetId, target: [headersZone], style: TOP_LEVEL_STYLE });
this.dispatch("SET_FORMATTING", { sheetId, target: [measuresZone], style: MEASURE_STYLE });
}
/**
* Merge a zone
*
* @param {string} sheetId
* @param {Object} zone
*
* @private
*/
_merge(sheetId, zone) {
this.dispatch("ADD_MERGE", { sheetId, target: [zone] });
this._addBorders(sheetId, position, table);
}
/**
* @param {string} sheetId
* @param {[number,number]} anchor
* @param {{ col: number, row: number }} position
* @param {SpreadsheetPivotTable} table
*/
_resizeSheet(sheetId, anchor, table) {
const colLimit = table.getColWidth() + 1; // +1 for the Top-Left
_resizeSheet(sheetId, { col, row }, table) {
const colLimit = table.getNumberOfDataColumns() + 1; // +1 for the Top-Left
const numberCols = this.getters.getNumberCols(sheetId);
const deltaCol = numberCols - anchor[0];
const deltaCol = numberCols - col;
if (deltaCol < colLimit) {
this.dispatch("ADD_COLUMNS_ROWS", {
dimension: "COL",
@@ -363,9 +310,9 @@ export default class PivotCorePlugin extends CorePlugin {
position: "after",
});
}
const rowLimit = table.getColHeight() + table.getRowHeight();
const rowLimit = table.getNumberOfHeaderRows() + table.getNumberOfDataRows();
const numberRows = this.getters.getNumberRows(sheetId);
const deltaRow = numberRows - anchor[1];
const deltaRow = numberRows - row;
if (deltaRow < rowLimit) {
this.dispatch("ADD_COLUMNS_ROWS", {
dimension: "ROW",
@@ -379,90 +326,33 @@ export default class PivotCorePlugin extends CorePlugin {
/**
* @param {string} sheetId
* @param {[number, number]} anchor
* @param {string} id
* @param {{ col: number, row: number }} position
* @param {SpreadsheetPivotTable} table
*/
_insertRows(sheetId, anchor, id, table) {
let y = anchor[1] + table.getColHeight() - 1;
const x = anchor[0];
this.dispatch("UPDATE_CELL", {
col: x,
row: y,
sheetId,
content: table.getRowTitle(),
});
y++;
for (const row of table.getRowHeaders()) {
const args = [id];
for (let i = 0; i < row.fields.length; i++) {
args.push(row.fields[i]);
args.push(row.values[i]);
}
this._addPivotFormula(sheetId, x, y, "ODOO.PIVOT.HEADER", args);
if (row.indent <= 2) {
const target = [{ top: y, bottom: y, left: x, right: x }];
const style = row.indent === 2 ? HEADER_STYLE : TOP_LEVEL_STYLE;
this.dispatch("SET_FORMATTING", { sheetId, target, style });
}
y++;
}
}
/**
* @param {string} sheetId
* @param {[number, number]} anchor
* @param {string} id
* @param {SpreadsheetPivotTable} table
*/
_insertBody(sheetId, anchor, id, table) {
let x = anchor[0] + 1;
for (const col of table.getMeasureHeaders()) {
let y = anchor[1] + table.getColHeight();
const measure = col.values[col.values.length - 1];
for (const row of table.getRowHeaders()) {
const args = [id, measure];
for (let i = 0; i < row.fields.length; i++) {
args.push(row.fields[i]);
args.push(row.values[i]);
}
for (let i = 0; i < col.fields.length - 1; i++) {
args.push(col.fields[i]);
args.push(col.values[i]);
}
this._addPivotFormula(sheetId, x, y, "ODOO.PIVOT", args);
y++;
}
x++;
}
}
_addBorders(sheetId, anchor, table) {
const colHeight = table.getColHeight();
const colWidth = table.getColWidth();
const anchorLeft = anchor[0];
const anchorTop = anchor[1];
const totalRow = anchorTop + colHeight + table.getRowHeaders().length - 1;
_addBorders(sheetId, { col, row }, table) {
const colHeight = table.getNumberOfHeaderRows();
const colWidth = table.getNumberOfDataColumns();
const totalRow = row + colHeight + table.getRowHeaders().length - 1;
const headerAndMeasureZone = {
top: anchorTop,
bottom: anchorTop + colHeight - 1,
left: anchorLeft,
right: anchorLeft + colWidth,
top: row,
bottom: row + colHeight - 1,
left: col,
right: col + colWidth,
};
this.dispatch("SET_ZONE_BORDERS", {
sheetId,
target: [
headerAndMeasureZone,
{
left: anchorLeft,
right: anchorLeft + colWidth,
left: col,
right: col + colWidth,
top: totalRow,
bottom: totalRow,
},
{
left: anchorLeft,
right: anchorLeft + colWidth,
top: anchorTop,
left: col,
right: col + colWidth,
top: row,
bottom: totalRow,
},
],
@@ -475,17 +365,22 @@ export default class PivotCorePlugin extends CorePlugin {
/**
* @param {string} sheetId
* @param {number} col
* @param {number} row
* @param {string} formula
* @param {Array<string>} args
* @param {{ col: number, row: number }} position
* @param {string} pivotId
* @param {PivotCell} pivotCell
*/
_addPivotFormula(sheetId, col, row, formula, args) {
_addPivotFormula(sheetId, pivotId, { col, row }, pivotCell) {
const formula = pivotCell.isHeader ? "ODOO.PIVOT.HEADER" : "ODOO.PIVOT";
const args = pivotCell.domain
? [pivotId, pivotCell.measure, ...pivotCell.domain].filter(isDefined)
: undefined;
this.dispatch("UPDATE_CELL", {
sheetId,
col,
row,
content: makePivotFormula(formula, args),
content: pivotCell.content || (args ? makePivotFormula(formula, args) : undefined),
style: pivotCell.style,
});
}