[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:
committed by
Lucas Lefèvre
parent
77c758fe8b
commit
17d54eff72
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user