Skip to content

Commit

Permalink
[IMP] pivot: allow to sort the pivot on any column
Browse files Browse the repository at this point in the history
This commit allows the user to right click on a pivot cell to sort
the pivot on that column.

closes #4998

Task: 3989395
Signed-off-by: Lucas Lefèvre (lul) <[email protected]>
  • Loading branch information
hokolomopo authored and LucasLefevre committed Dec 6, 2024
1 parent e858997 commit 4133585
Show file tree
Hide file tree
Showing 22 changed files with 1,070 additions and 109 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { PivotDimension } from "./pivot_dimension/pivot_dimension";
import { PivotDimensionGranularity } from "./pivot_dimension_granularity/pivot_dimension_granularity";
import { PivotDimensionOrder } from "./pivot_dimension_order/pivot_dimension_order";
import { PivotMeasureEditor } from "./pivot_measure/pivot_measure";
import { PivotSortSection } from "./pivot_sort_section/pivot_sort_section";

interface Props {
definition: PivotRuntimeDefinition;
Expand All @@ -53,6 +54,7 @@ export class PivotLayoutConfigurator extends Component<Props, SpreadsheetChildEn
PivotDimensionOrder,
PivotDimensionGranularity,
PivotMeasureEditor,
PivotSortSection,
};
static props = {
definition: Object,
Expand Down Expand Up @@ -235,9 +237,14 @@ export class PivotLayoutConfigurator extends Component<Props, SpreadsheetChildEn

updateMeasure(measure: PivotMeasure, newMeasure: PivotMeasure) {
const { measures }: { measures: PivotCoreMeasure[] } = this.props.definition;
this.props.onDimensionsUpdated({

const update: Partial<PivotCoreDefinition> = {
measures: measures.map((m) => (m.id === measure.id ? newMeasure : m)),
});
};
if (this.props.definition.sortedColumn?.measure === measure.id) {
update.sortedColumn = { ...this.props.definition.sortedColumn, measure: newMeasure.id };
}
this.props.onDimensionsUpdated(update);
}

private getMeasureId(fieldName: string, aggregator?: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,6 @@
</div>
</t>
</div>
<PivotSortSection definition="props.definition" pivotId="props.pivotId"/>
</t>
</templates>
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Component } from "@odoo/owl";
import { SpreadsheetChildEnv } from "../../../../..";
import { GRAY_100, GRAY_300, PRIMARY_BUTTON_BG } from "../../../../../constants";
import { formatValue } from "../../../../../helpers";
import {
getFieldDisplayName,
isSortedColumnValid,
} from "../../../../../helpers/pivot/pivot_helpers";
import { PivotRuntimeDefinition } from "../../../../../helpers/pivot/pivot_runtime_definition";
import { _t } from "../../../../../translation";
import { PivotDomain, UID } from "../../../../../types";
import { css } from "../../../../helpers";
import { Section } from "../../../components/section/section";

interface Props {
definition: PivotRuntimeDefinition;
pivotId: UID;
}

css/* scss */ `
.o-pivot-sort {
.o-sort-card {
width: fit-content;
background-color: ${GRAY_100};
border: 1px solid ${GRAY_300};
.o-sort-value {
color: ${PRIMARY_BUTTON_BG};
}
}
}
`;

export class PivotSortSection extends Component<Props, SpreadsheetChildEnv> {
static template = "o-spreadsheet-PivotSortSection";
static components = {
Section,
};
static props = {
definition: Object,
pivotId: String,
};

get hasValidSort() {
const pivot = this.env.model.getters.getPivot(this.props.pivotId);
return (
!!this.props.definition.sortedColumn &&
isSortedColumnValid(this.props.definition.sortedColumn, pivot)
);
}

get sortDescription() {
const sortOrder =
this.props.definition.sortedColumn?.order === "asc" ? _t("ascending") : _t("descending");
return _t("Sorted on column (%(ascOrDesc)s):", {
ascOrDesc: sortOrder,
});
}

get sortValuesAndFields() {
const sortedColumn = this.props.definition.sortedColumn;
if (!sortedColumn) {
return [];
}
const pivot = this.env.model.getters.getPivot(this.props.pivotId);
const locale = this.env.model.getters.getLocale();

const currentDomain: PivotDomain = [];
const sortValues: { field?: string; value: string }[] = [];
for (const domainItem of sortedColumn.domain) {
currentDomain.push(domainItem);
const { value, format } = pivot.getPivotHeaderValueAndFormat(currentDomain);
const label = formatValue(value, { format, locale });
const field = pivot.definition.getDimension(domainItem.field);
sortValues.push({ field: getFieldDisplayName(field), value: label });
}

if (sortedColumn.domain.length === 0) {
sortValues.push({ value: _t("Total") });
}
const measureLabel = pivot.getMeasure(sortedColumn.measure).displayName;
sortValues.push({ value: measureLabel, field: _t("Measure") });

return sortValues;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<templates>
<t t-name="o-spreadsheet-PivotSortSection">
<Section t-if="hasValidSort" class="'o-pivot-sort'">
<t t-set-slot="title">Sorting</t>
<div t-esc="sortDescription" class="pb-2"/>
<div class="d-flex flex-column gap-2">
<t t-foreach="sortValuesAndFields" t-as="valueAndField" t-key="valueAndField_index">
<div class="o-sort-card d-flex gap-1 px-2">
<t t-if="valueAndField.field">
<span class="fw-bolder" t-esc="valueAndField.field"/>
=
</t>
<span class="fw-bolder o-sort-value" t-esc="valueAndField.value"/>
</div>
</t>
</div>
</Section>
</t>
</templates>
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ export class PivotSidePanelStore extends SpreadsheetStore {
format: measure.format,
display: measure.display,
})),
sortedColumn: this.shouldKeepSortedColumn(definition) ? definition.sortedColumn : undefined,
};
if (!this.draft && deepEquals(coreDefinition, cleanedDefinition)) {
return;
Expand Down Expand Up @@ -265,4 +266,20 @@ export class PivotSidePanelStore extends SpreadsheetStore {
}
return granularitiesPerFields;
}

/**
* Check if we want to keep the sorted column when updating the pivot definition. We should remove it if either
* the measure is not in the new definition or the columns have changed.
*/
private shouldKeepSortedColumn(newDefinition: PivotCoreDefinition) {
const { sortedColumn } = newDefinition;
if (!sortedColumn) {
return true;
}
const oldDefinition = this.getters.getPivotCoreDefinition(this.pivotId);
return (
newDefinition.measures.find((measure) => measure.id === sortedColumn.measure) &&
deepEquals(oldDefinition.columns, newDefinition.columns)
);
}
}
15 changes: 15 additions & 0 deletions src/helpers/pivot/pivot_domain_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,18 @@ export function getRunningTotalDomainKey(
}
return domainToString([...domain.slice(0, index), ...domain.slice(index + 1)]);
}

export function sortPivotTree(
tree: DimensionTree,
baseDomain: PivotDomain,
sortFn: (a: PivotDomain, b: PivotDomain) => number
) {
const sortedTree = [...tree];
const domain = [...baseDomain];
sortedTree.sort((node1, node2) => sortFn([...domain, node1], [...domain, node2]));
for (const node of tree) {
const children = sortPivotTree(node.children, [...domain, node], sortFn);
node.children = children;
}
return sortedTree;
}
25 changes: 25 additions & 0 deletions src/helpers/pivot/pivot_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
PivotDimension,
PivotDomain,
PivotField,
PivotSortedColumn,
PivotTableCell,
} from "../../types/pivot";
import { domainToColRowDomain } from "./pivot_domain_helpers";
Expand Down Expand Up @@ -299,3 +300,27 @@ export function addIndentAndAlignToPivotHeader(
format: `${" ".repeat(indent)}${format}* `,
};
}

export function isSortedColumnValid(sortedColumn: PivotSortedColumn, pivot: Pivot): boolean {
try {
if (!pivot.getMeasure(sortedColumn.measure)) {
return false;
}

const columns = pivot.definition.columns;
for (let i = 0; i < sortedColumn.domain.length; i++) {
if (columns[i].nameWithGranularity !== sortedColumn.domain[i].field) {
return false;
}
const possibleValues: (CellValue | null)[] = pivot
.getPossibleFieldValues(columns[i])
.map((v) => v.value);
if (!possibleValues.includes(sortedColumn.domain[i].value)) {
return false;
}
}
return true;
} catch (e) {
return false;
}
}
93 changes: 93 additions & 0 deletions src/helpers/pivot/pivot_menu_items.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { SortDirection, SpreadsheetChildEnv } from "../..";
import { ActionSpec } from "../../actions/action";
import { _t } from "../../translation";
import { CellValueType } from "../../types";
import { deepEquals } from "../misc";
import { domainToColRowDomain } from "./pivot_domain_helpers";

export const pivotProperties: ActionSpec = {
name: _t("See pivot properties"),
Expand All @@ -18,6 +21,24 @@ export const pivotProperties: ActionSpec = {
icon: "o-spreadsheet-Icon.PIVOT",
};

export const pivotSortingAsc: ActionSpec = {
name: _t("Ascending"),
execute: (env) => sortPivot(env, "asc"),
isActive: (env) => isPivotSortMenuItemActive(env, "asc"),
};

export const pivotSortingDesc: ActionSpec = {
name: _t("Descending"),
execute: (env) => sortPivot(env, "desc"),
isActive: (env) => isPivotSortMenuItemActive(env, "desc"),
};

export const noPivotSorting: ActionSpec = {
name: _t("No sorting"),
execute: (env) => sortPivot(env, "none"),
isActive: (env) => isPivotSortMenuItemActive(env, "none"),
};

export const FIX_FORMULAS: ActionSpec = {
name: _t("Convert to individual formulas"),
execute(env) {
Expand Down Expand Up @@ -56,3 +77,75 @@ export const FIX_FORMULAS: ActionSpec = {
},
icon: "o-spreadsheet-Icon.PIVOT",
};

export function canSortPivot(env: SpreadsheetChildEnv): boolean {
const position = env.model.getters.getActivePosition();
const pivotId = env.model.getters.getPivotIdFromPosition(position);
if (
!pivotId ||
!env.model.getters.isExistingPivot(pivotId) ||
!env.model.getters.isSpillPivotFormula(position)
) {
return false;
}
const pivot = env.model.getters.getPivot(pivotId);
if (!pivot.isValid()) {
return false;
}
const pivotCell = env.model.getters.getPivotCellFromPosition(position);
return pivotCell.type === "VALUE" || pivotCell.type === "MEASURE_HEADER";
}

function sortPivot(env: SpreadsheetChildEnv, order: SortDirection | "none") {
const position = env.model.getters.getActivePosition();
const pivotId = env.model.getters.getPivotIdFromPosition(position);
const pivotCell = env.model.getters.getPivotCellFromPosition(position);
if (pivotCell.type === "EMPTY" || pivotCell.type === "HEADER" || !pivotId) {
return;
}

if (order === "none") {
env.model.dispatch("UPDATE_PIVOT", {
pivotId: pivotId,
pivot: {
...env.model.getters.getPivotCoreDefinition(pivotId),
sortedColumn: undefined,
},
});
return;
}

const pivot = env.model.getters.getPivot(pivotId);
const colDomain = domainToColRowDomain(pivot, pivotCell.domain).colDomain;
env.model.dispatch("UPDATE_PIVOT", {
pivotId: pivotId,
pivot: {
...env.model.getters.getPivotCoreDefinition(pivotId),
sortedColumn: { domain: colDomain, order, measure: pivotCell.measure },
},
});
}

function isPivotSortMenuItemActive(
env: SpreadsheetChildEnv,
order: SortDirection | "none"
): boolean {
const position = env.model.getters.getActivePosition();
const pivotId = env.model.getters.getPivotIdFromPosition(position);
const pivotCell = env.model.getters.getPivotCellFromPosition(position);
if (pivotCell.type === "EMPTY" || pivotCell.type === "HEADER" || !pivotId) {
return false;
}
const pivot = env.model.getters.getPivot(pivotId);
const colDomain = domainToColRowDomain(pivot, pivotCell.domain).colDomain;
const sortedColumn = pivot.definition.sortedColumn;

if (order === "none") {
return !sortedColumn;
}

if (!sortedColumn || sortedColumn.order !== order) {
return false;
}
return sortedColumn.measure === pivotCell.measure && deepEquals(sortedColumn.domain, colDomain);
}
24 changes: 22 additions & 2 deletions src/helpers/pivot/pivot_presentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ import {
isFieldInDomain,
replaceFieldValueInDomain,
} from "./pivot_domain_helpers";
import { AGGREGATORS_FN, toNormalizedPivotValue } from "./pivot_helpers";
import { AGGREGATORS_FN, isSortedColumnValid, toNormalizedPivotValue } from "./pivot_helpers";
import { PivotParams, PivotUIConstructor } from "./pivot_registry";
import { SpreadsheetPivotTable } from "./table_spreadsheet_pivot";

const PERCENT_FORMAT = "0.00%";

Expand Down Expand Up @@ -147,7 +148,7 @@ export default function (PivotClass: PivotUIConstructor) {

private getValuesToAggregate(measure: PivotMeasure, domain: PivotDomain) {
const { rowDomain, colDomain } = domainToColRowDomain(this, domain);
const table = this.getTableStructure();
const table = super.getTableStructure();
const values: FunctionResultObject[] = [];
if (
colDomain.length === 0 &&
Expand Down Expand Up @@ -730,6 +731,25 @@ export default function (PivotClass: PivotUIConstructor) {
}
throw new Error(`Value ${result.value} is not a number`);
}

getTableStructure(): SpreadsheetPivotTable {
const table = super.getTableStructure();
this.sortTableStructure(table);
return table;
}

private sortTableStructure(table: SpreadsheetPivotTable) {
if (!this.definition.sortedColumn || table.isSorted) {
return;
}
const measure = this.definition.sortedColumn.measure;
const isSortValid = isSortedColumnValid(this.definition.sortedColumn, this);
if (isSortValid) {
table.sort(measure, this.definition.sortedColumn, (measure, domain) =>
this._getPivotCellValueAndFormat(measure, domain)
);
}
}
}
return PivotPresentationLayer;
}
Loading

0 comments on commit 4133585

Please sign in to comment.