Skip to content

Commit 80fe705

Browse files
gnoviawanclaude
andauthored
fix: persist workspace panel visibility across restarts (#69)
* fix: persist workspace panel visibility across restarts (#40) Move sidebar and file explorer visibility into global app settings so panel state survives restarts and project switches. Serialize immediate writes, add rollback on failure, and wait on close so the latest toggle is not lost. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address review feedback for workspace panel persistence (#40) Capture panel write payloads in the persistence queue, tighten close-request typing, and reuse shared shortcut callback types. This keeps rollback behavior deterministic and aligns the Tauri window API with async close coordination. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e527ed7 commit 80fe705

17 files changed

+928
-65
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
3+
import { MemoryRouter } from 'react-router-dom'
4+
import { TitleBar } from './TitleBar'
5+
import { useSidebarStore } from '@/stores/sidebar-store'
6+
import { useFileExplorerStore } from '@/stores/file-explorer-store'
7+
import * as appSettingsHooks from '@/hooks/use-app-settings'
8+
9+
const { mockUpdatePanelVisibility, mockToastError, mockWindowApi } = vi.hoisted(() => ({
10+
mockUpdatePanelVisibility: vi.fn(() => Promise.resolve()),
11+
mockToastError: vi.fn(),
12+
mockWindowApi: {
13+
onMaximizeChange: vi.fn(() => vi.fn()),
14+
minimize: vi.fn(),
15+
toggleMaximize: vi.fn().mockResolvedValue({ success: true, data: false }),
16+
close: vi.fn()
17+
}
18+
}))
19+
20+
vi.mock('@/lib/api', () => ({
21+
windowApi: mockWindowApi
22+
}))
23+
24+
vi.mock('sonner', () => ({
25+
toast: {
26+
error: mockToastError
27+
}
28+
}))
29+
30+
describe('TitleBar', () => {
31+
beforeEach(() => {
32+
vi.clearAllMocks()
33+
vi.spyOn(appSettingsHooks, 'useUpdatePanelVisibility').mockReturnValue(
34+
mockUpdatePanelVisibility
35+
)
36+
useSidebarStore.setState({ isVisible: true })
37+
useFileExplorerStore.setState({ isVisible: true })
38+
})
39+
40+
function renderTitleBar() {
41+
return render(
42+
<MemoryRouter>
43+
<TitleBar />
44+
</MemoryRouter>
45+
)
46+
}
47+
48+
it('toggles sidebar via persistence-aware updater on click', async () => {
49+
renderTitleBar()
50+
51+
fireEvent.click(screen.getByRole('button', { name: 'Hide sidebar' }))
52+
53+
await waitFor(() => {
54+
expect(mockUpdatePanelVisibility).toHaveBeenCalledWith('sidebarVisible', false)
55+
})
56+
})
57+
58+
it('toggles file explorer via persistence-aware updater on click', async () => {
59+
renderTitleBar()
60+
61+
fireEvent.click(screen.getByRole('button', { name: 'Hide file explorer' }))
62+
63+
await waitFor(() => {
64+
expect(mockUpdatePanelVisibility).toHaveBeenCalledWith('fileExplorerVisible', false)
65+
})
66+
})
67+
68+
it('shows error toast when sidebar persistence update fails', async () => {
69+
mockUpdatePanelVisibility.mockRejectedValueOnce(new Error('persist failed'))
70+
71+
renderTitleBar()
72+
73+
fireEvent.click(screen.getByRole('button', { name: 'Hide sidebar' }))
74+
75+
await waitFor(() => {
76+
expect(mockToastError).toHaveBeenCalledWith('persist failed')
77+
})
78+
})
79+
80+
it('shows error toast when file explorer persistence update fails', async () => {
81+
mockUpdatePanelVisibility.mockRejectedValueOnce(new Error('persist failed'))
82+
83+
renderTitleBar()
84+
85+
fireEvent.click(screen.getByRole('button', { name: 'Hide file explorer' }))
86+
87+
await waitFor(() => {
88+
expect(mockToastError).toHaveBeenCalledWith('persist failed')
89+
})
90+
})
91+
})

src/renderer/components/TitleBar.tsx

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,40 @@
11
import { useState, useEffect } from 'react'
22
import { Minus, Square, Copy, X, PanelLeft, PanelRight, Settings, SlidersHorizontal } from 'lucide-react'
33
import { useNavigate, useLocation } from 'react-router-dom'
4-
import { useSidebarStore, useSidebarVisible } from '@/stores/sidebar-store'
5-
import { useFileExplorerStore, useFileExplorerVisible } from '@/stores/file-explorer-store'
4+
import { useSidebarVisible } from '@/stores/sidebar-store'
5+
import { useFileExplorerVisible } from '@/stores/file-explorer-store'
6+
import { useUpdatePanelVisibility } from '@/hooks/use-app-settings'
67
import { windowApi } from '@/lib/api'
8+
import { toast } from 'sonner'
79

810
const focusableButtonClass = 'h-full px-3 hover:bg-secondary inline-flex items-center focus:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-inset'
911

1012
export function TitleBar(): React.JSX.Element {
1113
const [isMaximized, setIsMaximized] = useState(false)
1214
const isSidebarVisible = useSidebarVisible()
1315
const isExplorerVisible = useFileExplorerVisible()
16+
const updatePanelVisibility = useUpdatePanelVisibility()
1417
const navigate = useNavigate()
1518
const location = useLocation()
1619

20+
const handleToggleSidebar = async (e: React.MouseEvent<HTMLButtonElement>): Promise<void> => {
21+
e.stopPropagation()
22+
try {
23+
await updatePanelVisibility('sidebarVisible', !isSidebarVisible)
24+
} catch (error) {
25+
toast.error(error instanceof Error ? error.message : 'Failed to update sidebar visibility')
26+
}
27+
}
28+
29+
const handleToggleFileExplorer = async (e: React.MouseEvent<HTMLButtonElement>): Promise<void> => {
30+
e.stopPropagation()
31+
try {
32+
await updatePanelVisibility('fileExplorerVisible', !isExplorerVisible)
33+
} catch (error) {
34+
toast.error(error instanceof Error ? error.message : 'Failed to update file explorer visibility')
35+
}
36+
}
37+
1738
useEffect(() => {
1839
return windowApi.onMaximizeChange((maximized) => {
1940
setIsMaximized(maximized)
@@ -42,7 +63,9 @@ export function TitleBar(): React.JSX.Element {
4263
style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}
4364
>
4465
<button
45-
onClick={(e) => { e.stopPropagation(); useSidebarStore.getState().toggleVisibility(); }}
66+
onClick={(e) => {
67+
void handleToggleSidebar(e)
68+
}}
4669
className={focusableButtonClass}
4770
title="Toggle sidebar"
4871
aria-label={isSidebarVisible ? 'Hide sidebar' : 'Show sidebar'}
@@ -55,7 +78,9 @@ export function TitleBar(): React.JSX.Element {
5578
</button>
5679

5780
<button
58-
onClick={(e) => { e.stopPropagation(); useFileExplorerStore.getState().toggleVisibility(); }}
81+
onClick={(e) => {
82+
void handleToggleFileExplorer(e)
83+
}}
5984
className={focusableButtonClass}
6085
title="Toggle file explorer"
6186
aria-label={isExplorerVisible ? 'Hide file explorer' : 'Show file explorer'}

0 commit comments

Comments
 (0)