mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
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
|
||||
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user