Merge pull request #22 from refactoringhq/task/click-opens-new-tab

feat: click opens note in current tab, Cmd+Click opens new tab
This commit is contained in:
Luca Rossi
2026-02-23 21:01:16 +01:00
committed by GitHub
7 changed files with 556 additions and 78 deletions

View File

@@ -270,6 +270,31 @@ describe('useTabManagement', () => {
expect(result.current.tabs).toHaveLength(1)
expect(result.current.activeTabPath).toBe('/vault/a.md')
})
it('switches to existing tab instead of replacing when note is already open', async () => {
const { result } = renderHook(() => useTabManagement())
// Open two tabs: A (active) and B
await act(async () => {
await result.current.handleSelectNote(makeEntry({ path: '/vault/a.md', title: 'A' }))
})
await act(async () => {
await result.current.handleSelectNote(makeEntry({ path: '/vault/b.md', title: 'B' }))
})
// Switch back to A
act(() => { result.current.handleSwitchTab('/vault/a.md') })
// Replace active tab with B — but B is already open, so it should just switch
await act(async () => {
await result.current.handleReplaceActiveTab(makeEntry({ path: '/vault/b.md', title: 'B' }))
})
// Should still have 2 tabs (not replace A), and B should be active
expect(result.current.tabs).toHaveLength(2)
expect(result.current.activeTabPath).toBe('/vault/b.md')
expect(result.current.tabs.map(t => t.entry.title)).toEqual(['A', 'B'])
})
})
describe('closeAllTabs', () => {

View File

@@ -76,6 +76,24 @@ function restoreOrder(prev: Tab[], savedOrder: string[]): Tab[] {
return ordered
}
function isTabOpen(tabs: Tab[], path: string): boolean {
return tabs.some((t) => t.entry.path === path)
}
async function loadAndSetTab(
entry: VaultEntry,
updater: (prev: Tab[], content: string) => Tab[],
setTabs: React.Dispatch<React.SetStateAction<Tab[]>>,
) {
try {
const content = await loadNoteContent(entry.path)
setTabs((prev) => updater(prev, content))
} catch (err) {
console.warn('Failed to load note content:', err)
setTabs((prev) => updater(prev, ''))
}
}
export type { Tab }
export function useTabManagement() {
@@ -88,54 +106,31 @@ export function useTabManagement() {
const handleCloseTabRef = useRef<(path: string) => void>(() => {})
const handleSelectNote = useCallback(async (entry: VaultEntry) => {
if (tabsRef.current.some((t) => t.entry.path === entry.path)) {
setActiveTabPath(entry.path)
return
}
try {
const content = await loadNoteContent(entry.path)
setTabs((prev) => addTabIfAbsent(prev, entry, content))
} catch (err) {
console.warn('Failed to load note content:', err)
setTabs((prev) => addTabIfAbsent(prev, entry, ''))
}
if (isTabOpen(tabsRef.current, entry.path)) { setActiveTabPath(entry.path); return }
await loadAndSetTab(entry, (prev, content) => addTabIfAbsent(prev, entry, content), setTabs)
setActiveTabPath(entry.path)
}, [])
const handleCloseTab = useCallback((path: string) => {
setTabs((prev) => {
const next = prev.filter((t) => t.entry.path !== path)
if (path === activeTabPathRef.current) {
setActiveTabPath(resolveNextActiveTab(prev, path))
}
if (path === activeTabPathRef.current) { setActiveTabPath(resolveNextActiveTab(prev, path)) }
return next
})
}, [])
useEffect(() => { handleCloseTabRef.current = handleCloseTab })
const handleSwitchTab = useCallback((path: string) => {
setActiveTabPath(path)
}, [])
const handleSwitchTab = useCallback((path: string) => { setActiveTabPath(path) }, [])
const handleReorderTabs = useCallback((fromIndex: number, toIndex: number) => {
setTabs((prev) => {
const next = reorderArray(prev, fromIndex, toIndex)
saveTabOrder(next)
return next
})
setTabs((prev) => { const next = reorderArray(prev, fromIndex, toIndex); saveTabOrder(next); return next })
}, [])
const handleReplaceActiveTab = useCallback(async (entry: VaultEntry) => {
if (isTabOpen(tabsRef.current, entry.path)) { setActiveTabPath(entry.path); return }
const currentPath = activeTabPathRef.current
if (!currentPath) { handleSelectNote(entry); return }
if (currentPath === entry.path) return
try {
const content = await loadNoteContent(entry.path)
setTabs((prev) => replaceTabEntry(prev, currentPath, entry, content))
} catch (err) {
console.warn('Failed to load note content for replace:', err)
setTabs((prev) => replaceTabEntry(prev, currentPath, entry, ''))
}
await loadAndSetTab(entry, (prev, content) => replaceTabEntry(prev, currentPath, entry, content), setTabs)
setActiveTabPath(entry.path)
}, [handleSelectNote])