Skip to content

Commit c7557c5

Browse files
hokolomopopro-odoo
authored andcommitted
[IMP] pivot: add collapse/expand icons to pivot
This commit adds icons to collapse/expand dimensions of a pivot. The computation of the collapsed state of the pivot is done in the `SpreadsheetPivotTable`. But since some features need the full, non-collapsed pivot (eg. calculated measures), we now need two different `SpreadsheetPivotTable` per pivot, `pivot.getTableStructure()` and `pivot.getNonCollapsedTableStructure()`. closes #6083 Task: 4674387 Signed-off-by: Pierre Rousseau (pro) <pro@odoo.com>
1 parent e844df6 commit c7557c5

29 files changed

+815
-114
lines changed

src/actions/menu_items_actions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ export const REINSERT_DYNAMIC_PIVOT_CHILDREN = (env: SpreadsheetChildEnv) =>
430430
sequence: index,
431431
execute: (env: SpreadsheetChildEnv) => {
432432
const zone = env.model.getters.getSelectedZone();
433-
const table = env.model.getters.getPivot(pivotId).getTableStructure().export();
433+
const table = env.model.getters.getPivot(pivotId).getCollapsedTableStructure().export();
434434
env.model.dispatch("INSERT_PIVOT_WITH_TABLE", {
435435
pivotId,
436436
table,
@@ -451,7 +451,7 @@ export const REINSERT_STATIC_PIVOT_CHILDREN = (env: SpreadsheetChildEnv) =>
451451
sequence: index,
452452
execute: (env: SpreadsheetChildEnv) => {
453453
const zone = env.model.getters.getSelectedZone();
454-
const table = env.model.getters.getPivot(pivotId).getTableStructure().export();
454+
const table = env.model.getters.getPivot(pivotId).getExpandedTableStructure().export();
455455
env.model.dispatch("INSERT_PIVOT_WITH_TABLE", {
456456
pivotId,
457457
table,

src/components/grid_cell_icon/grid_cell_icon.ts

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,25 @@
11
import { Component } from "@odoo/owl";
22
import { DEFAULT_VERTICAL_ALIGN, GRID_ICON_EDGE_LENGTH, GRID_ICON_MARGIN } from "../../constants";
33
import { positionToZone } from "../../helpers";
4-
import { Align, CellPosition, Rect, SpreadsheetChildEnv, VerticalAlign } from "../../types";
5-
import { css, cssPropertiesToCss } from "../helpers";
6-
7-
css/* scss */ `
8-
.o-grid-cell-icon {
9-
width: ${GRID_ICON_EDGE_LENGTH}px;
10-
height: ${GRID_ICON_EDGE_LENGTH}px;
11-
}
12-
`;
4+
import { GridIcon } from "../../registries/icons_on_cell_registry";
5+
import { CellPosition, Rect, SpreadsheetChildEnv, VerticalAlign } from "../../types";
6+
import { cssPropertiesToCss } from "../helpers";
137

148
export interface GridCellIconProps {
15-
cellPosition: CellPosition;
16-
horizontalAlign?: Align;
9+
icon: GridIcon;
1710
verticalAlign?: VerticalAlign;
1811
}
1912

2013
export class GridCellIcon extends Component<GridCellIconProps, SpreadsheetChildEnv> {
2114
static template = "o-spreadsheet-GridCellIcon";
2215
static props = {
23-
cellPosition: Object,
24-
horizontalAlign: { type: String, optional: true },
16+
icon: Object,
2517
verticalAlign: { type: String, optional: true },
2618
slots: Object,
2719
};
2820

2921
get iconStyle(): string {
30-
const cellPosition = this.props.cellPosition;
22+
const cellPosition = this.props.icon.position;
3123
const merge = this.env.model.getters.getMerge(cellPosition);
3224
const zone = merge || positionToZone(cellPosition);
3325
const rect = this.env.model.getters.getVisibleRectWithoutHeaders(zone);
@@ -36,6 +28,8 @@ export class GridCellIcon extends Component<GridCellIconProps, SpreadsheetChildE
3628
return cssPropertiesToCss({
3729
top: `${y}px`,
3830
left: `${x}px`,
31+
width: `${this.props.icon.size}px`,
32+
height: `${this.props.icon.size}px`,
3933
});
4034
}
4135

@@ -63,16 +57,17 @@ export class GridCellIcon extends Component<GridCellIconProps, SpreadsheetChildE
6357

6458
const cell = this.env.model.getters.getCell(cellPosition);
6559
const evaluatedCell = this.env.model.getters.getEvaluatedCell(cellPosition);
66-
const align = this.props.horizontalAlign || cell?.style?.align || evaluatedCell.defaultAlign;
60+
const align =
61+
this.props.icon.horizontalAlign || cell?.style?.align || evaluatedCell.defaultAlign;
6762

6863
switch (align) {
6964
case "right":
70-
return end - GRID_ICON_MARGIN - GRID_ICON_EDGE_LENGTH;
65+
return end - this.props.icon.size - this.props.icon.margin;
7166
case "left":
72-
return start + GRID_ICON_MARGIN;
67+
return start + this.props.icon.margin;
7368
default:
74-
const centeringOffset = Math.floor((end - start - GRID_ICON_EDGE_LENGTH) / 2);
75-
return end - GRID_ICON_EDGE_LENGTH - centeringOffset;
69+
const centeringOffset = Math.floor((end - start - this.props.icon.size) / 2);
70+
return end - this.props.icon.size - centeringOffset;
7671
}
7772
}
7873

src/components/grid_cell_icon/grid_cell_icon.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<t t-name="o-spreadsheet-GridCellIcon">
33
<div
44
class="o-grid-cell-icon position-absolute overflow-hidden"
5-
t-if="isPositionVisible(this.props.cellPosition)"
5+
t-if="isPositionVisible(this.props.icon.position)"
66
t-att-style="iconStyle">
77
<t t-slot="default"/>
88
</div>

src/components/grid_cell_icon_overlay/grid_cell_icon_overlay.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<templates>
22
<t t-name="o-spreadsheet-GridCellIconOverlay">
33
<t t-foreach="icons" t-as="icon" t-key="icon_index">
4-
<GridCellIcon cellPosition="icon.position" horizontalAlign="icon.horizontalAlign">
4+
<GridCellIcon icon="icon">
55
<t t-component="icon.component" cellPosition="icon.position"/>
66
</GridCellIcon>
77
</t>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { Component } from "@odoo/owl";
2+
import { deepEquals } from "../../helpers";
3+
import { CellPosition, SpreadsheetChildEnv } from "../../types";
4+
import { css } from "../helpers";
5+
6+
css/* scss */ `
7+
.o-spreadsheet {
8+
.o-pivot-collapse-icon {
9+
cursor: pointer;
10+
width: 11px;
11+
height: 11px;
12+
border: 1px solid #777;
13+
background-color: #eee;
14+
margin: 3px 0 3px 6px;
15+
16+
.o-icon {
17+
width: 5px;
18+
height: 5px;
19+
}
20+
}
21+
}
22+
`;
23+
24+
interface Props {
25+
cellPosition: CellPosition;
26+
}
27+
28+
export class PivotCollapseIcon extends Component<Props, SpreadsheetChildEnv> {
29+
static template = "o-spreadsheet-PivotCollapseIcon";
30+
static props = {
31+
cellPosition: Object,
32+
};
33+
34+
onClick() {
35+
const pivotCell = this.env.model.getters.getPivotCellFromPosition(this.props.cellPosition);
36+
const pivotId = this.env.model.getters.getPivotIdFromPosition(this.props.cellPosition);
37+
if (!pivotId || pivotCell.type !== "HEADER") {
38+
return;
39+
}
40+
const definition = this.env.model.getters.getPivotCoreDefinition(pivotId);
41+
42+
const collapsedDomains = definition.collapsedDomains?.[pivotCell.dimension]
43+
? [...definition.collapsedDomains[pivotCell.dimension]]
44+
: [];
45+
const index = collapsedDomains.findIndex((domain) => deepEquals(domain, pivotCell.domain));
46+
if (index !== -1) {
47+
collapsedDomains.splice(index, 1);
48+
} else {
49+
collapsedDomains.push(pivotCell.domain);
50+
}
51+
52+
const newDomains = definition.collapsedDomains
53+
? { ...definition.collapsedDomains }
54+
: { COL: [], ROW: [] };
55+
newDomains[pivotCell.dimension] = collapsedDomains;
56+
this.env.model.dispatch("UPDATE_PIVOT", {
57+
pivotId,
58+
pivot: { ...definition, collapsedDomains: newDomains },
59+
});
60+
}
61+
62+
get isCollapsed() {
63+
const pivotCell = this.env.model.getters.getPivotCellFromPosition(this.props.cellPosition);
64+
const pivotId = this.env.model.getters.getPivotIdFromPosition(this.props.cellPosition);
65+
if (!pivotId || pivotCell.type !== "HEADER") {
66+
return false;
67+
}
68+
const definition = this.env.model.getters.getPivotCoreDefinition(pivotId);
69+
const domains = definition.collapsedDomains?.[pivotCell.dimension] ?? [];
70+
return domains?.some((domain) => deepEquals(domain, pivotCell.domain));
71+
}
72+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<templates>
2+
<t t-name="o-spreadsheet-PivotCollapseIcon">
3+
<div
4+
class="o-pivot-collapse-icon o-hoverable-button d-flex align-items-center justify-content-center"
5+
t-on-click="onClick">
6+
<t t-if="isCollapsed" t-call="o-spreadsheet-Icon.PLUS"/>
7+
<t t-else="" t-call="o-spreadsheet-Icon.MINUS"/>
8+
</div>
9+
</t>
10+
</templates>

src/components/pivot_html_renderer/pivot_html_renderer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export class PivotHTMLRenderer extends Component<Props, SpreadsheetChildEnv> {
105105
});
106106

107107
setup() {
108-
const table = this.pivot.getTableStructure();
108+
const table = this.pivot.getExpandedTableStructure();
109109
const formulaId = this.env.model.getters.getPivotFormulaId(this.props.pivotId);
110110
this.data = {
111111
columns: this._buildColHeaders(formulaId, table),
@@ -153,7 +153,7 @@ export class PivotHTMLRenderer extends Component<Props, SpreadsheetChildEnv> {
153153
* The parent of "January" is "Australia"
154154
*/
155155
private addRecursiveRow(index: number): number[] {
156-
const rows = this.pivot.getTableStructure().rows;
156+
const rows = this.pivot.getExpandedTableStructure().rows;
157157
const row = [...rows[index].values];
158158
if (row.length <= 1) {
159159
return [index];

src/components/side_panel/pivot/pivot_side_panel/pivot_side_panel_store.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import { _t } from "../../../../translation";
99
import { Command, UID } from "../../../../types";
1010
import {
1111
PivotCoreDefinition,
12+
PivotCoreDimension,
1213
PivotCoreMeasure,
1314
PivotDimension,
15+
PivotDomain,
1416
PivotField,
1517
PivotFields,
1618
PivotMeasure,
@@ -198,6 +200,14 @@ export class PivotSidePanelStore extends SpreadsheetStore {
198200
})),
199201
sortedColumn: this.shouldKeepSortedColumn(definition) ? definition.sortedColumn : undefined,
200202
};
203+
if (cleanedDefinition.collapsedDomains) {
204+
const { COL, ROW } = cleanedDefinition.collapsedDomains;
205+
cleanedDefinition.collapsedDomains = {
206+
COL: COL.filter((domain) => this.areDomainFieldsValid(domain, cleanedDefinition.columns)),
207+
ROW: ROW.filter((domain) => this.areDomainFieldsValid(domain, cleanedDefinition.rows)),
208+
};
209+
}
210+
201211
if (!this.draft && deepEquals(coreDefinition, cleanedDefinition)) {
202212
return;
203213
}
@@ -301,4 +311,16 @@ export class PivotSidePanelStore extends SpreadsheetStore {
301311
deepEquals(oldDefinition.columns, newDefinition.columns)
302312
);
303313
}
314+
315+
private areDomainFieldsValid(domain: PivotDomain, dims: PivotCoreDimension[]) {
316+
const fieldsNameWithGranularity = dims.map(
317+
({ fieldName, granularity }) => fieldName + (granularity ? `:${granularity}` : "")
318+
);
319+
for (let i = 0; i < domain.length; i++) {
320+
if (domain[i].field !== fieldsNameWithGranularity[i]) {
321+
return false;
322+
}
323+
}
324+
return true;
325+
}
304326
}

src/functions/module_lookup.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ import {
1212
getPivotId,
1313
} from "./helper_lookup";
1414
import {
15+
LinearSearchMode,
1516
dichotomicSearch,
1617
expectNumberRangeError,
1718
generateMatrix,
1819
isEvaluationError,
1920
linearSearch,
20-
LinearSearchMode,
2121
strictToInteger,
2222
toBoolean,
2323
toMatrix,
@@ -877,7 +877,7 @@ export const PIVOT = {
877877
if (error) {
878878
return error;
879879
}
880-
const table = pivot.getTableStructure();
880+
const table = pivot.getCollapsedTableStructure();
881881
const cells = table.getPivotCells(_includedTotal, _includeColumnHeaders);
882882
const headerRows = _includeColumnHeaders ? table.columns.length : 0;
883883
const pivotTitle = this.getters.getPivotDisplayName(pivotId);

src/helpers/pivot/pivot_domain_helpers.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
PivotDomain,
77
PivotNode,
88
} from "../../types";
9-
import { clip, deepCopy } from "../misc";
9+
import { clip, deepCopy, deepEquals } from "../misc";
1010

1111
export const PREVIOUS_VALUE = "(previous)";
1212
export const NEXT_VALUE = "(next)";
@@ -51,10 +51,18 @@ function getFieldValueInDomain(
5151
}
5252

5353
export function isDomainIsInPivot(pivot: Pivot, domain: PivotDomain) {
54+
for (const node of domain) {
55+
if (
56+
pivot.definition.rows.find((row) => row.nameWithGranularity === node.field) === undefined &&
57+
pivot.definition.columns.find((col) => col.nameWithGranularity === node.field) === undefined
58+
) {
59+
return false;
60+
}
61+
}
5462
const { rowDomain, colDomain } = domainToColRowDomain(pivot, domain);
5563
return (
56-
checkIfDomainInInTree(rowDomain, pivot.getTableStructure().getRowTree()) &&
57-
checkIfDomainInInTree(colDomain, pivot.getTableStructure().getColTree())
64+
checkIfDomainInInTree(rowDomain, pivot.getExpandedTableStructure().getRowTree()) &&
65+
checkIfDomainInInTree(colDomain, pivot.getExpandedTableStructure().getColTree())
5866
);
5967
}
6068

@@ -168,8 +176,8 @@ export function getPreviousOrNextValueDomain(
168176
const dimension = getFieldDimensionType(pivot, fieldNameWithGranularity);
169177
const tree =
170178
dimension === "row"
171-
? pivot.getTableStructure().getRowTree()
172-
: pivot.getTableStructure().getColTree();
179+
? pivot.getExpandedTableStructure().getRowTree()
180+
: pivot.getExpandedTableStructure().getColTree();
173181
const dimDomain = getDimensionDomain(pivot, dimension, domain);
174182

175183
const currentTreeNode = walkDomainTree(dimDomain, tree, fieldNameWithGranularity);
@@ -268,3 +276,10 @@ export function sortPivotTree(
268276
}
269277
return sortedTree;
270278
}
279+
280+
export function isParentDomain(domain: PivotDomain, parentDomain: PivotDomain) {
281+
return (
282+
domain.length > parentDomain.length &&
283+
parentDomain.every((node, i) => deepEquals(node, domain[i]))
284+
);
285+
}

0 commit comments

Comments
 (0)