fix(studio): keep editor browser titles in sync after renames (#44154)

## What kind of change does this PR introduce?

Bug fix that resolves DEPR-414.

## What is the current behavior?

Browser titles in the table/SQL editors can keep showing an old entity
name after a rename.

## What is the new behavior?

- `EditorBaseLayout` now prefers the live tab label when composing the
browser title entity, so existing stale tab metadata no longer wins.
- Updating a tab label now also syncs the persisted `metadata.name`, so
renamed entities recover cleanly on reload instead of carrying stale
names in local storage.
- Adds a regression test covering the stale-metadata case in the editor
layout.

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
Danny WhiteandJoshen Lim authored and GitHub committed 2026-03-25 16:18:26 +08:00
1 parent dcab88059b
commit a261bafbe2
3 files changed
+127 -2

No files matched your search

@@ -0,0 +1,116 @@
import { render, screen } from '@testing-library/react'
import type { ReactNode } from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { EditorBaseLayout } from './EditorBaseLayout'
const { mockTabsState, mockEditorType } = vi.hoisted(() => ({
mockTabsState: {
activeTab: 'r-1',
openTabs: ['r-1'],
tabsMap: {
'r-1': {
id: 'r-1',
type: 'r',
label: 'routines',
metadata: {
name: 'tasks',
schema: 'public',
tableId: 1,
},
},
},
} as any,
mockEditorType: vi.fn(),
}))
vi.mock('common', () => ({
useParams: () => ({ ref: 'default' }),
}))
vi.mock('next/navigation', () => ({
usePathname: () => '/project/default/editor/1',
}))
vi.mock('state/tabs', () => ({
useTabsStateSnapshot: () => mockTabsState,
}))
vi.mock('ui', () => ({
cn: (...classes: Array<string | false | null | undefined>) => classes.filter(Boolean).join(' '),
}))
vi.mock('../ProjectLayout', () => ({
ProjectLayoutWithAuth: ({
browserTitle,
children,
}: {
browserTitle?: Record<string, string>
children: ReactNode
}) => (
<div>
<div data-testid="browser-title">{JSON.stringify(browserTitle ?? {})}</div>
{children}
</div>
),
}))
vi.mock('../Tabs/CollapseButton', () => ({
CollapseButton: () => null,
}))
vi.mock('../Tabs/Tabs', () => ({
EditorTabs: () => null,
}))
vi.mock('./EditorsLayout.hooks', () => ({
useEditorType: () => mockEditorType(),
}))
describe('EditorBaseLayout browser title', () => {
beforeEach(() => {
mockEditorType.mockReturnValue('table')
mockTabsState.activeTab = 'r-1'
mockTabsState.openTabs = ['r-1']
mockTabsState.tabsMap = {
'r-1': {
id: 'r-1',
type: 'r',
label: 'routines',
metadata: {
name: 'tasks',
schema: 'public',
tableId: 1,
},
},
}
})
it('prefers the live tab label over stale metadata when composing the title entity', () => {
render(
<EditorBaseLayout title="Tables" product="Table Editor">
<div>Page content</div>
</EditorBaseLayout>
)
expect(JSON.parse(screen.getByTestId('browser-title').textContent ?? '{}')).toEqual({
section: 'Tables',
entity: 'routines',
})
})
it('falls back to metadata when a tab does not have a label yet', () => {
mockTabsState.tabsMap['r-1'].label = undefined
render(
<EditorBaseLayout title="Tables" product="Table Editor">
<div>Page content</div>
</EditorBaseLayout>
)
expect(JSON.parse(screen.getByTestId('browser-title').textContent ?? '{}')).toEqual({
section: 'Tables',
entity: 'tasks',
})
})
})
@@ -35,16 +35,19 @@ export const EditorBaseLayout = ({
pathname === `/project/${ref}/editor` || pathname === `/project/${ref}/sql` || hasNoOpenTabs
const activeEditorTab = tabs.activeTab ? tabs.tabsMap[tabs.activeTab] : undefined
// Prefer the live tab label so browser titles update immediately after a rename,
// even when persisted tab metadata is still catching up.
const activeEditorTabLabel = activeEditorTab?.label ?? activeEditorTab?.metadata?.name
const activeEditorTabEntity =
activeEditorTab === undefined
? undefined
: editor === 'sql'
? activeEditorTab.type === 'sql'
? activeEditorTab.metadata?.name || activeEditorTab.label
? activeEditorTabLabel
: undefined
: editor === 'table'
? activeEditorTab.type !== 'sql'
? activeEditorTab.metadata?.name || activeEditorTab.label
? activeEditorTabLabel
: undefined
: undefined
+6
View File
@@ -226,6 +226,12 @@ export function createTabsState(projectRef: string) {
if (!!store.tabsMap[id]) {
if ('label' in updates) {
store.tabsMap[id].label = updates.label
// Keep the persisted name aligned with the visible label so browser titles
// and tab state recover cleanly after entity renames.
if (typeof updates.label === 'string' && store.tabsMap[id].metadata) {
store.tabsMap[id].metadata.name = updates.label
}
const recentItem = store.recentItems.find((item) => item.id === id)
if (recentItem) syncRecentItemWithTab(recentItem, store.tabsMap[id])
}