mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(web): preserve file search when returning from preview (#1251)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getSettingsBackTarget } from './useAppGoBack'
|
||||
import { getSessionFilesBackSearch, getSettingsBackTarget } from './useAppGoBack'
|
||||
|
||||
describe('getSettingsBackTarget', () => {
|
||||
it.each([
|
||||
@@ -14,3 +14,26 @@ describe('getSettingsBackTarget', () => {
|
||||
expect(getSettingsBackTarget(pathname)).toBe(target)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSessionFilesBackSearch', () => {
|
||||
it('preserves the directory tab and file search query', () => {
|
||||
expect(getSessionFilesBackSearch({
|
||||
path: 'encoded-path',
|
||||
staged: false,
|
||||
tab: 'directories',
|
||||
query: '感',
|
||||
})).toEqual({
|
||||
tab: 'directories',
|
||||
query: '感',
|
||||
})
|
||||
})
|
||||
|
||||
it('drops unrelated and invalid search values', () => {
|
||||
expect(getSessionFilesBackSearch({
|
||||
path: 'encoded-path',
|
||||
tab: 'changes',
|
||||
query: '',
|
||||
})).toEqual({})
|
||||
expect(getSessionFilesBackSearch(null)).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,6 +8,21 @@ export function getSettingsBackTarget(pathname: string): string | null {
|
||||
return null
|
||||
}
|
||||
|
||||
export function getSessionFilesBackSearch(search: unknown): {
|
||||
tab?: 'directories'
|
||||
query?: string
|
||||
} {
|
||||
if (!search || typeof search !== 'object') return {}
|
||||
|
||||
const currentSearch = search as { tab?: unknown; query?: unknown }
|
||||
return {
|
||||
...(currentSearch.tab === 'directories' ? { tab: 'directories' as const } : {}),
|
||||
...(typeof currentSearch.query === 'string' && currentSearch.query.length > 0
|
||||
? { query: currentSearch.query }
|
||||
: {}),
|
||||
}
|
||||
}
|
||||
|
||||
export function useAppGoBack(): () => void {
|
||||
const navigate = useNavigate()
|
||||
const router = useRouter()
|
||||
@@ -31,13 +46,7 @@ export function useAppGoBack(): () => void {
|
||||
// For single file view, go back to files list
|
||||
if (pathname.match(/^\/sessions\/[^/]+\/file$/)) {
|
||||
const filesPath = pathname.replace(/\/file$/, '/files')
|
||||
|
||||
const tab = (search && typeof search === 'object' && 'tab' in search)
|
||||
? (search as { tab?: unknown }).tab
|
||||
: undefined
|
||||
const nextSearch = tab === 'directories' ? { tab: 'directories' as const } : {}
|
||||
|
||||
navigate({ to: filesPath, search: nextSearch })
|
||||
navigate({ to: filesPath, search: getSessionFilesBackSearch(search) })
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+15
-2
@@ -1295,15 +1295,21 @@ const sessionDetailRoute = createRoute({
|
||||
const sessionFilesRoute = createRoute({
|
||||
getParentRoute: () => sessionDetailRoute,
|
||||
path: 'files',
|
||||
validateSearch: (search: Record<string, unknown>): { tab?: 'changes' | 'directories' } => {
|
||||
validateSearch: (search: Record<string, unknown>): { tab?: 'changes' | 'directories'; query?: string } => {
|
||||
const tabValue = typeof search.tab === 'string' ? search.tab : undefined
|
||||
const tab = tabValue === 'directories'
|
||||
? 'directories'
|
||||
: tabValue === 'changes'
|
||||
? 'changes'
|
||||
: undefined
|
||||
const query = typeof search.query === 'string' && search.query.length > 0
|
||||
? search.query
|
||||
: undefined
|
||||
|
||||
return tab ? { tab } : {}
|
||||
return {
|
||||
...(tab ? { tab } : {}),
|
||||
...(query ? { query } : {}),
|
||||
}
|
||||
},
|
||||
component: FilesPage,
|
||||
})
|
||||
@@ -1318,6 +1324,7 @@ type SessionFileSearch = {
|
||||
path: string
|
||||
staged?: boolean
|
||||
tab?: 'changes' | 'directories'
|
||||
query?: string
|
||||
}
|
||||
|
||||
const sessionFileRoute = createRoute({
|
||||
@@ -1337,6 +1344,9 @@ const sessionFileRoute = createRoute({
|
||||
: tabValue === 'changes'
|
||||
? 'changes'
|
||||
: undefined
|
||||
const query = typeof search.query === 'string' && search.query.length > 0
|
||||
? search.query
|
||||
: undefined
|
||||
|
||||
const result: SessionFileSearch = { path }
|
||||
if (staged !== undefined) {
|
||||
@@ -1345,6 +1355,9 @@ const sessionFileRoute = createRoute({
|
||||
if (tab !== undefined) {
|
||||
result.tab = tab
|
||||
}
|
||||
if (query !== undefined) {
|
||||
result.query = query
|
||||
}
|
||||
return result
|
||||
},
|
||||
component: FilePage,
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { I18nProvider } from '@/lib/i18n-context'
|
||||
import { encodeBase64 } from '@/lib/utils'
|
||||
import FilesPage from './files'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
navigate: vi.fn(),
|
||||
fileSearch: vi.fn(),
|
||||
search: {
|
||||
tab: 'directories' as const,
|
||||
query: '感',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@tanstack/react-router', () => ({
|
||||
useNavigate: () => mocks.navigate,
|
||||
useParams: () => ({ sessionId: 'session-1' }),
|
||||
useSearch: () => mocks.search,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/app-context', () => ({
|
||||
useAppContext: () => ({ api: {} }),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/useAppGoBack', () => ({
|
||||
useAppGoBack: () => vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/queries/useSession', () => ({
|
||||
useSession: () => ({
|
||||
session: {
|
||||
id: 'session-1',
|
||||
metadata: { path: '/workspace/project' },
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/queries/useGitStatusFiles', () => ({
|
||||
useGitStatusFiles: () => ({
|
||||
status: null,
|
||||
error: null,
|
||||
isLoading: false,
|
||||
refetch: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/queries/useSessionFileSearch', () => ({
|
||||
useSessionFileSearch: (...args: unknown[]) => {
|
||||
mocks.fileSearch(...args)
|
||||
return {
|
||||
files: [{
|
||||
fileName: '感言.ts',
|
||||
filePath: 'src',
|
||||
fullPath: 'src/感言.ts',
|
||||
fileType: 'file' as const,
|
||||
}],
|
||||
error: null,
|
||||
isLoading: false,
|
||||
refetch: vi.fn(),
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/components/SessionHeader', () => ({
|
||||
SessionHeader: () => null,
|
||||
}))
|
||||
|
||||
vi.mock('@/components/SessionFiles/DirectoryTree', () => ({
|
||||
DirectoryTree: () => null,
|
||||
}))
|
||||
|
||||
function renderFilesPage() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
},
|
||||
})
|
||||
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<I18nProvider>
|
||||
<FilesPage />
|
||||
</I18nProvider>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
describe('FilesPage search navigation', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
window.localStorage.clear()
|
||||
window.sessionStorage.clear()
|
||||
})
|
||||
|
||||
it('restores the route query and carries it through file navigation', () => {
|
||||
renderFilesPage()
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
expect(input).toHaveValue('感')
|
||||
expect(mocks.fileSearch).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'session-1',
|
||||
'感',
|
||||
{ enabled: true },
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /感言\.ts/ }))
|
||||
expect(mocks.navigate).toHaveBeenCalledWith({
|
||||
to: '/sessions/$sessionId/file',
|
||||
params: { sessionId: 'session-1' },
|
||||
search: {
|
||||
path: encodeBase64('src/感言.ts'),
|
||||
tab: 'directories',
|
||||
query: '感',
|
||||
},
|
||||
})
|
||||
|
||||
fireEvent.change(input, { target: { value: '言' } })
|
||||
expect(mocks.navigate).toHaveBeenLastCalledWith({
|
||||
to: '/sessions/$sessionId/files',
|
||||
params: { sessionId: 'session-1' },
|
||||
search: {
|
||||
tab: 'directories',
|
||||
query: '言',
|
||||
},
|
||||
replace: true,
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Clear search' }))
|
||||
expect(mocks.navigate).toHaveBeenLastCalledWith({
|
||||
to: '/sessions/$sessionId/files',
|
||||
params: { sessionId: 'session-1' },
|
||||
search: { tab: 'directories' },
|
||||
replace: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -317,12 +317,24 @@ export default function FilesPage() {
|
||||
const { sessionId } = useParams({ from: '/sessions/$sessionId/files' })
|
||||
const search = useSearch({ from: '/sessions/$sessionId/files' })
|
||||
const { session } = useSession(api, sessionId)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const initialTab = search.tab === 'directories' ? 'directories' : 'changes'
|
||||
const [activeTab, setActiveTab] = useState<'changes' | 'directories'>(initialTab)
|
||||
const [directorySort, setDirectorySort] = useState<DirectorySort>(readDirectorySort)
|
||||
const searchQuery = search.query ?? ''
|
||||
|
||||
const setSearchQuery = useCallback((query: string) => {
|
||||
navigate({
|
||||
to: '/sessions/$sessionId/files',
|
||||
params: { sessionId },
|
||||
search: {
|
||||
...(activeTab === 'directories' ? { tab: 'directories' as const } : {}),
|
||||
...(query ? { query } : {}),
|
||||
},
|
||||
replace: true,
|
||||
})
|
||||
}, [activeTab, navigate, sessionId])
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
@@ -370,19 +382,18 @@ export default function FilesPage() {
|
||||
)
|
||||
|
||||
const handleOpenFile = useCallback((path: string, staged?: boolean) => {
|
||||
const fileSearch = staged === undefined
|
||||
? (activeTab === 'directories'
|
||||
? { path: encodeBase64(path), tab: 'directories' as const }
|
||||
: { path: encodeBase64(path) })
|
||||
: (activeTab === 'directories'
|
||||
? { path: encodeBase64(path), staged, tab: 'directories' as const }
|
||||
: { path: encodeBase64(path), staged })
|
||||
const fileSearch = {
|
||||
path: encodeBase64(path),
|
||||
...(staged !== undefined ? { staged } : {}),
|
||||
...(activeTab === 'directories' ? { tab: 'directories' as const } : {}),
|
||||
...(searchQuery ? { query: searchQuery } : {}),
|
||||
}
|
||||
navigate({
|
||||
to: '/sessions/$sessionId/file',
|
||||
params: { sessionId },
|
||||
search: fileSearch
|
||||
})
|
||||
}, [activeTab, navigate, sessionId])
|
||||
}, [activeTab, navigate, searchQuery, sessionId])
|
||||
|
||||
const branchLabel = getDetachedBranchLabel(gitStatus?.branch, t)
|
||||
const showGitErrorBanner = Boolean(gitError)
|
||||
@@ -423,10 +434,13 @@ export default function FilesPage() {
|
||||
navigate({
|
||||
to: '/sessions/$sessionId/files',
|
||||
params: { sessionId },
|
||||
search: nextTab === 'changes' ? {} : { tab: nextTab },
|
||||
search: {
|
||||
...(nextTab === 'directories' ? { tab: nextTab } : {}),
|
||||
...(searchQuery ? { query: searchQuery } : {}),
|
||||
},
|
||||
replace: true,
|
||||
})
|
||||
}, [navigate, sessionId])
|
||||
}, [navigate, searchQuery, sessionId])
|
||||
|
||||
const handleToggleFiles = useCallback(() => {
|
||||
navigate({
|
||||
|
||||
Reference in New Issue
Block a user