From 73808a7cc361e4f3279df6a6dd5cfe66c84945bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Rahir=20=28rar=29?= Date: Tue, 2 Jun 2026 12:24:51 +0200 Subject: [PATCH 1/2] [FIX] TableResizer: deactivate when the grid selection is inactive When the grid selection is inactive, whether through the composer or a selectionInput, the user intension is to modify a specific definition or cell content but not to alter an object present in the grid. They act as a sort of "modal" that allows to select ranges in the grid and nothing else. As such, the table resizer should not be active when the grid selection isn't eiter. Task: 6268168 --- src/components/grid/grid.ts | 8 ++++++++ src/components/grid/grid.xml | 2 +- src/components/tables/table_resizer/table_resizer.ts | 5 ----- tests/table/table_resizer_component.test.ts | 11 +++++++++++ 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/components/grid/grid.ts b/src/components/grid/grid.ts index 206c5e71a4..153a7af991 100644 --- a/src/components/grid/grid.ts +++ b/src/components/grid/grid.ts @@ -911,4 +911,12 @@ export class Grid extends Component { get displaySelectionHandler() { return this.env.isMobile() && this.composerFocusStore.activeComposer.editionMode === "inactive"; } + + get displayTableResizer() { + return ( + this.env.model.getters.isGridSelectionActive() && + !this.env.model.getters.isReadonly() && + !this.env.model.getters.isSheetLocked(this.env.model.getters.getActiveSheetId()) + ); + } } diff --git a/src/components/grid/grid.xml b/src/components/grid/grid.xml index 1d4660462f..f6436da904 100644 --- a/src/components/grid/grid.xml +++ b/src/components/grid/grid.xml @@ -58,7 +58,7 @@ onClose="() => this.closeMenu()" /> diff --git a/src/components/tables/table_resizer/table_resizer.ts b/src/components/tables/table_resizer/table_resizer.ts index 680167ac23..ff3ff9bbd5 100644 --- a/src/components/tables/table_resizer/table_resizer.ts +++ b/src/components/tables/table_resizer/table_resizer.ts @@ -32,11 +32,6 @@ export class TableResizer extends Component { get containerStyle(): string { const tableZone = this.props.table.range.zone; - const sheetId = this.props.table.range.sheetId; - - if (this.env.model.getters.isReadonly() || this.env.model.getters.isSheetLocked(sheetId)) { - return cssPropertiesToCss({ display: "none" }); - } const bottomRight = { ...tableZone, left: tableZone.right, top: tableZone.bottom }; const rect = this.env.model.getters.getVisibleRect(bottomRight); if (rect.height === 0 || rect.width === 0) { diff --git a/tests/table/table_resizer_component.test.ts b/tests/table/table_resizer_component.test.ts index 5e55c12edb..4990d7ef95 100644 --- a/tests/table/table_resizer_component.test.ts +++ b/tests/table/table_resizer_component.test.ts @@ -1,4 +1,5 @@ import { Model, UID } from "../../src"; +import { CellComposerStore } from "../../src/components/composer/composer/cell_composer_store"; import { Grid } from "../../src/components/grid/grid"; import { DEFAULT_CELL_HEIGHT, DEFAULT_CELL_WIDTH } from "../../src/constants"; import { toZone, zoneToXc } from "../../src/helpers/zones"; @@ -114,4 +115,14 @@ describe("Table resizer component", () => { await nextTick(); expect(".o-table-resizer").toHaveCount(0); }); + + test("table resizer is not loaded when the grid selection is not active", async () => { + createTable(model, "A1:B2"); + await nextTick(); + expect(".o-table-resizer").toHaveCount(1); + // start the edition and steal the selection from the grid + env.getStore(CellComposerStore).startEdition(); + await nextTick(); + expect(".o-table-resizer").toHaveCount(0); + }); }); From 732fd9f86684b07e427cdb455691ff1ff74b03d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Rahir=20=28rar=29?= Date: Tue, 2 Jun 2026 13:15:10 +0200 Subject: [PATCH 2/2] [IMP] Model: plugins only see the grid selection Except from the grid selection, the other actors of the SelectionProcessor only live in the stores, the plugins don't know about them. Since the plugins sometimes call the processor, it means they always assume that they will impact the grid selection. With this revision, every call to the selection processor coming from the plugins will ensure that the grid selection is active. Task: 6268168 --- src/model.ts | 23 +++++++++++- .../selection_stream_processor.ts | 16 ++++++++ tests/model/model.test.ts | 37 +++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/model.ts b/src/model.ts index 3b4b72ba63..82c0b0a320 100644 --- a/src/model.ts +++ b/src/model.ts @@ -24,7 +24,10 @@ import { statefulUIPluginRegistry, } from "./plugins/plugin_registries"; import { UIPlugin, UIPluginConfig, UIPluginConstructor } from "./plugins/ui_plugin"; -import { SelectionStreamProcessorImpl } from "./selection_stream/selection_stream_processor"; +import { + selectionModifiers, + SelectionStreamProcessorImpl, +} from "./selection_stream/selection_stream_processor"; import { StateObserver } from "./state_observer"; import { _t, setDefaultTranslationMethod } from "./translation"; import { StateUpdateMessage } from "./types/collaborative/transport_service"; @@ -121,6 +124,7 @@ export class Model extends EventBus implements CommandDispatcher { private state: StateObserver; readonly selection: SelectionStreamProcessor; + readonly pluginSelection: SelectionStreamProcessor; /** * Getters are the main way the rest of the UI read data from the model. Also, @@ -187,6 +191,21 @@ export class Model extends EventBus implements CommandDispatcher { // Initiate stream processor this.selection = new SelectionStreamProcessorImpl(this.getters); + this.pluginSelection = new Proxy(this.selection, { + observable: false, + get: (target, prop, receiver) => { + const value = Reflect.get(target, prop, receiver); + if (typeof value !== "function" || !selectionModifiers.has(prop)) { + return value; + } + return (...args: unknown[]) => { + if (!this.getters.isGridSelectionActive()) { + this.selection.getBackToDefault(); + } + return (value as Function).apply(target, args); + }; + }, + } as ProxyHandler); this.coreHandlers.push(this.range); this.handlers.push(this.range); @@ -433,7 +452,7 @@ export class Model extends EventBus implements CommandDispatcher { stateObserver: this.state, dispatch: this.dispatch, canDispatch: this.canDispatch, - selection: this.selection, + selection: this.pluginSelection, moveClient: this.session.move.bind(this.session), custom: this.config.custom, uiActions: this.config, diff --git a/src/selection_stream/selection_stream_processor.ts b/src/selection_stream/selection_stream_processor.ts index 00f0aaf763..010fa3b48b 100644 --- a/src/selection_stream/selection_stream_processor.ts +++ b/src/selection_stream/selection_stream_processor.ts @@ -677,3 +677,19 @@ export class SelectionStreamProcessorImpl implements SelectionStreamProcessor { ); } } + +export const selectionModifiers: Set = new Set([ + "selectZone", + "selectCell", + "moveAnchorCell", + "updateAnchorCell", + "setAnchorCorner", + "addCellToSelection", + "resizeAnchorZone", + "selectColumn", + "selectRow", + "selectAll", + "loopSelection", + "selectTableAroundSelection", + "commitSelection", +]); diff --git a/tests/model/model.test.ts b/tests/model/model.test.ts index 498759b3ff..2d89b1b420 100644 --- a/tests/model/model.test.ts +++ b/tests/model/model.test.ts @@ -420,4 +420,41 @@ describe("Model", () => { type: "SNAPSHOT", }); }); + + test("Selection Processor falls back to gridSelection when invoked from the plugins", () => { + class MyUIPlugin extends UIPlugin { + handle(cmd: Command) { + // @ts-ignore + if (cmd.type === "CAPTURE") { + this.selection.capture( + this, + { + cell: { col: 0, row: 0 }, + zone: toZone("A1"), + }, + { + handleEvent: () => { + throw new Error("Should not be called"); + }, + } + ); + } + // @ts-ignore + else if (cmd.type === "MOVE") { + this.selection.selectCell(0, 1); + } + } + } + addTestPlugin(featurePluginRegistry, MyUIPlugin); + const model = new Model(); + expect(model.getters.isGridSelectionActive()).toBe(true); + expect(model.getters.getSelectedZone()).toEqual(toZone("A1")); + // @ts-ignore + model.dispatch("CAPTURE"); + expect(model.getters.isGridSelectionActive()).toBe(false); + // @ts-ignore + model.dispatch("MOVE"); + expect(model.getters.isGridSelectionActive()).toBe(true); + expect(model.getters.getSelectedZone()).toEqual(toZone("A2")); + }); });