diff --git a/packages/o-spreadsheet-engine/src/functions/helpers.ts b/packages/o-spreadsheet-engine/src/functions/helpers.ts index 46185d15cc..627d8d8c38 100644 --- a/packages/o-spreadsheet-engine/src/functions/helpers.ts +++ b/packages/o-spreadsheet-engine/src/functions/helpers.ts @@ -98,6 +98,14 @@ export function toNumber( } } +/** Convert the data to a number, ignoring undefined values */ +export function toNumberIgnoreUndefined( + data: FunctionResultObject | CellValue | undefined, + locale: Locale +): number | undefined { + return data === undefined ? undefined : toNumber(data, locale); +} + export function tryToNumber( value: string | number | boolean | null | undefined, locale: Locale @@ -220,6 +228,13 @@ export function toBoolean(data: FunctionResultObject | CellValue | undefined): b } } +/** Convert the data to a boolean, ignoring undefined values */ +export function toBooleanIgnoreUndefined( + data: FunctionResultObject | CellValue | undefined +): boolean | undefined { + return data === undefined ? undefined : toBoolean(data); +} + function strictToBoolean(data: FunctionResultObject | CellValue | undefined): boolean { const value = toValue(data); if (value === "") { diff --git a/packages/o-spreadsheet-engine/src/functions/module_lookup.ts b/packages/o-spreadsheet-engine/src/functions/module_lookup.ts index e9e23bb504..aa9e1197a9 100644 --- a/packages/o-spreadsheet-engine/src/functions/module_lookup.ts +++ b/packages/o-spreadsheet-engine/src/functions/module_lookup.ts @@ -3,13 +3,15 @@ import { PIVOT_MAX_NUMBER_OF_CELLS } from "../constants"; import { getFullReference, splitReference } from "../helpers/"; import { toXC } from "../helpers/coordinates"; import { range } from "../helpers/misc"; -import { addAlignFormatToPivotHeader } from "../helpers/pivot/pivot_helpers"; +import { + addAlignFormatToPivotHeader, + getPivotStyleFromFnArgs, +} from "../helpers/pivot/pivot_helpers"; import { toZone } from "../helpers/zones"; import { _t } from "../translation"; import { CellErrorType, EvaluationError, InvalidReferenceError } from "../types/errors"; import { AddFunctionDescription } from "../types/functions"; import { Arg, FunctionResultObject, Matrix, Maybe, Zone } from "../types/misc"; -import { PivotVisibilityOptions } from "../types/pivot"; import { arg } from "./arguments"; import { expectNumberGreaterThanOrEqualToOne } from "./helper_assert"; import { @@ -19,12 +21,12 @@ import { getPivotId, } from "./helper_lookup"; import { - LinearSearchMode, dichotomicSearch, expectNumberRangeError, generateMatrix, isEvaluationError, linearSearch, + LinearSearchMode, strictToInteger, toBoolean, toMatrix, @@ -907,30 +909,34 @@ export const PIVOT = { ], compute: function ( pivotFormulaId: Maybe, - rowCount: Maybe = { value: 10000 }, - includeTotal: Maybe = { value: true }, - includeColumnHeaders: Maybe = { value: true }, - columnCount: Maybe = { value: Number.MAX_VALUE }, - includeMeasureTitles: Maybe = { value: true } + rowCount: Maybe, + includeTotal: Maybe, + includeColumnHeaders: Maybe, + columnCount: Maybe, + includeMeasureTitles: Maybe ) { const _pivotFormulaId = toString(pivotFormulaId); - const _rowCount = toNumber(rowCount, this.locale); - if (_rowCount < 0) { + const pivotId = getPivotId(_pivotFormulaId, this.getters); + const pivot = this.getters.getPivot(pivotId); + const coreDefinition = this.getters.getPivotCoreDefinition(pivotId); + + const pivotStyle = getPivotStyleFromFnArgs( + coreDefinition, + rowCount, + includeTotal, + includeColumnHeaders, + columnCount, + includeMeasureTitles, + this.locale + ); + + if (pivotStyle.numberOfRows < 0) { return new EvaluationError(_t("The number of rows must be positive.")); } - const _columnCount = toNumber(columnCount, this.locale); - if (_columnCount < 0) { + if (pivotStyle.numberOfColumns < 0) { return new EvaluationError(_t("The number of columns must be positive.")); } - const visibilityOptions: PivotVisibilityOptions = { - displayColumnHeaders: toBoolean(includeColumnHeaders), - displayTotals: toBoolean(includeTotal), - displayMeasuresRow: toBoolean(includeMeasureTitles), - }; - const pivotId = getPivotId(_pivotFormulaId, this.getters); - const pivot = this.getters.getPivot(pivotId); - const coreDefinition = this.getters.getPivotCoreDefinition(pivotId); addPivotDependencies(this, coreDefinition, coreDefinition.measures); pivot.init({ reload: pivot.needsReevaluation }); const error = pivot.assertIsValid({ throwOnError: false }); @@ -941,21 +947,21 @@ export const PIVOT = { if (table.numberOfCells > PIVOT_MAX_NUMBER_OF_CELLS) { return new EvaluationError(getPivotTooBigErrorMessage(table.numberOfCells, this.locale)); } - const cells = table.getPivotCells(visibilityOptions); + const cells = table.getPivotCells(pivotStyle); let headerRows = 0; - if (visibilityOptions.displayColumnHeaders) { + if (pivotStyle.displayColumnHeaders) { headerRows = table.columns.length - 1; } - if (visibilityOptions.displayMeasuresRow) { + if (pivotStyle.displayMeasuresRow) { headerRows++; } const pivotTitle = this.getters.getPivotName(pivotId); - const tableHeight = Math.min(headerRows + _rowCount, cells[0].length); + const tableHeight = Math.min(headerRows + pivotStyle.numberOfRows, cells[0].length); if (tableHeight === 0) { return [[{ value: pivotTitle }]]; } - const tableWidth = Math.min(1 + _columnCount, cells.length); + const tableWidth = Math.min(1 + pivotStyle.numberOfColumns, cells.length); const result: Matrix = []; for (const col of range(0, tableWidth)) { result[col] = []; @@ -978,7 +984,7 @@ export const PIVOT = { } } } - if (visibilityOptions.displayColumnHeaders || visibilityOptions.displayMeasuresRow) { + if (pivotStyle.displayColumnHeaders || pivotStyle.displayMeasuresRow) { result[0][0] = { value: pivotTitle }; } return result; diff --git a/packages/o-spreadsheet-engine/src/helpers/pivot/pivot_helpers.ts b/packages/o-spreadsheet-engine/src/helpers/pivot/pivot_helpers.ts index 02b2e0812a..9eaca2e133 100644 --- a/packages/o-spreadsheet-engine/src/helpers/pivot/pivot_helpers.ts +++ b/packages/o-spreadsheet-engine/src/helpers/pivot/pivot_helpers.ts @@ -25,6 +25,7 @@ import { PivotField, PivotFields, PivotSortedColumn, + PivotStyle, PivotTableCell, } from "../../types/pivot"; import { Pivot } from "../../types/pivot_runtime"; @@ -33,6 +34,14 @@ import { deepEquals, getUniqueText, isDefined } from "../misc"; import { PivotRuntimeDefinition } from "./pivot_runtime_definition"; import { pivotTimeAdapter } from "./pivot_time_adapter"; +export const DEFAULT_PIVOT_STYLE: Required = { + displayTotals: true, + displayColumnHeaders: true, + displayMeasuresRow: true, + numberOfRows: Number.MAX_VALUE, + numberOfColumns: Number.MAX_VALUE, +}; + const AGGREGATOR_NAMES = { count: _t("Count"), count_distinct: _t("Count Distinct"), @@ -446,3 +455,39 @@ export function togglePivotCollapse(position: CellPosition, env: SpreadsheetChil pivot: { ...definition, collapsedDomains: newDomains }, }); } + +export function getPivotStyleFromFnArgs( + definition: PivotCoreDefinition, + rowCountArg: Maybe, + includeTotalArg: Maybe, + includeColumnHeadersArg: Maybe, + columnCountArg: Maybe, + includeMeasuresRowArg: Maybe, + locale: Locale +): Required { + const style = definition.style; + + const numberOfRows = + rowCountArg !== undefined + ? toNumber(rowCountArg, locale) + : style?.numberOfRows ?? DEFAULT_PIVOT_STYLE.numberOfRows; + const numberOfColumns = + columnCountArg !== undefined + ? toNumber(columnCountArg, locale) + : style?.numberOfColumns ?? DEFAULT_PIVOT_STYLE.numberOfColumns; + + const displayTotals = + includeTotalArg !== undefined + ? toBoolean(includeTotalArg) + : style?.displayTotals ?? DEFAULT_PIVOT_STYLE.displayTotals; + const displayColumnHeaders = + includeColumnHeadersArg !== undefined + ? toBoolean(includeColumnHeadersArg) + : style?.displayColumnHeaders ?? DEFAULT_PIVOT_STYLE.displayColumnHeaders; + const displayMeasuresRow = + includeMeasuresRowArg !== undefined + ? toBoolean(includeMeasuresRowArg) + : style?.displayMeasuresRow ?? DEFAULT_PIVOT_STYLE.displayMeasuresRow; + + return { numberOfRows, numberOfColumns, displayTotals, displayColumnHeaders, displayMeasuresRow }; +} diff --git a/packages/o-spreadsheet-engine/src/helpers/pivot/table_spreadsheet_pivot.ts b/packages/o-spreadsheet-engine/src/helpers/pivot/table_spreadsheet_pivot.ts index d78f8a36bb..0893ad71b8 100644 --- a/packages/o-spreadsheet-engine/src/helpers/pivot/table_spreadsheet_pivot.ts +++ b/packages/o-spreadsheet-engine/src/helpers/pivot/table_spreadsheet_pivot.ts @@ -5,14 +5,14 @@ import { PivotCollapsedDomains, PivotDomain, PivotSortedColumn, + PivotStyle, PivotTableCell, PivotTableColumn, PivotTableRow, - PivotVisibilityOptions, } from "../../types/pivot"; import { deepEquals, lazy } from "../misc"; import { isParentDomain, sortPivotTree } from "./pivot_domain_helpers"; -import { parseDimension, toNormalizedPivotValue } from "./pivot_helpers"; +import { DEFAULT_PIVOT_STYLE, parseDimension, toNormalizedPivotValue } from "./pivot_helpers"; interface CollapsiblePivotTableColumn extends PivotTableColumn { collapsedHeader?: boolean; @@ -157,29 +157,23 @@ export class SpreadsheetPivotTable { return this.columns.at(-1)?.length || 0; } - private getSkippedRows(visibilityOptions: PivotVisibilityOptions) { + private getSkippedRows(pivotStyle: Required) { const skippedRows: Set = new Set(); - if (!visibilityOptions.displayColumnHeaders) { + if (!pivotStyle.displayColumnHeaders) { for (let i = 0; i < this.columns.length - 1; i++) { skippedRows.add(i); } } - if (!visibilityOptions.displayMeasuresRow) { + if (!pivotStyle.displayMeasuresRow) { skippedRows.add(this.columns.length - 1); } return skippedRows; } - getPivotCells( - visibilityOptions: PivotVisibilityOptions = { - displayColumnHeaders: true, - displayTotals: true, - displayMeasuresRow: true, - } - ): PivotTableCell[][] { - const key = JSON.stringify(visibilityOptions); + getPivotCells(pivotStyle: Required = DEFAULT_PIVOT_STYLE): PivotTableCell[][] { + const key = JSON.stringify(pivotStyle); if (!this.pivotCells[key]) { - const { displayTotals } = visibilityOptions; + const { displayTotals } = pivotStyle; const numberOfDataRows = this.rows.length; const numberOfDataColumns = this.getNumberOfDataColumns(); let pivotHeight = this.columns.length + numberOfDataRows; @@ -191,7 +185,7 @@ export class SpreadsheetPivotTable { pivotWidth -= this.measures.length; } const domainArray: PivotTableCell[][] = []; - const skippedRows = this.getSkippedRows(visibilityOptions); + const skippedRows = this.getSkippedRows(pivotStyle); for (let col = 0; col < pivotWidth; col++) { domainArray.push([]); for (let row = 0; row < pivotHeight; row++) { diff --git a/packages/o-spreadsheet-engine/src/plugins/ui_core_views/pivot_ui.ts b/packages/o-spreadsheet-engine/src/plugins/ui_core_views/pivot_ui.ts index f41c3495dc..4547ea3a7e 100644 --- a/packages/o-spreadsheet-engine/src/plugins/ui_core_views/pivot_ui.ts +++ b/packages/o-spreadsheet-engine/src/plugins/ui_core_views/pivot_ui.ts @@ -1,13 +1,13 @@ import { astToFormula } from "../../formulas/formula_formatter"; import { Token } from "../../formulas/tokenizer"; import { toScalar } from "../../functions/helper_matrices"; -import { toBoolean } from "../../functions/helpers"; import { deepEquals, getUniqueText } from "../../helpers/misc"; import { getFirstPivotFunction, getNumberOfPivotFunctions, } from "../../helpers/pivot/pivot_composer_helpers"; import { domainToColRowDomain } from "../../helpers/pivot/pivot_domain_helpers"; +import { getPivotStyleFromFnArgs } from "../../helpers/pivot/pivot_helpers"; import withPivotPresentationLayer from "../../helpers/pivot/pivot_presentation"; import { pivotRegistry } from "../../helpers/pivot/pivot_registry"; import { resetMapValueDimensionDate } from "../../helpers/pivot/spreadsheet_pivot/date_spreadsheet_pivot"; @@ -21,7 +21,7 @@ import { UpdatePivotCommand, } from "../../types/commands"; import { CellPosition, FunctionResultObject, isMatrix, SortDirection, UID } from "../../types/misc"; -import { PivotCoreMeasure, PivotTableCell, PivotVisibilityOptions } from "../../types/pivot"; +import { PivotCoreMeasure, PivotTableCell } from "../../types/pivot"; import { Pivot } from "../../types/pivot_runtime"; import { CoreViewPlugin, CoreViewPluginConfig } from "../core_view_plugin"; import { UIPluginConfig } from "../ui_plugin"; @@ -219,20 +219,16 @@ export class PivotUIPlugin extends CoreViewPlugin { return EMPTY_PIVOT_CELL; } if (functionName === "PIVOT") { - const includeTotal = toScalar(args[2]); - const shouldIncludeTotal = includeTotal === undefined ? true : toBoolean(includeTotal); - const includeColumnHeaders = toScalar(args[3]); - const includeMeasures = toScalar(args[5]); - const shouldIncludeMeasures = - includeMeasures === undefined ? true : toBoolean(includeMeasures); - const shouldIncludeColumnHeaders = - includeColumnHeaders === undefined ? true : toBoolean(includeColumnHeaders); - const visibilityOptions: PivotVisibilityOptions = { - displayColumnHeaders: shouldIncludeColumnHeaders, - displayTotals: shouldIncludeTotal, - displayMeasuresRow: shouldIncludeMeasures, - }; - const pivotCells = pivot.getCollapsedTableStructure().getPivotCells(visibilityOptions); + const pivotStyle = getPivotStyleFromFnArgs( + this.getters.getPivotCoreDefinition(pivotId), + toScalar(args[1]), + toScalar(args[2]), + toScalar(args[3]), + toScalar(args[4]), + toScalar(args[5]), + this.getters.getLocale() + ); + const pivotCells = pivot.getCollapsedTableStructure().getPivotCells(pivotStyle); const pivotCol = position.col - mainPosition.col; const pivotRow = position.row - mainPosition.row; return pivotCells[pivotCol][pivotRow]; diff --git a/packages/o-spreadsheet-engine/src/types/pivot.ts b/packages/o-spreadsheet-engine/src/types/pivot.ts index 8fefb670ff..746d9092a4 100644 --- a/packages/o-spreadsheet-engine/src/types/pivot.ts +++ b/packages/o-spreadsheet-engine/src/types/pivot.ts @@ -62,6 +62,7 @@ export interface CommonPivotCoreDefinition { sortedColumn?: PivotSortedColumn; collapsedDomains?: PivotCollapsedDomains; customFields?: Record; + style?: PivotStyle; } export interface PivotSortedColumn { @@ -239,8 +240,10 @@ export interface DimensionTreeNode { export type DimensionTree = DimensionTreeNode[]; -export interface PivotVisibilityOptions { - displayColumnHeaders: boolean; - displayTotals: boolean; - displayMeasuresRow: boolean; +export interface PivotStyle { + numberOfRows?: number; + numberOfColumns?: number; + displayTotals?: boolean; + displayColumnHeaders?: boolean; + displayMeasuresRow?: boolean; } diff --git a/src/components/side_panel/chart/main_chart_panel/main_chart_panel.css b/src/components/side_panel/chart/main_chart_panel/main_chart_panel.css deleted file mode 100644 index 27546866a0..0000000000 --- a/src/components/side_panel/chart/main_chart_panel/main_chart_panel.css +++ /dev/null @@ -1,32 +0,0 @@ -.o-spreadsheet { - .o-chart { - .o-panel { - display: flex; - .o-panel-element { - flex: 1 0 auto; - padding: 8px 0px; - text-align: center; - cursor: pointer; - border-right: 1px solid var(--os-gray-300); - - &.inactive { - color: var(--os-text-body); - background-color: var(--os-gray-100); - border-bottom: 1px solid var(--os-gray-300); - } - - &:not(.inactive) { - color: var(--os-button-active-text-color); - border-bottom: 1px solid #fff; - } - - .fa { - margin-right: 4px; - } - } - .o-panel-element:last-child { - border-right: none; - } - } - } -} diff --git a/src/components/side_panel/chart/main_chart_panel/main_chart_panel.xml b/src/components/side_panel/chart/main_chart_panel/main_chart_panel.xml index 48abc9d2ec..0d95ddef2c 100644 --- a/src/components/side_panel/chart/main_chart_panel/main_chart_panel.xml +++ b/src/components/side_panel/chart/main_chart_panel/main_chart_panel.xml @@ -1,19 +1,19 @@
-
+
- + Configuration
- + Design
diff --git a/src/components/side_panel/pivot/pivot_side_panel/pivot_design_panel/pivot_design_panel.css b/src/components/side_panel/pivot/pivot_side_panel/pivot_design_panel/pivot_design_panel.css new file mode 100644 index 0000000000..0077d5ad0b --- /dev/null +++ b/src/components/side_panel/pivot/pivot_side_panel/pivot_design_panel/pivot_design_panel.css @@ -0,0 +1,7 @@ +.o-spreadsheet { + .o-pivot-design { + .row { + height: 24px; + } + } +} diff --git a/src/components/side_panel/pivot/pivot_side_panel/pivot_design_panel/pivot_design_panel.ts b/src/components/side_panel/pivot/pivot_side_panel/pivot_design_panel/pivot_design_panel.ts new file mode 100644 index 0000000000..4a3fd9af06 --- /dev/null +++ b/src/components/side_panel/pivot/pivot_side_panel/pivot_design_panel/pivot_design_panel.ts @@ -0,0 +1,37 @@ +import { DEFAULT_PIVOT_STYLE } from "@odoo/o-spreadsheet-engine/helpers/pivot/pivot_helpers"; +import { SpreadsheetChildEnv } from "@odoo/o-spreadsheet-engine/types/spreadsheet_env"; +import { Component } from "@odoo/owl"; +import { Store, useLocalStore } from "../../../../../store_engine"; +import { PivotStyle, UID } from "../../../../../types"; +import { Checkbox } from "../../../components/checkbox/checkbox"; +import { Section } from "../../../components/section/section"; +import { PivotSidePanelStore } from "../pivot_side_panel_store"; + +interface Props { + pivotId: UID; +} + +export class PivotDesignPanel extends Component { + static template = "o-spreadsheet-PivotDesignPanel"; + static props = { pivotId: String }; + static components = { Section, Checkbox }; + + store!: Store; + + setup() { + this.store = useLocalStore(PivotSidePanelStore, this.props.pivotId, "neverDefer"); + } + + updatePivotStyleProperty(key: keyof PivotStyle, value: PivotStyle[keyof PivotStyle]) { + this.store.update({ style: { ...this.pivotStyle, [key]: value } }); + } + + get pivotStyle() { + const pivot = this.env.model.getters.getPivotCoreDefinition(this.props.pivotId); + return pivot.style || {}; + } + + get defaultStyle() { + return DEFAULT_PIVOT_STYLE; + } +} diff --git a/src/components/side_panel/pivot/pivot_side_panel/pivot_design_panel/pivot_design_panel.xml b/src/components/side_panel/pivot/pivot_side_panel/pivot_design_panel/pivot_design_panel.xml new file mode 100644 index 0000000000..63c24d0bd7 --- /dev/null +++ b/src/components/side_panel/pivot/pivot_side_panel/pivot_design_panel/pivot_design_panel.xml @@ -0,0 +1,64 @@ + + +
+
+
+
+
Max rows:
+
+ +
+
+
+
Max columns:
+
+ +
+
+
+
Show totals:
+
+ +
+
+
+
Show column titles:
+
+ +
+
+
+
Show measure titles:
+
+ +
+
+
+
+
+
+
diff --git a/src/components/side_panel/pivot/pivot_side_panel/pivot_side_panel.ts b/src/components/side_panel/pivot/pivot_side_panel/pivot_side_panel.ts index 33360170bd..cbb7899d9b 100644 --- a/src/components/side_panel/pivot/pivot_side_panel/pivot_side_panel.ts +++ b/src/components/side_panel/pivot/pivot_side_panel/pivot_side_panel.ts @@ -1,17 +1,22 @@ import { SpreadsheetChildEnv } from "@odoo/o-spreadsheet-engine/types/spreadsheet_env"; -import { Component } from "@odoo/owl"; +import { Component, useRef, useState } from "@odoo/owl"; import { getPivotHighlights } from "../../../../helpers/pivot/pivot_highlight"; import { pivotSidePanelRegistry } from "../../../../helpers/pivot/pivot_side_panel_registry"; -import { UID } from "../../../../types"; +import { Pixel, UID } from "../../../../types"; import { useHighlights } from "../../../helpers/highlight_hook"; import { Section } from "../../components/section/section"; import { PivotLayoutConfigurator } from "../pivot_layout_configurator/pivot_layout_configurator"; +import { PivotDesignPanel } from "./pivot_design_panel/pivot_design_panel"; interface Props { pivotId: UID; onCloseSidePanel: () => void; } +interface State { + panel: "configuration" | "design"; +} + export class PivotSidePanel extends Component { static template = "o-spreadsheet-PivotSidePanel"; static props = { @@ -21,6 +26,14 @@ export class PivotSidePanel extends Component { static components = { PivotLayoutConfigurator, Section, + PivotDesignPanel, + }; + + state = useState({ panel: "configuration" }); + private panelContentRef = useRef("panelContent"); + private scrollPositions: Record<"configuration" | "design", Pixel> = { + configuration: 0, + design: 0, }; setup() { @@ -38,4 +51,12 @@ export class PivotSidePanel extends Component { get highlights() { return getPivotHighlights(this.env.model.getters, this.props.pivotId); } + + switchPanel(panel: "configuration" | "design") { + const el = this.panelContentRef.el as HTMLElement; + if (el) { + this.scrollPositions[this.state.panel] = el.scrollTop; + } + this.state.panel = panel; + } } diff --git a/src/components/side_panel/pivot/pivot_side_panel/pivot_side_panel.xml b/src/components/side_panel/pivot/pivot_side_panel/pivot_side_panel.xml index 83c2b1e612..48e1ab1ea4 100644 --- a/src/components/side_panel/pivot/pivot_side_panel/pivot_side_panel.xml +++ b/src/components/side_panel/pivot/pivot_side_panel/pivot_side_panel.xml @@ -1,5 +1,31 @@ - +
+
+
+ + Configuration +
+
+ + Design +
+
+ +
+
+ +
+
+ +
+
+
diff --git a/src/components/side_panel/pivot/pivot_side_panel/pivot_side_panel_store.ts b/src/components/side_panel/pivot/pivot_side_panel/pivot_side_panel_store.ts index 5ee27dbda7..0ff023f6a9 100644 --- a/src/components/side_panel/pivot/pivot_side_panel/pivot_side_panel_store.ts +++ b/src/components/side_panel/pivot/pivot_side_panel/pivot_side_panel_store.ts @@ -24,15 +24,19 @@ import { getPivotTooBigErrorMessage } from "../../../../../packages/o-spreadshee export class PivotSidePanelStore extends SpreadsheetStore { mutators = ["reset", "deferUpdates", "applyUpdate", "discardPendingUpdate", "update"] as const; - private updatesAreDeferred: boolean; + private _updatesAreDeferred: boolean; private draft: PivotCoreDefinition | null = null; private notification = this.get(NotificationStore); private alreadyNotified = false; private alreadyNotifiedForPivotSize = false; - constructor(get: Get, private pivotId: UID) { + constructor( + get: Get, + private pivotId: UID, + private updateMode: "neverDefer" | "canDefer" = "canDefer" + ) { super(get); - this.updatesAreDeferred = + this._updatesAreDeferred = this.getters.getPivotCoreDefinition(this.pivotId).deferUpdates ?? false; } @@ -45,6 +49,10 @@ export class PivotSidePanelStore extends SpreadsheetStore { } } + get updatesAreDeferred() { + return this.updateMode === "neverDefer" ? false : this._updatesAreDeferred; + } + get fields() { return this.pivot.getFields(); } @@ -131,7 +139,7 @@ export class PivotSidePanelStore extends SpreadsheetStore { reset(pivotId: UID) { this.pivotId = pivotId; - this.updatesAreDeferred = true; + this._updatesAreDeferred = true; this.draft = null; } @@ -142,7 +150,7 @@ export class PivotSidePanelStore extends SpreadsheetStore { } else { this.update({ deferUpdates: shouldDefer }); } - this.updatesAreDeferred = shouldDefer; + this._updatesAreDeferred = shouldDefer; } applyUpdate() { diff --git a/src/components/side_panel/side_panel/side_panel.css b/src/components/side_panel/side_panel/side_panel.css index 001ba113d1..030f1fe694 100644 --- a/src/components/side_panel/side_panel/side_panel.css +++ b/src/components/side_panel/side_panel/side_panel.css @@ -89,6 +89,26 @@ margin-left: -5px; } } + + .o-sidePanel-tab { + padding: 8px 0px; + cursor: pointer; + border-right: 1px solid var(--os-gray-300); + + &.inactive { + color: var(--os-text-body); + background-color: var(--os-gray-100); + border-bottom: 1px solid var(--os-gray-300); + } + + &:not(.inactive) { + color: var(--os-button-active-text-color); + border-bottom: 1px solid #fff; + } + } + .o-sidePanel-tab:last-child { + border-right: none; + } } .o-fw-bold { diff --git a/tests/figures/chart/charts_component.test.ts b/tests/figures/chart/charts_component.test.ts index 30d30ea641..98469ec060 100644 --- a/tests/figures/chart/charts_component.test.ts +++ b/tests/figures/chart/charts_component.test.ts @@ -1116,11 +1116,11 @@ describe("charts", () => { const chartPanel = fixture.querySelector(".o-panel-content")!; chartPanel.scrollTop = 100; - const configTab = fixture.querySelector(".o-panel-element.inactive")!; + const configTab = fixture.querySelector(".o-sidePanel-tab.inactive")!; await click(configTab); expect(chartPanel.scrollTop).toBe(0); - const designTab = fixture.querySelector(".o-panel-element.inactive")!; + const designTab = fixture.querySelector(".o-sidePanel-tab.inactive")!; await click(designTab); expect(chartPanel.scrollTop).toBe(100); }); diff --git a/tests/pivots/pivot_design_side_panel.test.ts b/tests/pivots/pivot_design_side_panel.test.ts new file mode 100644 index 0000000000..5e475ecc14 --- /dev/null +++ b/tests/pivots/pivot_design_side_panel.test.ts @@ -0,0 +1,97 @@ +import { SpreadsheetChildEnv } from "@odoo/o-spreadsheet-engine/types/spreadsheet_env"; +import { Model } from "../../src"; +import { SidePanels } from "../../src/components/side_panel/side_panels/side_panels"; +import { setCellContent } from "../test_helpers/commands_helpers"; +import { click, setInputValueAndTrigger, simulateClick } from "../test_helpers/dom_helper"; +import { mountComponentWithPortalTarget, nextTick, setGrid } from "../test_helpers/helpers"; +import { addPivot, updatePivot } from "../test_helpers/pivot_helpers"; + +describe("Spreadsheet pivot side panel", () => { + let model: Model; + let fixture: HTMLElement; + let env: SpreadsheetChildEnv; + let notifyUser: jest.Mock; + + beforeEach(async () => { + notifyUser = jest.fn(); + ({ env, model, fixture } = await mountComponentWithPortalTarget(SidePanels, { + env: { notifyUser }, + })); + // prettier-ignore + const grid = { + A1: "Customer", B1: "Product", C1: "Amount", + A2: "Alice", B2: "Chair", C2: "10", + A3: "Bob", B3: "Table", C3: "20", + }; + setGrid(model, grid); + + addPivot(model, "A1:C3", {}, "1"); + env.openSidePanel("PivotSidePanel", { pivotId: "1" }); + await nextTick(); + }); + + test("Can switch between config and design panels", async () => { + const panelDivs = fixture.querySelectorAll(".o-panel-content > div"); + const panelTabs = fixture.querySelectorAll(".o-sidePanel-tab"); + + expect(panelTabs[0]).toHaveText(" Configuration "); + expect(panelTabs[0]).not.toHaveClass("inactive"); + expect(panelTabs[1]).toHaveText(" Design "); + expect(panelTabs[1]).toHaveClass("inactive"); + expect(panelDivs[0]).not.toHaveClass("d-none"); + expect(panelDivs[1]).toHaveClass("d-none"); + + await click(panelTabs[1]); + expect(panelTabs[0]).toHaveClass("inactive"); + expect(panelTabs[1]).not.toHaveClass("inactive"); + expect(panelDivs[0]).toHaveClass("d-none"); + expect(panelDivs[1]).not.toHaveClass("d-none"); + }); + + test("Pivot design panel is correctly initialized", async () => { + updatePivot(model, "1", { + style: { displayColumnHeaders: false, numberOfColumns: 87, displayTotals: true }, + }); + await nextTick(); + + expect("input.o-pivot-n-of-rows").toHaveValue(""); + expect("input.o-pivot-n-of-columns").toHaveValue("87"); + expect("input[name='displayColumnHeaders']").toHaveValue(false); + expect("input[name='displayTotals']").toHaveValue(true); + expect("input[name='displayMeasuresRow']").toHaveValue(true); + }); + + test("Can edit the pivot style with the side panel", async () => { + await setInputValueAndTrigger("input.o-pivot-n-of-rows", "12"); + await setInputValueAndTrigger("input.o-pivot-n-of-columns", "34"); + await simulateClick("input[name='displayColumnHeaders']"); + await simulateClick("input[name='displayTotals']"); + await simulateClick("input[name='displayMeasuresRow']"); + + expect(model.getters.getPivotCoreDefinition("1").style).toEqual({ + numberOfRows: 12, + numberOfColumns: 34, + displayColumnHeaders: false, + displayTotals: false, + displayMeasuresRow: false, + }); + }); + + test("Editing the pivot style will warn the user if the sheet has only static pivot cells", async () => { + setCellContent(model, "E1", "=PIVOT.HEADER(1)"); + await setInputValueAndTrigger("input.o-pivot-n-of-rows", "12"); + + expect(notifyUser).toHaveBeenCalledWith({ + text: "Pivot updates only work with dynamic pivot tables. Use the formula '=PIVOT(1)' or re-insert the static pivot from the Data menu.", + sticky: true, + type: "info", + }); + }); + + test("Pivot style edition is never deferred", async () => { + updatePivot(model, "1", { deferUpdates: true }); + await nextTick(); + await setInputValueAndTrigger("input.o-pivot-n-of-rows", "12"); + expect(model.getters.getPivotCoreDefinition("1").style?.numberOfRows).toBe(12); + }); +}); diff --git a/tests/pivots/pivot_plugin.test.ts b/tests/pivots/pivot_plugin.test.ts index 7c0250d591..2f06fe2fe2 100644 --- a/tests/pivots/pivot_plugin.test.ts +++ b/tests/pivots/pivot_plugin.test.ts @@ -399,6 +399,31 @@ describe("Pivot plugin", () => { }); }); + test("getPivotCellFromPosition handles both the pivot style and the function arguments", () => { + // prettier-ignore + const grid = { + A1: "Customer", B1: "Price", + A2: "Alice", B2: "10", + A3: "Bob", B3: "30", + }; + const model = createModelFromGrid(grid); + addPivot(model, "A1:B3", { + columns: [{ fieldName: "Customer" }], + rows: [{ fieldName: "Price" }], + measures: [{ id: "testCount", fieldName: "__count", aggregator: "sum" }], + }); + const D1 = toCellPosition(model.getters.getActiveSheetId(), "D1"); + + setCellContent(model, "C1", "=PIVOT(1)"); + expect(model.getters.getPivotCellFromPosition(D1)).toMatchObject({ type: "HEADER" }); + + updatePivot(model, "1", { style: { displayColumnHeaders: false } }); + expect(model.getters.getPivotCellFromPosition(D1)).toMatchObject({ type: "MEASURE_HEADER" }); + + setCellContent(model, "C1", "=PIVOT(1,,,TRUE)"); + expect(model.getters.getPivotCellFromPosition(D1)).toMatchObject({ type: "HEADER" }); + }); + test("DUPLICATE_PIVOT_IN_NEW_SHEET is prevented if the pivot is in error", () => { const model = new Model(); addPivot(model, "A1:A2", {}, "pivot1"); diff --git a/tests/pivots/spreadsheet_pivot/__snapshots__/spreadsheet_pivot_side_panel.test.ts.snap b/tests/pivots/spreadsheet_pivot/__snapshots__/spreadsheet_pivot_side_panel.test.ts.snap index 9ac08a54c4..ae73030636 100644 --- a/tests/pivots/spreadsheet_pivot/__snapshots__/spreadsheet_pivot_side_panel.test.ts.snap +++ b/tests/pivots/spreadsheet_pivot/__snapshots__/spreadsheet_pivot_side_panel.test.ts.snap @@ -77,159 +77,322 @@ exports[`Spreadsheet pivot side panel It should correctly be displayed 1`] = ` class="o-sidePanelBody" >
-
-
-
- -
- Name - - - -
- -
- -
- -
- -
- +
+ + Configuration +
+
+ + Design +
+
+
+
- Range - - -
- -
-
+
+
+
+ +
+ Name + + + +
+ +
+ +
+ +
+ +
- +
+ Range + + +
+ +
+
+ +
+ + + +
+ +
+ + +
+ + +
+
-
- - -
+
+
+ Columns + + + +
+ + +
+ Rows + + + +
+ + +
+ Measures + + + +
+ + +
- - -
- -
- Columns - + + Defer updates + +
- +
+
+
+
- Rows - + Display options
- -
- Measures - - - +
+
+ Max rows: +
+
+ +
+
+
+
+ Max columns: +
+
+ +
+
+
+
+ Show totals: +
+
+ +
+
+
+
+ Show column titles: +
+
+ +
+
+
+
+ Show measure titles: +
+
+ +
+
+
-
- -
-
- - - - -
-
@@ -314,144 +477,307 @@ exports[`Spreadsheet pivot side panel It should display only the selection input class="o-sidePanelBody" >
-
+
+ + Configuration +
+
+ + Design +
+
+
+
- -
- Name - +
+
+
+ +
+ Name + + + +
+ +
+ +
+ +
+ +
+ +
+
+ Range + + +
+ +
+
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+ +
+ + + + +
+ +
+
+ + The pivot cannot be created because the dataset is missing. + + +
-
-
- + + + +
- +
+
- Range + Display options
-
+
-
- - -
+
+ +
+
+
+
+ Max columns: +
+
+ +
+
+
+
+ Show totals: +
+
+
- - - + + +
- -
- - -
-
- - - - + +
+
+
+
+ Show measure titles: +
+
+ +
-
- - The pivot cannot be created because the dataset is missing. -
- -
-
- - - - -
-
diff --git a/tests/pivots/spreadsheet_pivot/spreadsheet_pivot.test.ts b/tests/pivots/spreadsheet_pivot/spreadsheet_pivot.test.ts index 1e92bb560b..3984b30065 100644 --- a/tests/pivots/spreadsheet_pivot/spreadsheet_pivot.test.ts +++ b/tests/pivots/spreadsheet_pivot/spreadsheet_pivot.test.ts @@ -855,6 +855,104 @@ describe("Spreadsheet Pivot", () => { ]); }); + test("Pivot style is applied to the result of PIVOT formula.", () => { + // prettier-ignore + const grid = { + A1: "Date", B1: "Price", C1: "=PIVOT(1)", + A2: "2024-12-28", B2: "10", + A3: "2024-11-28", B3: "20", + A4: "1995-04-14", B4: "30", + }; + const model = createModelFromGrid(grid); + addPivot(model, "A1:B4", { + rows: [], + columns: [{ fieldName: "Date", granularity: "day" }], + measures: [{ id: "Price:sum", fieldName: "Price", aggregator: "sum" }], + }); + + // prettier-ignore + expect(getEvaluatedGrid(model, "C1:H2")).toEqual([ + ["Pivot", "14 Apr 1995", "28 Nov 2024", "28 Dec 2024", "Total", ""], + ["", "Price", "Price", "Price", "Price", ""], + ]); + + updatePivot(model, "1", { style: { numberOfColumns: 1 } }); + // prettier-ignore + expect(getEvaluatedGrid(model, "C1:E2")).toEqual([ + ["Pivot", "14 Apr 1995", ""], + ["", "Price", ""], + ]); + + updatePivot(model, "1", { style: { displayColumnHeaders: false } }); + // prettier-ignore + expect(getEvaluatedGrid(model, "C1:H2")).toEqual([ + ["Pivot", "Price", "Price", "Price", "Price", ""], + ["Total", "30", "20", "10", "60", ""], + ]); + + updatePivot(model, "1", { style: { numberOfColumns: 2, displayMeasuresRow: false } }); + // prettier-ignore + expect(getEvaluatedGrid(model, "C1:F2")).toEqual([ + ["Pivot", "14 Apr 1995", "28 Nov 2024", ""], + ["Total", "30", "20", ""], + ]); + + updatePivot(model, "1", { + rows: [{ fieldName: "Date", granularity: "day" }], + columns: [], + style: { displayTotals: false }, + }); + // prettier-ignore + expect(getEvaluatedGrid(model, "C1:D6")).toEqual([ + ["Pivot", "Total"], + ["", "Price"], + ["14 Apr 1995", "30"], + ["28 Nov 2024", "20"], + ["28 Dec 2024", "10"], + ["", ""], + ]); + + updatePivot(model, "1", { style: { numberOfRows: 2 } }); + // prettier-ignore + expect(getEvaluatedGrid(model, "C1:D5")).toEqual([ + ["Pivot", "Total"], + ["", "Price"], + ["14 Apr 1995", "30"], + ["28 Nov 2024", "20"], + ["", ""], + ]); + }); + + test("Pivot style is overridden by the arguments of the pivot formula", () => { + // prettier-ignore + const grid = { + A1: "Date", B1: "Price", C1: "=PIVOT(1)", + A2: "2024-12-28", B2: "10", + A3: "2024-11-28", B3: "20", + A4: "1995-04-14", B4: "30", + }; + const model = createModelFromGrid(grid); + addPivot(model, "A1:B4", { + rows: [], + columns: [{ fieldName: "Date", granularity: "day" }], + measures: [{ id: "Price:sum", fieldName: "Price", aggregator: "sum" }], + style: { numberOfColumns: 1 }, + }); + + // prettier-ignore + expect(getEvaluatedGrid(model, "C1:E2")).toEqual([ + ["Pivot", "14 Apr 1995", "",], + ["", "Price", "",], + ]); + + setCellContent(model, "C1", "=PIVOT(1,,,,3)"); + // prettier-ignore + expect(getEvaluatedGrid(model, "C1:G2")).toEqual([ + ["Pivot", "14 Apr 1995", "28 Nov 2024", "28 Dec 2024", "",], + ["", "Price", "Price", "Price", "",], + ]); + }); + test("PIVOT row headers are indented relative to the groupBy depth.", () => { // prettier-ignore const grid = { diff --git a/tests/pivots/spreadsheet_pivot/spreadsheet_pivot_side_panel.test.ts b/tests/pivots/spreadsheet_pivot/spreadsheet_pivot_side_panel.test.ts index 498c8961d7..f7aa314fd4 100644 --- a/tests/pivots/spreadsheet_pivot/spreadsheet_pivot_side_panel.test.ts +++ b/tests/pivots/spreadsheet_pivot/spreadsheet_pivot_side_panel.test.ts @@ -3,6 +3,7 @@ import { SpreadsheetPivot } from "@odoo/o-spreadsheet-engine/helpers/pivot/sprea import { SpreadsheetChildEnv } from "@odoo/o-spreadsheet-engine/types/spreadsheet_env"; import { getPivotTooBigErrorMessage } from "../../../packages/o-spreadsheet-engine/src/components/translations_terms"; import { Model, PivotSortedColumn, SpreadsheetPivotTable } from "../../../src"; +import { SidePanels } from "../../../src/components/side_panel/side_panels/side_panels"; import { toXC, toZone } from "../../../src/helpers"; import { topbarMenuRegistry } from "../../../src/registries/menus"; import { NotificationStore } from "../../../src/stores/notification_store"; @@ -25,7 +26,7 @@ import { getCellText, getCoreTable, getEvaluatedCell } from "../../test_helpers/ import { doAction, editStandaloneComposer, - mountSpreadsheet, + mountComponentWithPortalTarget, nextTick, setGrid, } from "../../test_helpers/helpers"; @@ -40,7 +41,9 @@ describe("Spreadsheet pivot side panel", () => { beforeEach(async () => { notifyUser = jest.fn(); - ({ env, model, fixture } = await mountSpreadsheet(undefined, { notifyUser })); + ({ env, model, fixture } = await mountComponentWithPortalTarget(SidePanels, { + env: { notifyUser }, + })); // prettier-ignore const grid = { A1: "Customer", B1: "Product", C1: "Amount", diff --git a/tests/test_helpers/chart_helpers.ts b/tests/test_helpers/chart_helpers.ts index 941ddbe5fc..b29a87f445 100644 --- a/tests/test_helpers/chart_helpers.ts +++ b/tests/test_helpers/chart_helpers.ts @@ -60,7 +60,7 @@ export async function openChartDesignSidePanel( if (!fixture.querySelector(".o-chart")) { await openChartConfigSidePanel(model, env, chartId); } - await simulateClick(".o-panel-element.inactive"); + await simulateClick(".o-sidePanel-tab.inactive"); } export function getColorPickerValue(fixture: HTMLElement, selector: string) {