From afee9e47f21269ebd6404b44a3a85ea82bf8c310 Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Wed, 18 Jun 2025 15:48:40 +0200 Subject: [PATCH] Dashboards: undo redo move tab (#106786) * move tab Fixed issue * make i18 * always select the tab after moving it, select it after undo too --- .../edit-pane/DashboardEditPane.tsx | 8 +++ .../dashboard-scene/edit-pane/shared.ts | 27 ++++++++ .../layout-tabs/TabsLayoutManager.test.tsx | 67 +++++++++++++++++++ .../scene/layout-tabs/TabsLayoutManager.tsx | 17 ++++- .../layout-tabs/TabsLayoutManagerRenderer.tsx | 2 +- public/locales/en-US/grafana.json | 1 + 6 files changed, 120 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx index d3cf199c661..1da83352d74 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx @@ -122,6 +122,10 @@ export class DashboardEditPane extends SceneObjectBase { this.clearSelection(); } + if (action.movedObject) { + this.selectObject(action.movedObject, action.movedObject.state.key!, { force: true }); + } + if (action.removedObject) { this.newObjectAddedToCanvas(action.removedObject); } @@ -139,6 +143,10 @@ export class DashboardEditPane extends SceneObjectBase { this.newObjectAddedToCanvas(action.addedObject); } + if (action.movedObject) { + this.selectObject(action.movedObject, action.movedObject.state.key!, { force: true }); + } + if (action.removedObject) { this.clearSelection(); } diff --git a/public/app/features/dashboard-scene/edit-pane/shared.ts b/public/app/features/dashboard-scene/edit-pane/shared.ts index 752a4e5c723..1d47a9694dc 100644 --- a/public/app/features/dashboard-scene/edit-pane/shared.ts +++ b/public/app/features/dashboard-scene/edit-pane/shared.ts @@ -82,6 +82,7 @@ export class ConditionalRenderingChangedEvent extends BusEventWithPayload void; @@ -128,6 +129,13 @@ export interface ChangeDescriptionActionHelperProps { source: DashboardScene; } +export interface MoveElementActionHelperProps { + movedObject: SceneObject; + source: SceneObject; + perform: () => void; + undo: () => void; +} + export const dashboardEditActions = { /** * Registers and peforms an edit action @@ -230,6 +238,25 @@ export const dashboardEditActions = { }, }); }, + + moveElement(props: MoveElementActionHelperProps) { + const { movedObject, source, perform, undo } = props; + + const element = getEditableElementFor(movedObject); + if (!element) { + throw new Error('Moved object is not an editable element'); + } + + const typeName = element.getEditableElementInfo().typeName; + + dashboardEditActions.edit({ + description: t('dashboard.edit-actions.move', 'Move {{typeName}}', { typeName }), + movedObject, + source, + perform, + undo, + }); + }, }; export function undoRedoWasClicked(e: React.FocusEvent) { diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.test.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.test.tsx index 8c4b2fb97f3..280c312e1c3 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.test.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.test.tsx @@ -17,7 +17,12 @@ jest.mock('../../edit-pane/shared', () => ({ perform(); lastUndo = undo; }), + moveElement: jest.fn(({ perform, undo }) => { + perform(); + lastUndo = undo; + }), }, + ObjectsReorderedOnCanvasEvent: jest.fn().mockImplementation(() => ({})), })); describe('TabsLayoutManager', () => { @@ -179,4 +184,66 @@ describe('TabsLayoutManager', () => { expect(manager.state.currentTabIndex).toBe(1); // tab2 should be current again }); }); + + describe('moveTab', () => { + beforeEach(() => { + lastUndo = undefined; + jest.clearAllMocks(); + }); + + it('should move a tab to a new position', () => { + const manager = new TabsLayoutManager({ tabs: [] }); + const tab1 = manager.addNewTab(new TabItem({ title: 'Tab 1' })); + const tab2 = manager.addNewTab(new TabItem({ title: 'Tab 2' })); + const tab3 = manager.addNewTab(new TabItem({ title: 'Tab 3' })); + + expect(manager.state.tabs).toEqual([tab1, tab2, tab3]); + + manager.moveTab(0, 2); + + expect(manager.state.tabs).toEqual([tab2, tab3, tab1]); + expect(dashboardEditActions.moveElement).toHaveBeenCalled(); + }); + + it('should handle undo action correctly when moving a tab', () => { + const manager = new TabsLayoutManager({ tabs: [] }); + const tab1 = manager.addNewTab(new TabItem({ title: 'Tab 1' })); + const tab2 = manager.addNewTab(new TabItem({ title: 'Tab 2' })); + const tab3 = manager.addNewTab(new TabItem({ title: 'Tab 3' })); + + expect(manager.state.tabs).toEqual([tab1, tab2, tab3]); + + manager.moveTab(0, 2); + + expect(manager.state.tabs).toEqual([tab2, tab3, tab1]); + + // Use the real undo function from the mock + expect(typeof lastUndo).toBe('function'); + lastUndo && lastUndo(); + + expect(manager.state.tabs).toEqual([tab1, tab2, tab3]); + }); + + it('should update currentTabIndex when moving the current tab', () => { + const manager = new TabsLayoutManager({ tabs: [] }); + const tab1 = manager.addNewTab(new TabItem({ title: 'Tab 1' })); + const tab2 = manager.addNewTab(new TabItem({ title: 'Tab 2' })); + const tab3 = manager.addNewTab(new TabItem({ title: 'Tab 3' })); + + // Set tab2 as current + manager.setState({ currentTabIndex: 1 }); + expect(manager.state.currentTabIndex).toBe(1); + + manager.moveTab(1, 0); + + expect(manager.state.tabs).toEqual([tab2, tab1, tab3]); + expect(manager.state.currentTabIndex).toBe(0); + + // Undo should restore the original state + lastUndo && lastUndo(); + + expect(manager.state.tabs).toEqual([tab1, tab2, tab3]); + expect(manager.state.currentTabIndex).toBe(1); + }); + }); }); diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx index 913b8469931..40ae7ac0602 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx @@ -230,7 +230,22 @@ export class TabsLayoutManager extends SceneObjectBase i this.setState({ tabs, currentTabIndex: 0 }); } - public moveTab(_tabKey: string, fromIndex: number, toIndex: number) { + public moveTab(fromIndex: number, toIndex: number) { + const objectToMove = this.state.tabs[fromIndex]; + + dashboardEditActions.moveElement({ + source: this, + movedObject: objectToMove, + perform: () => { + this.rearrangeTabs(fromIndex, toIndex); + }, + undo: () => { + this.rearrangeTabs(toIndex, fromIndex); + }, + }); + } + + private rearrangeTabs(fromIndex: number, toIndex: number) { const tabs = [...this.state.tabs]; const [removed] = tabs.splice(fromIndex, 1); tabs.splice(toIndex, 0, removed); diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManagerRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManagerRenderer.tsx index 95a4a7f6a40..2e8af50fe10 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManagerRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManagerRenderer.tsx @@ -38,7 +38,7 @@ export function TabsLayoutManagerRenderer({ model }: SceneComponentProps
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index a83c42b9ddb..d6f400baac2 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -4055,6 +4055,7 @@ }, "edit-actions": { "add": "Add {{typeName}}", + "move": "Move {{typeName}}", "panel-background": "Change panel background", "panel-description": "Change panel description", "panel-title": "Change panel title",