diff --git a/src/components/figures/chart/chartJs/chartjs_show_values_plugin.ts b/src/components/figures/chart/chartJs/chartjs_show_values_plugin.ts index 77f65ef7c0..347fc914d3 100644 --- a/src/components/figures/chart/chartJs/chartjs_show_values_plugin.ts +++ b/src/components/figures/chart/chartJs/chartjs_show_values_plugin.ts @@ -87,6 +87,10 @@ function drawLineOrBarOrRadarChartValues( if (isNaN(value)) { continue; } + const valueToDisplay = options.callback(Number(value), dataset, i); + if (valueToDisplay === "") { + continue; + } const point = dataset.data[i]; const xPosition = point.x; @@ -122,7 +126,6 @@ function drawLineOrBarOrRadarChartValues( ctx.fillStyle = point.options.backgroundColor; ctx.strokeStyle = options.background || "#ffffff"; - const valueToDisplay = options.callback(Number(value), dataset, i); drawTextWithBackground(valueToDisplay, xPosition, yPosition, ctx); } } diff --git a/src/components/icons/icons.xml b/src/components/icons/icons.xml index b282de1d96..5ff3ffd408 100644 --- a/src/components/icons/icons.xml +++ b/src/components/icons/icons.xml @@ -611,6 +611,11 @@ + +
+ +
+
@@ -1048,4 +1053,22 @@ /> + + + + + + + + + + + diff --git a/src/components/selection_input/selection_input.css b/src/components/selection_input/selection_input.css index 217c518215..2296293584 100644 --- a/src/components/selection_input/selection_input.css +++ b/src/components/selection_input/selection_input.css @@ -27,4 +27,7 @@ font-size: calc(100% + 4px); } } + .o-selection-extension { + margin-bottom: -10px; + } } diff --git a/src/components/selection_input/selection_input.ts b/src/components/selection_input/selection_input.ts index 22045cc1f8..dcaab1d98a 100644 --- a/src/components/selection_input/selection_input.ts +++ b/src/components/selection_input/selection_input.ts @@ -1,10 +1,12 @@ import { Component, onWillUpdateProps, useEffect, useRef, useState } from "@odoo/owl"; +import { ActionSpec } from "../../actions/action"; import { deepEquals, range } from "../../helpers"; import { Store, useLocalStore } from "../../store_engine"; import { Color, SpreadsheetChildEnv } from "../../types"; import { cssPropertiesToCss } from "../helpers/css"; import { useDragAndDropListItems } from "../helpers/drag_and_drop_dom_items_hook"; import { updateSelectionWithArrowKeys } from "../helpers/selection_helpers"; +import { CogWheelMenu } from "../side_panel/components/cog_wheel_menu/cog_wheel_menu"; import { RangeInputValue, SelectionInputStore } from "./selection_input_store"; interface Props { @@ -20,6 +22,8 @@ interface Props { colors?: Color[]; disabledRanges?: boolean[]; disabledRangeTitle?: string; + getRowMenuItems?: (index: number) => ActionSpec[] | undefined; + getRowExtensions?: (index: number) => SelectionInputRowExtension[] | undefined; } type SelectionRangeEditMode = "select-range" | "text-edit"; @@ -35,6 +39,20 @@ interface SelectionRange extends Omit { color?: Color; disabled?: boolean; } + +export interface SelectionInputRowExtension { + key: string; + title?: string; + icon?: string; + ranges: string[]; + hasSingleRange?: boolean; + isInvalid?: boolean; + onSelectionChanged?: (ranges: string[]) => void; + onSelectionConfirmed?: () => void; + onSelectionRemoved?: (index: number) => void; + onRemoveExtension?: () => void; + removeLabel?: string; +} /** * This component can be used when the user needs to input some * ranges. He can either input the ranges with the regular DOM `` @@ -58,7 +76,10 @@ export class SelectionInput extends Component { colors: { type: Array, optional: true, default: [] }, disabledRanges: { type: Array, optional: true, default: [] }, disabledRangeTitle: { type: String, optional: true }, + getRowMenuItems: { type: Function, optional: true }, + getRowExtensions: { type: Function, optional: true }, }; + static components = { CogWheelMenu, SelectionInput }; private state: State = useState({ isMissing: false, mode: "select-range", @@ -92,6 +113,18 @@ export class SelectionInput extends Component { return this.store.disabledRanges.some(Boolean); } + getRowMenuItems(index: number): ActionSpec[] { + return this.props.getRowMenuItems?.(index) || []; + } + + hasMenu(index: number): boolean { + return this.getRowMenuItems(index).length > 0; + } + + getRowExtensions(index: number): SelectionInputRowExtension[] { + return this.props.getRowExtensions?.(index) || []; + } + setup() { useEffect( () => this.focusedInput.el?.focus(), diff --git a/src/components/selection_input/selection_input.xml b/src/components/selection_input/selection_input.xml index 22460e6c19..4240c3972d 100644 --- a/src/components/selection_input/selection_input.xml +++ b/src/components/selection_input/selection_input.xml @@ -1,58 +1,96 @@ -
+
- - - -
- +
- - - - + t-if="ranges.length > 1 and props.onSelectionReordered" + title="Reorder range" + t-on-pointerdown="(ev) => this.startDragAndDrop(range.id, ev)" + class="o-drag-handle d-flex align-items-center mb-2 o-button-icon"> + +
+ + + + + + + +
+
+ + +
+
+
+ + +
+
+ +
+
+ +
+
+ ✕ +
+
+
-
-
- - +
+ + + +
+
+ + +
@@ -363,36 +373,46 @@ exports[`Spreadsheet pivot side panel It should display only the selection input
-
- - -
+ - -
-
- - +
+ +
+ + + +
+
+ + +
diff --git a/tests/side_panels/building_blocks/__snapshots__/data_series.test.ts.snap b/tests/side_panels/building_blocks/__snapshots__/data_series.test.ts.snap index 221115c78b..199b4966ad 100644 --- a/tests/side_panels/building_blocks/__snapshots__/data_series.test.ts.snap +++ b/tests/side_panels/building_blocks/__snapshots__/data_series.test.ts.snap @@ -19,24 +19,34 @@ exports[`Data Series Can render a data series component 1`] = `
-
- - +
+ + + +
+
+ + +
diff --git a/tests/side_panels/building_blocks/__snapshots__/label_range.test.ts.snap b/tests/side_panels/building_blocks/__snapshots__/label_range.test.ts.snap index 96cc5baf86..85d6fc512a 100644 --- a/tests/side_panels/building_blocks/__snapshots__/label_range.test.ts.snap +++ b/tests/side_panels/building_blocks/__snapshots__/label_range.test.ts.snap @@ -14,24 +14,34 @@ exports[`Label range Can add options to the label range component 1`] = `
-
- - +
+ + + +
+
+ + +
@@ -76,24 +86,34 @@ exports[`Label range Can render a label range component 1`] = `
-
- - +
+ + + +
+
+ + +
diff --git a/tests/side_panels/building_blocks/chart_show_values.test.ts b/tests/side_panels/building_blocks/chart_show_values.test.ts new file mode 100644 index 0000000000..cf5e9120b0 --- /dev/null +++ b/tests/side_panels/building_blocks/chart_show_values.test.ts @@ -0,0 +1,112 @@ +import { ChartShowValues } from "../../../src/components/side_panel/chart/building_blocks/show_values/show_values"; +import { DispatchResult, UID } from "../../../src/types"; +import { setInputValueAndTrigger, simulateClick } from "../../test_helpers/dom_helper"; +import { mountComponent } from "../../test_helpers/helpers"; + +async function mountChartShowValues(props: Partial) { + const defaultProps: ChartShowValues["props"] = { + chartId: "chart-id" as UID, + definition: { + showValues: false, + }, + updateChart: () => DispatchResult.Success, + canUpdateChart: () => DispatchResult.Success, + defaultValue: false, + }; + return mountComponent(ChartShowValues, { + props: { ...defaultProps, ...props }, + }); +} + +const chartId = "chart-id" as UID; + +describe("ChartShowValues", () => { + test("reflects the showValues state from the chart definition", async () => { + const { fixture } = await mountChartShowValues({ + chartId, + definition: { showValues: true }, + }); + + const checkbox = fixture.querySelector("input[type='checkbox']") as HTMLInputElement; + expect(checkbox.checked).toBe(true); + }); + + test("uses the default falsy value when the definition does not specify showValues", async () => { + const { fixture } = await mountChartShowValues({ + chartId, + definition: {}, + }); + + const checkbox = fixture.querySelector("input[type='checkbox']") as HTMLInputElement; + expect(checkbox.checked).toBe(false); + }); + + test("updates the chart when the checkbox value changes", async () => { + const updateChart = jest.fn(); + await mountChartShowValues({ + chartId, + definition: { showValues: false }, + updateChart, + }); + + await simulateClick("input[type='checkbox']"); + expect(updateChart).toHaveBeenCalledWith(chartId, { showValues: true }); + + await simulateClick("input[type='checkbox']"); + expect(updateChart).toHaveBeenCalledWith(chartId, { showValues: false }); + }); + + test("can change the mode when modes and onModeChanged props are provided", async () => { + const onModeChanged = jest.fn(); + const modes = [ + { value: "value", label: "As Value" }, + { value: "label", label: "As Label" }, + ]; + const { fixture } = await mountChartShowValues({ + chartId, + definition: { showValues: true, showValuesMode: "value" }, + modes, + onModeChanged, + }); + + const select = fixture.querySelector("select") as HTMLSelectElement; + expect(select.value).toBe("value"); + const options = select.querySelectorAll("option"); + expect(options).toHaveLength(2); + expect(options[0].value).toBe("value"); + expect(options[0].textContent).toBe("As Value"); + expect(options[1].value).toBe("label"); + expect(options[1].textContent).toBe("As Label"); + + await setInputValueAndTrigger(select, "label"); + expect(onModeChanged).toHaveBeenCalledWith("label"); + }); + + test("does not render the mode selector when modes prop is empty", async () => { + const onModeChanged = jest.fn(); + const { fixture } = await mountChartShowValues({ + chartId, + definition: { showValues: true, showValuesMode: "value" }, + modes: [], + onModeChanged, + }); + + const select = fixture.querySelector("select") as HTMLSelectElement; + expect(select).toBeFalsy(); + }); + + test("does not render the mode selector when onModeChanged prop is not provided", async () => { + const modes = [ + { value: "value", label: "As Value" }, + { value: "label", label: "As Label" }, + ]; + const { fixture } = await mountChartShowValues({ + chartId, + definition: { showValues: true, showValuesMode: "value" }, + modes, + }); + + const select = fixture.querySelector("select") as HTMLSelectElement; + expect(select).toBeFalsy(); + }); +}); diff --git a/tests/test_helpers/chart_helpers.ts b/tests/test_helpers/chart_helpers.ts index 4164200a69..2ef9d5727a 100644 --- a/tests/test_helpers/chart_helpers.ts +++ b/tests/test_helpers/chart_helpers.ts @@ -122,6 +122,7 @@ export const GENERAL_CHART_CREATION_CONTEXT: Required = { axesDesign: {}, fillArea: true, showValues: false, + showValuesMode: "value", hideDataMarkers: false, funnelColors: [], showLabels: false,