diff --git a/apps/studio/components/layouts/editors/EditorBaseLayout.test.tsx b/apps/studio/components/layouts/editors/EditorBaseLayout.test.tsx new file mode 100644 index 00000000000..30fb150181f --- /dev/null +++ b/apps/studio/components/layouts/editors/EditorBaseLayout.test.tsx @@ -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) => classes.filter(Boolean).join(' '), +})) + +vi.mock('../ProjectLayout', () => ({ + ProjectLayoutWithAuth: ({ + browserTitle, + children, + }: { + browserTitle?: Record + children: ReactNode + }) => ( +
+
{JSON.stringify(browserTitle ?? {})}
+ {children} +
+ ), +})) + +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( + +
Page content
+
+ ) + + 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( + +
Page content
+
+ ) + + expect(JSON.parse(screen.getByTestId('browser-title').textContent ?? '{}')).toEqual({ + section: 'Tables', + entity: 'tasks', + }) + }) +}) diff --git a/apps/studio/components/layouts/editors/EditorBaseLayout.tsx b/apps/studio/components/layouts/editors/EditorBaseLayout.tsx index 4eb1480c849..fdb75377244 100644 --- a/apps/studio/components/layouts/editors/EditorBaseLayout.tsx +++ b/apps/studio/components/layouts/editors/EditorBaseLayout.tsx @@ -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 diff --git a/apps/studio/state/tabs.tsx b/apps/studio/state/tabs.tsx index d757d1c61ae..edfaec99503 100644 --- a/apps/studio/state/tabs.tsx +++ b/apps/studio/state/tabs.tsx @@ -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]) }