From 17d54eff72e6eb7c290cc455d33bb32c7e6f48eb Mon Sep 17 00:00:00 2001 From: "Adrien Minne (adrm)" Date: Fri, 14 Jul 2023 10:31:22 +0000 Subject: [PATCH] [REF] spreadsheet: refactor insert pivot methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../static/src/pivot/pivot_table.js | 117 ++++++++-- .../src/pivot/plugins/pivot_core_plugin.js | 217 +++++------------- 2 files changed, 157 insertions(+), 177 deletions(-) diff --git a/addons/spreadsheet/static/src/pivot/pivot_table.js b/addons/spreadsheet/static/src/pivot/pivot_table.js index 28bf9475908..ea73e454cab 100644 --- a/addons/spreadsheet/static/src/pivot/pivot_table.js +++ b/addons/spreadsheet/static/src/pivot/pivot_table.js @@ -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; } /** diff --git a/addons/spreadsheet/static/src/pivot/plugins/pivot_core_plugin.js b/addons/spreadsheet/static/src/pivot/plugins/pivot_core_plugin.js index 442cf1d9db1..c03bcd4a317 100644 --- a/addons/spreadsheet/static/src/pivot/plugins/pivot_core_plugin.js +++ b/addons/spreadsheet/static/src/pivot/plugins/pivot_core_plugin.js @@ -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} 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, }); }