From 808d9f45005a3fd5f9e07c7726c50ec5f053054b Mon Sep 17 00:00:00 2001 From: "Adrien Minne (adrm)" Date: Fri, 14 Nov 2025 09:00:49 +0100 Subject: [PATCH] [IMP] topbar: improve toolbar and topbar composer This commits improves the usability of the toolbar: - The composer is now in a new line to have more space for tools - Tools button have a bit more padding/margin - Added Insert Chart/Pivot/Function buttons in the toolbar Task: 5231006 --- src/actions/insert_actions.ts | 27 +- .../action_button/action_button.css | 10 +- src/components/icons/icons.xml | 2 +- .../menu_button_tool.ts} | 17 +- .../menu_button_tool.xml} | 6 +- src/components/top_bar/top_bar.css | 18 +- src/components/top_bar/top_bar.xml | 32 +- .../top_bar/top_bar_tools_registry.ts | 62 +- src/index.ts | 6 + src/registries/menus/topbar_menu_registry.ts | 30 - src/registries/toolbar_menu_registry.ts | 11 + .../top_bar_component.test.ts.snap | 1084 +++++---- .../bottom_bar_component.test.ts.snap | 2 +- tests/menus/context_menu_component.test.ts | 1 - tests/menus/menu_items_registry.test.ts | 4 +- .../__snapshots__/chart_title.test.ts.snap | 4 +- .../spreadsheet_component.test.ts.snap | 2099 +++++++++-------- .../spreadsheet/spreadsheet_component.test.ts | 1 - tests/top_bar_component.test.ts | 39 +- 19 files changed, 1881 insertions(+), 1574 deletions(-) rename src/components/top_bar/{number_formats_tool/number_formats_tool.ts => menu_button_tool/menu_button_tool.ts} (75%) rename src/components/top_bar/{number_formats_tool/number_formats_tool.xml => menu_button_tool/menu_button_tool.xml} (67%) diff --git a/src/actions/insert_actions.ts b/src/actions/insert_actions.ts index a610b4d074..263abdccc7 100644 --- a/src/actions/insert_actions.ts +++ b/src/actions/insert_actions.ts @@ -211,37 +211,38 @@ export const insertTable: ActionSpec = { icon: "o-spreadsheet-Icon.PAINT_TABLE", }; -export const insertFunction: ActionSpec = { - name: _t("Function"), - icon: "o-spreadsheet-Icon.FORMULA", -}; - export const insertFunctionSum: ActionSpec = { + id: "insert_function_sum", name: _t("SUM"), execute: (env) => env.startCellEdition(`=SUM(`), }; export const insertFunctionAverage: ActionSpec = { + id: "insert_function_average", name: _t("AVERAGE"), execute: (env) => env.startCellEdition(`=AVERAGE(`), }; export const insertFunctionCount: ActionSpec = { + id: "insert_function_count", name: _t("COUNT"), execute: (env) => env.startCellEdition(`=COUNT(`), }; export const insertFunctionMax: ActionSpec = { + id: "insert_function_max", name: _t("MAX"), execute: (env) => env.startCellEdition(`=MAX(`), }; export const insertFunctionMin: ActionSpec = { + id: "insert_function_min", name: _t("MIN"), execute: (env) => env.startCellEdition(`=MIN(`), }; -export const categorieFunctionAll: ActionSpec = { +export const categoryFunctionAll: ActionSpec = { + id: "category_function_all", name: _t("All"), children: [allFunctionListMenuBuilder], }; @@ -273,6 +274,20 @@ export const categoriesFunctionListMenuBuilder: ActionBuilder = () => { }); }; +export const insertFunction: ActionSpec = { + name: _t("Function"), + icon: "o-spreadsheet-Icon.FORMULA", + children: [ + insertFunctionSum, + insertFunctionAverage, + insertFunctionCount, + insertFunctionMax, + { ...insertFunctionMin, separator: true }, + categoryFunctionAll, + categoriesFunctionListMenuBuilder, + ], +}; + export const insertLink: ActionSpec = { name: _t("Link"), execute: ACTIONS.INSERT_LINK, diff --git a/src/components/action_button/action_button.css b/src/components/action_button/action_button.css index d21abadf14..9d9392c1d2 100644 --- a/src/components/action_button/action_button.css +++ b/src/components/action_button/action_button.css @@ -3,11 +3,17 @@ display: flex; justify-content: center; align-items: center; - margin: 2px 1px; - padding: 0px 1px; + margin: 2px 2px; + padding: 0px 4px; border-radius: 2px; min-width: 22px; + + .o-caret-down { + margin-left: 4px; + width: fit-content; + } } + .o-disabled { opacity: 0.6; cursor: default; diff --git a/src/components/icons/icons.xml b/src/components/icons/icons.xml index 7108b66ee5..2971bec064 100644 --- a/src/components/icons/icons.xml +++ b/src/components/icons/icons.xml @@ -587,7 +587,7 @@ -
+
diff --git a/src/components/top_bar/number_formats_tool/number_formats_tool.ts b/src/components/top_bar/menu_button_tool/menu_button_tool.ts similarity index 75% rename from src/components/top_bar/number_formats_tool/number_formats_tool.ts rename to src/components/top_bar/menu_button_tool/menu_button_tool.ts index af122c129b..c4ae181164 100644 --- a/src/components/top_bar/number_formats_tool/number_formats_tool.ts +++ b/src/components/top_bar/menu_button_tool/menu_button_tool.ts @@ -1,7 +1,6 @@ import { SpreadsheetChildEnv } from "@odoo/o-spreadsheet-engine/types/spreadsheet_env"; import { Component, useRef, useState } from "@odoo/owl"; -import { Action, createAction } from "../../../actions/action"; -import { formatNumberMenuItemSpec } from "../../../registries/menus"; +import { Action, ActionSpec, createAction } from "../../../actions/action"; import { Rect } from "../../../types"; import { ActionButton } from "../../action_button/action_button"; import { getBoundingRectAsPOJO } from "../../helpers/dom_helpers"; @@ -10,6 +9,7 @@ import { MenuPopover } from "../../menu_popover/menu_popover"; interface Props { class: string; + action: ActionSpec; } interface State { @@ -17,11 +17,10 @@ interface State { anchorRect: Rect; } -export class NumberFormatsTool extends Component { - static template = "o-spreadsheet-NumberFormatsTool"; +export class MenuButtonTool extends Component { + static template = "o-spreadsheet-MenuButtonTool"; static components = { MenuPopover, ActionButton }; - static props = { class: String }; - formatNumberMenuItemSpec = formatNumberMenuItemSpec; + static props = { class: String, action: Object }; topBarToolStore!: ToolBarDropdownStore; buttonRef = useRef("buttonRef"); @@ -38,7 +37,7 @@ export class NumberFormatsTool extends Component { if (this.isActive) { this.topBarToolStore.closeDropdowns(); } else { - const menu = createAction(this.formatNumberMenuItemSpec); + const menu = createAction(this.props.action); this.state.menuItems = menu.children(this.env).sort((a, b) => a.sequence - b.sequence); this.state.anchorRect = getBoundingRectAsPOJO(this.buttonRef.el!); this.topBarToolStore.openDropdown(); @@ -48,4 +47,8 @@ export class NumberFormatsTool extends Component { get isActive() { return this.topBarToolStore.isActive; } + + onClose() { + this.topBarToolStore.closeDropdowns(); + } } diff --git a/src/components/top_bar/number_formats_tool/number_formats_tool.xml b/src/components/top_bar/menu_button_tool/menu_button_tool.xml similarity index 67% rename from src/components/top_bar/number_formats_tool/number_formats_tool.xml rename to src/components/top_bar/menu_button_tool/menu_button_tool.xml index bf5432159c..8a884d4e33 100644 --- a/src/components/top_bar/number_formats_tool/number_formats_tool.xml +++ b/src/components/top_bar/menu_button_tool/menu_button_tool.xml @@ -1,7 +1,7 @@ -
+
diff --git a/src/components/top_bar/top_bar.css b/src/components/top_bar/top_bar.css index 0ffabbe247..a44454bc1a 100644 --- a/src/components/top_bar/top_bar.css +++ b/src/components/top_bar/top_bar.css @@ -1,10 +1,4 @@ .o-spreadsheet { - @media (max-width: 1200px) { - .o-topbar-responsive { - flex-direction: column !important; - } - } - @media (max-width: 768px) { .topbar-banner span { overflow: auto; @@ -21,8 +15,8 @@ .o-topbar-divider { border-right: 1px solid var(--os-separator-color); width: 0; - margin: 0 6px; - height: 30px; + margin: 0 4px; + height: 100%; } .o-toolbar-button { @@ -93,11 +87,17 @@ .o-toolbar-button { height: 35px; - width: 31px; + min-width: 31px; .o-toolbar-button.o-mobile-disabled * { color: var(--os-disabled-text-color); cursor: not-allowed; } } } + + .o-topbar-tools-popover { + .o-topbar-divider { + height: 30px; + } + } } diff --git a/src/components/top_bar/top_bar.xml b/src/components/top_bar/top_bar.xml index 15a7892644..1e5ee9c251 100644 --- a/src/components/top_bar/top_bar.xml +++ b/src/components/top_bar/top_bar.xml @@ -25,23 +25,19 @@
-
-
+
+
+ + + Readonly Access + +
+ +
+ +
-
- - - Readonly Access - -
-
+
-
+
@@ -104,7 +100,7 @@ popoverPositioning="'bottom-left'" /> -
+
= { + id: string; component: C; props: PropsOf; sequence: number; @@ -24,6 +25,16 @@ export class ToolBarRegistry { return this; } + replaceChild(childId: string, key: string, value: ToolBarItem): this { + const items = this.content[key]; + const index = items.findIndex((item) => item.id === childId); + if (index === -1) { + throw new Error(`Could not find item with id ${childId} in category ${key}`); + } + this.content[key][index] = value; + return this; + } + getEntries(id: string): ToolBarItem[] { return this.content[id].sort((a, b) => a.sequence - b.sequence); } diff --git a/tests/__snapshots__/top_bar_component.test.ts.snap b/tests/__snapshots__/top_bar_component.test.ts.snap index 37527d286d..79814ef0b0 100644 --- a/tests/__snapshots__/top_bar_component.test.ts.snap +++ b/tests/__snapshots__/top_bar_component.test.ts.snap @@ -57,244 +57,113 @@ exports[`TopBar component simple rendering 1`] = `
+
+
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + -
-
-
- - % - -
- -
-
-
- -
-
-
- -
-
+ - - - % - - - - + + - - + + + - - .0 - - - - + + + + - - - .00 - - - - + + -
- - - - - - - -
- -
- -
- - -
-
- -
-
+ +
- + %
- - -
-
+ +
+ +
+
+ - - - + % + + + + + + + + + .0 - - + + + + - - - + .00 - - + + + +
- - -
- - - - - - - - +
-
- + + -
+ +
+ +
+
+
-
- + + +
- - - - - - - -
-
-
- - - - - - + +
-
+ +
+ + +
+ +
+
+ - - - - - - - + + -
+ + + + + + + -
+ +
+ + + + + + + + +
- -
- -
-
-
+
+ + +
+ +
+
+
- -
- -
-
-
-
+
+ + + + + + + + +
+ + + + + + + + + + + +
+ +
+
+
+ - - - - - - -
- -
- +
+
+ +
-
-
+ + +
+
+ - - - - - - -
- -
- +
+
+ +
-
- + -
-
- - - - - - -
- -
- +
-
+
+ +
+ + + +
+
-
- -
+
+
+ +
-
+ +
+
+ + +
+ +
+
+ + +
+ - - - + + + +
+ + + + + + + +
+ +
+ +
+ +
+ + +
+
- -
-
-
+ + + + +
-
- + class="o-icon fa-small o-caret-down" + > + +
+ +
+ +
+ +
+
+ + +
+ + + +
+ + +
+ - + + +
+
+
+
+
+
+
+ +
+
+
+ + + + + + +
-
+
`; diff --git a/tests/bottom_bar/__snapshots__/bottom_bar_component.test.ts.snap b/tests/bottom_bar/__snapshots__/bottom_bar_component.test.ts.snap index 8677c26ccb..b37eec02dc 100644 --- a/tests/bottom_bar/__snapshots__/bottom_bar_component.test.ts.snap +++ b/tests/bottom_bar/__snapshots__/bottom_bar_component.test.ts.snap @@ -251,7 +251,7 @@ exports[`BottomBar component simple rendering 1`] = ` tabindex="-1" >
{ width: MENU_WIDTH, }; }, - "o-topbar-responsive": () => ({ x: 0, y: 0, width: 1000, height: 1000 }), "o-dropdown": () => ({ x: 0, y: 0, width: 30, height: 30 }), "o-spreadsheet": () => ({ x: 0, y: 0, width: 1000, height: 1000 }), }); diff --git a/tests/menus/menu_items_registry.test.ts b/tests/menus/menu_items_registry.test.ts index 59e7cac9ef..f68e16bc6e 100644 --- a/tests/menus/menu_items_registry.test.ts +++ b/tests/menus/menu_items_registry.test.ts @@ -1009,7 +1009,7 @@ describe("Menu Item actions", () => { }); const env = makeTestEnv(); const allFunctions = getNode( - ["insert", "insert_function", "categorie_function_all"], + ["insert", "insert_function", "category_function_all"], env ).children(env); expect(allFunctions.map((f) => f.name(env))).toContain("TEST.FUNC"); @@ -1037,7 +1037,7 @@ describe("Menu Item actions", () => { const functionCategories = getNode(["insert", "insert_function"], env).children(env); expect(functionCategories.map((f) => f.name(env))).not.toContain("hidden"); const allFunctions = getNode( - ["insert", "insert_function", "categorie_function_all"], + ["insert", "insert_function", "category_function_all"], env ).children(env); expect(allFunctions.map((f) => f.name(env))).not.toContain("HIDDEN.FUNC"); diff --git a/tests/side_panels/building_blocks/__snapshots__/chart_title.test.ts.snap b/tests/side_panels/building_blocks/__snapshots__/chart_title.test.ts.snap index ab78eba95c..028db16128 100644 --- a/tests/side_panels/building_blocks/__snapshots__/chart_title.test.ts.snap +++ b/tests/side_panels/building_blocks/__snapshots__/chart_title.test.ts.snap @@ -97,7 +97,7 @@ exports[`Chart title Can render a chart title component 1`] = `
+
+
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + -
-
-
- - % - -
- -
-
-
- -
-
- -
-
-
+ - - - % - - - - + + - - + + + - - .0 - - - - + + + + - - - .00 - - - - + + -
- - - - - - - -
- -
- -
- - -
-
- -
-
+ +
- + %
- - -
-
+ +
+ +
+
+ - - - + % - - + + + + - - - + .0 + + + + + + + + + .00 - - + + + +
- - -
- - - - - - - - +
-
+ + +
+ + +
+ +
+
+
+ class="o-number-editor d-flex align-items-center o-hoverable-button o-toolbar-button" + title="Font Size" + > + + + +
+ +
+
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
-
- - - - - + + - -
+ +
+
+ + +
+ +
+
+
- + @@ -469,320 +444,421 @@ exports[`Simple Spreadsheet Component simple rendering snapshot 1`] = `
+
+
- + - - +
+ + + + + + -
-
+
+ +
+ +
+
+
-
- - - - - - -
- -
- +
+
+ +
-
-
+ + +
+
+ - - - - - - -
- -
- +
+
+ +
-
-
+ + +
+
+ - - - - - - -
- -
- +
+
+ +
-
-
+ + +
+
+ - - - - - - -
- -
- +
+
+ +
-
+ -
-
+ +
+ +
+
+ -
+
+ +
+ + + + + + + + + + + + + +
+ - - - - - - -
- -
- +
-
+
+ +
+ + + +
+ + +
+ +
+
+
-
- -
+
+
+ +
- -
- -
- - - - - +
-
+ + +
+ + +
-
-
+
-
-
- -
-
- - -
- - + + +
+
+
+
+
+
+
+ +
+
+
+ + + + + + +
-
+
@@ -1074,7 +1150,7 @@ exports[`Simple Spreadsheet Component simple rendering snapshot 1`] = ` tabindex="-1" >
+
+
- -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + % + +
+ +
+
+
+ +
+
+ +
+ +
+
+ - - - + % - - + + + + - - - + .0 - - + + + + - - - - + + .00 + + + +
+
+ +
+ +
+ + +
+ + +
+ +
+
+
+
+ - -
-
+
- - % - -
- -
-
- -
+
- -
-
+ +
+ +
+
+ - - - % - - - - + + + + + + - - - .0 - - - - + + + + + + - - - .00 - - - - + + -
+ + + +
+
- -
- -
-
-
- - -
-
+ + +
+ +
+
-
- - - -
+ - -
+
-
+
- - -
-
- + - - + +
+ + + + + + + + + + + +
+ +
+
+
+
+ +
+ + +
+
- - -
- - - - - - - - +
-
- + + -
-
-
- - - - - - - - -
-
-
- - - - - - - - -
+
+ +
-
-
-
- - - - - - -
- -
- +
+
+ +
-
-
+ + +
+ + +
+ +
+
+ + - - - - - - - -
- -
- -
- - -
-
+
+ + + + + + - - - - - - - -
- -
- -
- - -
-
+ + + + + +
+ - - - - - - -
- -
- +
+
+ +
-
- + -
+ + +
+ +
+
-
- - - - - - - -
- -
- -
- -
- -
- -
+
+
+ +
- -
- -
- - - - - +
-
+ + + + + +
+ +
+ + + + + + + +
-
+ +
@@ -2153,7 +2306,7 @@ exports[`components take the small screen into account 1`] = ` tabindex="-1" >
{ extendMockGetBoundingClientRect({ - "o-topbar-responsive": () => ({ x: 0, y: 0, width: 1000, height: 1000 }), "o-dropdown": () => ({ x: 0, y: 0, width: 30, height: 30 }), "o-spreadsheet": () => { return { x: 0, y: 0, width: spreadsheetWidth, height: 1000 }; diff --git a/tests/top_bar_component.test.ts b/tests/top_bar_component.test.ts index be5c2e5422..547fcee2d4 100644 --- a/tests/top_bar_component.test.ts +++ b/tests/top_bar_component.test.ts @@ -69,7 +69,6 @@ beforeEach(() => { extendMockGetBoundingClientRect({ "o-spreadsheet": () => ({ x: 0, y: 0, width: spreadsheetWidth, height: spreadsheetHeight }), "o-popover": () => ({ width: 50, height: 50 }), - "o-topbar-responsive": () => ({ x: 0, y: 0, width: spreadsheetWidth, height: 1000 }), "o-toolbar-tools": () => ({ x: 0, y: 0, width: spreadsheetWidth, height: topBarToolsHeight }), "tool-container": () => ({ x: 0, y: 0, width: toolWidth, height: topBarToolsHeight }), "more-tools-container": () => ({ @@ -562,6 +561,29 @@ describe("TopBar component", () => { ); }); + test("Can insert a chart with the toolbar", async () => { + const { model } = await mountParent(); + await click(fixture, '.o-topbar-toolbar .o-menu-item-button[title="Insert chart"]'); + expect(model.getters.getChartIds(model.getters.getActiveSheetId()).length).toBe(1); + }); + + test("Can insert a pivot with the toolbar", async () => { + const { model } = await mountParent(); + await click(fixture, '.o-topbar-toolbar .o-menu-item-button[title="Insert pivot table"]'); + expect(model.getters.getPivotIds().length).toBe(1); + }); + + test("Can insert a function with the toolbar", async () => { + const startCellEdition = jest.fn(); + const { model } = await mountParent(new Model(), { startCellEdition }); + setCellContent(model, "A1", "10"); + selectCell(model, "B1"); + await click(fixture, '.o-topbar-toolbar .o-menu-item-button[title="Insert function"]'); + await click(fixture, '.o-popover .o-menu-item[data-name="insert_function_sum"]'); + expect(startCellEdition).toHaveBeenCalledWith("=SUM("); + expect(".o-popover").toHaveCount(0); + }); + test("opening, then closing same menu", async () => { const model = new Model(); setCellContent(model, "B2", "b2"); @@ -1094,20 +1116,15 @@ test("Clicking on a topbar button triggers two renders", async () => { describe("Responsive Top bar behaviour", () => { const categories = topBarToolBarRegistry.getCategories(); describe("items are hidden when the screen is resized", () => { - const topbarToolsWidthThresholds = [750, 650]; - const widthThresholds = topbarToolsWidthThresholds.map((threshold, index) => [ - threshold, - index, - ]); + const widthThresholds = [750, 650]; - test.each(widthThresholds)("Screen slightly smaller than %spx ", async (threshold, index) => { + test.each(widthThresholds)("Screen slightly smaller than %spx ", async (threshold) => { spreadsheetWidth = threshold - 1; await mountParent(); await nextTick(); - const tools = [...fixture.querySelectorAll(".o-toolbar-tools .tool-container")].filter( - (element) => !element.classList.contains("d-none") - ); - expect(tools.length).toBe(categories.length - (index + 1)); + const tools = [...fixture.querySelectorAll(".o-toolbar-tools .tool-container:not(.d-none)")]; + expect(tools.length).toBeLessThan(categories.length); + expect(tools.length).toBe(Math.floor((spreadsheetWidth - moreToolsWidth) / toolWidth)); }); test("toolbar items hidden are available in a popover", async () => {