Dynamic dashboards: Deduplicate titles of new rows and tabs (#103330)

Deduplicate titles of new rows and tabs
This commit is contained in:
Oscar Kilhed
2025-04-03 10:44:09 +02:00
committed by GitHub
parent b49c532999
commit d7771f7d21
4 changed files with 97 additions and 0 deletions
@@ -16,6 +16,7 @@ import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutMan
import { RowRepeaterBehavior } from '../layout-default/RowRepeaterBehavior';
import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager';
import { getRowFromClipboard } from '../layouts-shared/paste';
import { generateUniqueTitle } from '../layouts-shared/utils';
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
import { LayoutRegistryItem } from '../types/LayoutRegistryItem';
@@ -95,6 +96,13 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
public addNewRow(row?: RowItem): RowItem {
const newRow = row ?? new RowItem({ isNew: true });
const existingNames = new Set(this.state.rows.map((row) => row.state.title).filter((title) => title !== undefined));
const newTitle = generateUniqueTitle(newRow.state.title, existingNames);
if (newTitle !== newRow.state.title) {
newRow.setState({ title: newTitle });
}
this.setState({ rows: [...this.state.rows, newRow] });
this.publishEvent(new NewObjectAddedToCanvasEvent(newRow), true);
return newRow;
@@ -18,6 +18,7 @@ import { getDashboardSceneFor } from '../../utils/utils';
import { RowItem } from '../layout-rows/RowItem';
import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager';
import { getTabFromClipboard } from '../layouts-shared/paste';
import { generateUniqueTitle } from '../layouts-shared/utils';
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
import { LayoutRegistryItem } from '../types/LayoutRegistryItem';
@@ -125,6 +126,12 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
public addNewTab(tab?: TabItem) {
const newTab = tab ?? new TabItem({ isNew: true });
const existingNames = new Set(this.state.tabs.map((tab) => tab.state.title).filter((title) => title !== undefined));
const newTitle = generateUniqueTitle(newTab.state.title, existingNames);
if (newTitle !== newTab.state.title) {
newTab.setState({ title: newTitle });
}
this.setState({ tabs: [...this.state.tabs, newTab], currentTabIndex: this.state.tabs.length });
this.publishEvent(new NewObjectAddedToCanvasEvent(newTab), true);
return newTab;
@@ -0,0 +1,50 @@
import { generateUniqueTitle } from './utils';
describe('generateUniqueTitle', () => {
it('should return the original title if it is not in the existing titles', () => {
const title = 'My Title';
const existingTitles = new Set<string>(['Other Title', 'Another Title']);
expect(generateUniqueTitle(title, existingTitles)).toBe(title);
});
it('should handle undefined title by using empty string as base', () => {
const existingTitles = new Set<string>(['Title 1', 'Title 2']);
expect(generateUniqueTitle(undefined, existingTitles)).toBe('');
});
it('should append "1" to a title that does not end with a number', () => {
const title = 'My Title';
const existingTitles = new Set<string>(['My Title']);
expect(generateUniqueTitle(title, existingTitles)).toBe('My Title 1');
});
it('should increment a number at the end of a title', () => {
const title = 'My Title 1';
const existingTitles = new Set<string>(['My Title 1', 'My Title 2']);
expect(generateUniqueTitle(title, existingTitles)).toBe('My Title 3');
});
it('should handle multiple increments when needed', () => {
const title = 'My Title';
const existingTitles = new Set<string>(['My Title', 'My Title 1', 'My Title 2', 'My Title 3']);
expect(generateUniqueTitle(title, existingTitles)).toBe('My Title 4');
});
it('should handle titles with multiple numbers', () => {
const title = 'My Title 123';
const existingTitles = new Set<string>(['My Title 123', 'My Title 124']);
expect(generateUniqueTitle(title, existingTitles)).toBe('My Title 125');
});
it('should handle titles with spaces before the number', () => {
const title = 'My Title 1';
const existingTitles = new Set<string>(['My Title 1', 'My Title 2']);
expect(generateUniqueTitle(title, existingTitles)).toBe('My Title 3');
});
it('should handle empty existing titles set', () => {
const title = 'My Title';
const existingTitles = new Set<string>();
expect(generateUniqueTitle(title, existingTitles)).toBe(title);
});
});
@@ -34,3 +34,35 @@ export function useEditPaneInputAutoFocus({ autoFocus }: EditPaneInputAutoFocusP
return ref;
}
export function generateUniqueTitle(title: string | undefined, existingTitles: Set<string>): string {
const baseTitle = title ?? '';
if (existingTitles.has(baseTitle)) {
const titleMatch = baseTitle.match(/^(.*?)(\d+)$/);
if (titleMatch) {
// If title ends with a number, increment it
const baseTitle = titleMatch[1];
const currentNumber = parseInt(titleMatch[2], 10);
let newTitle = `${baseTitle}${currentNumber + 1}`;
// Keep incrementing until we find an unused title
while (existingTitles.has(newTitle)) {
const nextNumber = parseInt(newTitle.match(/\d+$/)![0], 10) + 1;
newTitle = `${baseTitle}${nextNumber}`;
}
return newTitle;
} else {
// If title doesn't end with a number, append "1"
let i = 1;
let newTitle = `${baseTitle} ${i}`;
while (existingTitles.has(newTitle)) {
i++;
newTitle = `${baseTitle} ${i}`;
}
return newTitle;
}
}
return baseTitle;
}