Dashboards: undo redo move tab (#106786)

* move tab

Fixed issue

* make i18

* always select the tab after moving it, select it after undo too
This commit is contained in:
Oscar Kilhed
2025-06-18 15:48:40 +02:00
committed by GitHub
parent 8893b9a6eb
commit afee9e47f2
6 changed files with 120 additions and 2 deletions
@@ -122,6 +122,10 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
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<DashboardEditPaneState> {
this.newObjectAddedToCanvas(action.addedObject);
}
if (action.movedObject) {
this.selectObject(action.movedObject, action.movedObject.state.key!, { force: true });
}
if (action.removedObject) {
this.clearSelection();
}
@@ -82,6 +82,7 @@ export class ConditionalRenderingChangedEvent extends BusEventWithPayload<SceneO
export interface DashboardEditActionEventPayload {
removedObject?: SceneObject;
addedObject?: SceneObject;
movedObject?: SceneObject;
source: SceneObject;
description?: string;
perform: () => 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) {
@@ -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);
});
});
});
@@ -230,7 +230,22 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> 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);
@@ -38,7 +38,7 @@ export function TabsLayoutManagerRenderer({ model }: SceneComponentProps<TabsLay
return;
}
model.moveTab(result.draggableId, result.source.index, result.destination.index);
model.moveTab(result.source.index, result.destination.index);
}}
>
<div className={styles.tabsRow}>
+1
View File
@@ -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",