diff --git a/src/components/figures/figure/figure.css b/src/components/figures/figure/figure.css index e97a198455..1a3a1d377a 100644 --- a/src/components/figures/figure/figure.css +++ b/src/components/figures/figure/figure.css @@ -2,6 +2,7 @@ div.o-figure { color-scheme: light; color: var(--os-text-body); + border: solid var(--os-figure-border-color); position: absolute; width: 100%; diff --git a/src/components/figures/figure/figure.ts b/src/components/figures/figure/figure.ts index 26b60d49a2..618eedd347 100644 --- a/src/components/figures/figure/figure.ts +++ b/src/components/figures/figure/figure.ts @@ -69,11 +69,11 @@ export class FigureComponent extends Component { if (this.env.isDashboard()) { return 0; } - return this.isSelected ? ACTIVE_BORDER_WIDTH : this.borderWidth; + return this.isSelected ? ACTIVE_BORDER_WIDTH : 0; } getBorderStyle(position: "top" | "right" | "bottom" | "left"): string { - return `border-${position}-width: ${this.getBorderWidth()}px;`; + return `border-${position}-width: ${this.getBorderWidth()}px`; } get wrapperStyle() { @@ -86,6 +86,25 @@ export class FigureComponent extends Component { }); } + get figureStyle() { + const properties: CSSProperties = { + "border-width": `${this.borderWidth}px`, + }; + if (this.isSelected) { + // ADRM TODO: wait for real carousel data view and improve this (we have 2 different borders handler, and it's ugly w/ data view) + properties["border-color"] = "transparent"; + } + return cssPropertiesToCss(properties) + ";" + this.props.style; + } + + get figureClass() { + return ( + this.props.class + + (this.props.figureUI.roundedBorders ? " rounded-3" : "") + + (this.props.figureUI.shadow ? " shadow" : "") + ); + } + getResizerPosition(resizer: ResizeAnchor): string { const anchorCenteringOffset = (ANCHOR_SIZE - ACTIVE_BORDER_WIDTH) / 2; const style: CSSProperties = {}; diff --git a/src/components/figures/figure/figure.xml b/src/components/figures/figure/figure.xml index 21c681213b..5277fc10e7 100644 --- a/src/components/figures/figure/figure.xml +++ b/src/components/figures/figure/figure.xml @@ -6,13 +6,13 @@ t-att-style="wrapperStyle" t-ref="figureWrapper">
{ static template = "o-spreadsheet-CarouselPanel"; static props = { onCloseSidePanel: Function, figureId: String }; - static components = { Section, TextInput, TextStyler, CogWheelMenu }; + static components = { Section, TextInput, TextStyler, CogWheelMenu, FigureOptions }; DEFAULT_CAROUSEL_TITLE_STYLE = DEFAULT_CAROUSEL_TITLE_STYLE; diff --git a/src/components/side_panel/carousel_panel/carousel_panel.xml b/src/components/side_panel/carousel_panel/carousel_panel.xml index 09245bfa37..944b3d4017 100644 --- a/src/components/side_panel/carousel_panel/carousel_panel.xml +++ b/src/components/side_panel/carousel_panel/carousel_panel.xml @@ -51,19 +51,22 @@
- - diff --git a/src/components/side_panel/chart/bar_chart/bar_chart_design_panel.xml b/src/components/side_panel/chart/bar_chart/bar_chart_design_panel.xml index 7b36213594..314ba60658 100644 --- a/src/components/side_panel/chart/bar_chart/bar_chart_design_panel.xml +++ b/src/components/side_panel/chart/bar_chart/bar_chart_design_panel.xml @@ -31,5 +31,6 @@ /> + diff --git a/src/components/side_panel/chart/building_blocks/figure_options/figure_options.ts b/src/components/side_panel/chart/building_blocks/figure_options/figure_options.ts new file mode 100644 index 0000000000..9aa922bae4 --- /dev/null +++ b/src/components/side_panel/chart/building_blocks/figure_options/figure_options.ts @@ -0,0 +1,54 @@ +import { Component } from "@odoo/owl"; +import { Figure, UID } from "../../../../.."; +import { SpreadsheetChildEnv } from "../../../../../types/spreadsheet_env"; +import { Checkbox } from "../../../components/checkbox/checkbox"; +import { SidePanelCollapsible } from "../../../components/collapsible/side_panel_collapsible"; +import { Section } from "../../../components/section/section"; + +interface Props { + figureId: UID; + isInitiallyCollapsed?: boolean; +} + +export class FigureOptions extends Component { + static template = "o-spreadsheet.FigureOptions"; + static components = { Section, Checkbox, SidePanelCollapsible }; + static props = { + figureId: String, + isInitiallyCollapsed: { type: Boolean, optional: true }, + }; + + get figure(): Figure { + const figure = this.env.model.getters.getFigure(this.sheetId, this.props.figureId); + if (!figure) { + throw new Error(`Figure with id ${this.props.figureId} not found`); + } + return figure; + } + + get sheetId(): UID { + const sheetId = this.env.model.getters.getFigureSheetId(this.props.figureId); + if (!sheetId) { + throw new Error(`Sheet id for figure with id ${this.props.figureId} not found`); + } + return sheetId; + } + + updateRoundedBorders(value: boolean) { + this.env.model.dispatch("UPDATE_FIGURE", { + sheetId: this.sheetId, + figureId: this.props.figureId, + ...this.figure, + roundedBorders: value, + }); + } + + updateShadow(value: boolean) { + this.env.model.dispatch("UPDATE_FIGURE", { + sheetId: this.sheetId, + figureId: this.props.figureId, + ...this.figure, + shadow: value, + }); + } +} diff --git a/src/components/side_panel/chart/building_blocks/figure_options/figure_options.xml b/src/components/side_panel/chart/building_blocks/figure_options/figure_options.xml new file mode 100644 index 0000000000..2d9f45113d --- /dev/null +++ b/src/components/side_panel/chart/building_blocks/figure_options/figure_options.xml @@ -0,0 +1,24 @@ + + + + +
+ + +
+
+
+
+
diff --git a/src/components/side_panel/chart/chart_with_axis/design_panel.ts b/src/components/side_panel/chart/chart_with_axis/design_panel.ts index e9c2d39410..be1c14df09 100644 --- a/src/components/side_panel/chart/chart_with_axis/design_panel.ts +++ b/src/components/side_panel/chart/chart_with_axis/design_panel.ts @@ -9,6 +9,7 @@ import { AxisDefinition, AxisDesignEditor, } from "../building_blocks/axis_design/axis_design_editor"; +import { FigureOptions } from "../building_blocks/figure_options/figure_options"; import { GeneralDesignEditor } from "../building_blocks/general_design/general_design_editor"; import { ChartHumanizeNumbers } from "../building_blocks/humanize_numbers/humanize_numbers"; import { ChartLegend } from "../building_blocks/legend/legend"; @@ -29,6 +30,7 @@ export class ChartWithAxisDesignPanel< ChartLegend, ChartShowValues, ChartHumanizeNumbers, + FigureOptions, }; static props = ChartSidePanelPropsObject; @@ -43,4 +45,8 @@ export class ChartWithAxisDesignPanel< } return axes; } + + get figureId() { + return this.env.model.getters.getFigureIdFromChartId(this.props.chartId); + } } diff --git a/src/plugins/core/figures.ts b/src/plugins/core/figures.ts index 0b79af50d6..dbb75db97a 100644 --- a/src/plugins/core/figures.ts +++ b/src/plugins/core/figures.ts @@ -277,6 +277,8 @@ export class FigurePlugin extends CorePlugin implements FigureState case "row": case "width": case "height": + case "roundedBorders": + case "shadow": if (value !== undefined) { this.history.update("figures", sheetId, figure.id, key, value as number); } diff --git a/src/types/figure.ts b/src/types/figure.ts index e9aa14ed26..5780ffbf43 100644 --- a/src/types/figure.ts +++ b/src/types/figure.ts @@ -7,6 +7,8 @@ export interface FigureInfo { width: Pixel; height: Pixel; tag: string; + roundedBorders?: boolean; + shadow?: boolean; } export interface Figure extends FigureInfo, AnchorOffset {} diff --git a/tests/figures/figure_component.test.ts b/tests/figures/figure_component.test.ts index 0c70b3532e..4b42c2f358 100644 --- a/tests/figures/figure_component.test.ts +++ b/tests/figures/figure_component.test.ts @@ -33,6 +33,7 @@ import { setCellContent, setViewportOffset, setZoom, + updateFigure, } from "../test_helpers/commands_helpers"; import { TEST_CHART_DATA } from "../test_helpers/constants"; import { @@ -1854,6 +1855,26 @@ describe("figures", () => { }); }); }); + + test("Can display a figure with a shadow", async () => { + createFigure(model, { id: "figureId" }); + await nextTick(); + expect(".o-figure").not.toHaveClass("shadow"); + + updateFigure(model, { figureId: "figureId", shadow: true }); + await nextTick(); + expect(".o-figure").toHaveClass("shadow"); + }); + + test("Can display a figure with rounded borders", async () => { + createFigure(model, { id: "figureId" }); + await nextTick(); + expect(".o-figure").not.toHaveClass("rounded-3"); + + updateFigure(model, { figureId: "figureId", roundedBorders: true }); + await nextTick(); + expect(".o-figure").toHaveClass("rounded-3"); + }); }); describe.each(ZOOM_VALUES.map((zoom) => zoom / 100))("figures with zoom %s", (zoom) => { diff --git a/tests/side_panels/building_blocks/figure_options.test.ts b/tests/side_panels/building_blocks/figure_options.test.ts new file mode 100644 index 0000000000..e16c56c343 --- /dev/null +++ b/tests/side_panels/building_blocks/figure_options.test.ts @@ -0,0 +1,46 @@ +import { FigureOptions } from "../../../src/components/side_panel/chart/building_blocks/figure_options/figure_options"; +import { Model } from "../../../src/model"; +import { createFigure, simulateClick } from "../../test_helpers"; +import { mountComponentWithPortalTarget } from "../../test_helpers/helpers"; + +let model: Model; +let sheetId: string; +const figureId = "figureId"; + +beforeEach(() => { + model = new Model(); + sheetId = model.getters.getActiveSheetId(); +}); + +async function mountComponent(props: FigureOptions["props"]) { + await mountComponentWithPortalTarget(FigureOptions, { + props: { ...props }, + model, + }); +} + +describe("Figure options", () => { + test("Can add a shadow to the figure", async () => { + createFigure(model, { figureId }); + await mountComponent({ figureId }); + + expect("input[name='shadow']").toHaveValue(false); + expect(model.getters.getFigure(sheetId, figureId)?.shadow).toBe(undefined); + await simulateClick("input[name='shadow']"); + + expect(model.getters.getFigure(sheetId, figureId)?.shadow).toBe(true); + expect("input[name='shadow']").toHaveValue(true); + }); + + test("Can add rounded borders to the figure", async () => { + createFigure(model, { figureId }); + await mountComponent({ figureId }); + + expect("input[name='roundedBorders']").toHaveValue(false); + expect(model.getters.getFigure(sheetId, figureId)?.roundedBorders).toBe(undefined); + await simulateClick("input[name='roundedBorders']"); + + expect(model.getters.getFigure(sheetId, figureId)?.roundedBorders).toBe(true); + expect("input[name='roundedBorders']").toHaveValue(true); + }); +}); diff --git a/tests/test_helpers/commands_helpers.ts b/tests/test_helpers/commands_helpers.ts index 6273bcdc6a..a9b778c981 100644 --- a/tests/test_helpers/commands_helpers.ts +++ b/tests/test_helpers/commands_helpers.ts @@ -181,8 +181,13 @@ export function createFigure( }); } -export function updateFigure(model: Model, cmd: Omit) { - return model.dispatch("UPDATE_FIGURE", cmd); +export function updateFigure( + model: Model, + cmd: Partial> & { figureId: UID } +) { + const sheetId = model.getters.getFigureSheetId(cmd.figureId)!; + const figure = model.getters.getFigure(sheetId, cmd.figureId)!; + return model.dispatch("UPDATE_FIGURE", { ...figure, ...cmd, sheetId, figureId: cmd.figureId }); } export function deleteFigure(