mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-19 13:33:35 +08:00
fix(desktop): keep session refresh usable
Fixes #893. Reproduced the stuck session-list refresh by keeping the automatic fetch promise pending, then clicking the manual refresh button. Also covered the existing-sessions loading state so a background refresh does not leave the control disabled or spinning forever. Tested: cd desktop && bun run test -- src/components/layout/Sidebar.test.tsx --run Tested: bun run check:desktop Confidence: high Scope-risk: narrow
This commit is contained in:
parent
90cbe9f62c
commit
1f7b1e68c4
@ -293,6 +293,35 @@ describe('Sidebar', () => {
|
|||||||
expect(screen.getByRole('button', { name: 'Collapse display' })).toBeInTheDocument()
|
expect(screen.getByRole('button', { name: 'Collapse display' })).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('lets a manual session refresh supersede a stuck automatic refresh', async () => {
|
||||||
|
fetchSessions.mockReturnValue(new Promise(() => {}))
|
||||||
|
|
||||||
|
render(<Sidebar />)
|
||||||
|
|
||||||
|
await waitFor(() => expect(fetchSessions).toHaveBeenCalledTimes(1))
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Refresh sessions' }))
|
||||||
|
|
||||||
|
await waitFor(() => expect(fetchSessions).toHaveBeenCalledTimes(2))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps the session refresh control usable when a background refresh is still loading existing sessions', async () => {
|
||||||
|
useSessionStore.setState({
|
||||||
|
sessions: [
|
||||||
|
makeSession('session-loaded', 'Loaded session', '/workspace/alpha', '2026-05-15T10:00:00.000Z'),
|
||||||
|
],
|
||||||
|
isLoading: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
render(<Sidebar />)
|
||||||
|
|
||||||
|
const refreshButton = screen.getByRole('button', { name: 'Refresh sessions' })
|
||||||
|
expect(refreshButton).not.toBeDisabled()
|
||||||
|
expect(refreshButton.querySelector('svg')).not.toHaveClass('animate-spin')
|
||||||
|
|
||||||
|
fireEvent.click(refreshButton)
|
||||||
|
await waitFor(() => expect(fetchSessions).toHaveBeenCalled())
|
||||||
|
})
|
||||||
|
|
||||||
it('reorders project groups by dragging project headers while preserving expanded state', async () => {
|
it('reorders project groups by dragging project headers while preserving expanded state', async () => {
|
||||||
const base = new Date('2026-05-15T10:00:00.000Z').getTime()
|
const base = new Date('2026-05-15T10:00:00.000Z').getTime()
|
||||||
useSessionStore.setState({
|
useSessionStore.setState({
|
||||||
|
|||||||
@ -127,6 +127,7 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {
|
|||||||
))
|
))
|
||||||
}, [hiddenProjectKeys, orderedProjectGroups])
|
}, [hiddenProjectKeys, orderedProjectGroups])
|
||||||
const showInitialLoading = isLoading && sessions.length === 0
|
const showInitialLoading = isLoading && sessions.length === 0
|
||||||
|
const showRefreshLoading = showInitialLoading
|
||||||
const filteredSessionIds = useMemo(() => filteredSessions.map((session) => session.id), [filteredSessions])
|
const filteredSessionIds = useMemo(() => filteredSessions.map((session) => session.id), [filteredSessions])
|
||||||
const selectedCount = selectedSessionIds.size
|
const selectedCount = selectedSessionIds.size
|
||||||
const sessionsById = useMemo(
|
const sessionsById = useMemo(
|
||||||
@ -709,12 +710,11 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void refreshSessionsNow()}
|
onClick={() => void refreshSessionsNow()}
|
||||||
disabled={isLoading}
|
|
||||||
className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-[12px] border border-[var(--color-sidebar-search-border)] bg-[var(--color-sidebar-search-bg)] text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-sidebar-item-hover)] hover:text-[var(--color-text-primary)] disabled:cursor-default disabled:opacity-65 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-focus)]"
|
className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-[12px] border border-[var(--color-sidebar-search-border)] bg-[var(--color-sidebar-search-bg)] text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-sidebar-item-hover)] hover:text-[var(--color-text-primary)] disabled:cursor-default disabled:opacity-65 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-focus)]"
|
||||||
aria-label={t('sidebar.refreshSessions')}
|
aria-label={t('sidebar.refreshSessions')}
|
||||||
title={t('sidebar.refreshSessions')}
|
title={t('sidebar.refreshSessions')}
|
||||||
>
|
>
|
||||||
<RefreshCw className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} strokeWidth={1.9} aria-hidden="true" />
|
<RefreshCw className={`h-4 w-4 ${showRefreshLoading ? 'animate-spin' : ''}`} strokeWidth={1.9} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -1213,7 +1213,7 @@ function useSessionListAutoRefresh(fetchSessions: () => Promise<void>): () => Pr
|
|||||||
const lastStartedAtRef = useRef(0)
|
const lastStartedAtRef = useRef(0)
|
||||||
|
|
||||||
const refreshSessions = useCallback((force = false) => {
|
const refreshSessions = useCallback((force = false) => {
|
||||||
if (inFlightRef.current) return inFlightRef.current
|
if (inFlightRef.current && !force) return inFlightRef.current
|
||||||
|
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
if (!force && now - lastStartedAtRef.current < SESSION_LIST_FOCUS_REFRESH_MIN_MS) {
|
if (!force && now - lastStartedAtRef.current < SESSION_LIST_FOCUS_REFRESH_MIN_MS) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user