mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-24 22:10:59 +00:00
fix(web): show file metadata in preview header (#1450)
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { mkdir, rm, stat, writeFile } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { tmpdir } from 'os'
|
||||
import { RpcHandlerManager } from '../../../api/rpc/RpcHandlerManager'
|
||||
import { registerFileHandlers } from './files'
|
||||
|
||||
async function createTempDir(prefix: string): Promise<string> {
|
||||
const path = join(tmpdir(), `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`)
|
||||
await mkdir(path, { recursive: true })
|
||||
return path
|
||||
}
|
||||
|
||||
describe('file RPC handlers', () => {
|
||||
let rootDir: string
|
||||
let rpc: RpcHandlerManager
|
||||
|
||||
beforeEach(async () => {
|
||||
rootDir = await createTempDir('hapi-file-handler')
|
||||
rpc = new RpcHandlerManager({ scopePrefix: 'session-test' })
|
||||
registerFileHandlers(rpc, rootDir)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(rootDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('returns file metadata alongside content', async () => {
|
||||
const filePath = join(rootDir, 'README.md')
|
||||
await writeFile(filePath, '# test')
|
||||
const expectedStats = await stat(filePath)
|
||||
|
||||
const response = await rpc.handleRequest({
|
||||
method: 'session-test:readFile',
|
||||
params: JSON.stringify({ path: 'README.md' })
|
||||
})
|
||||
const parsed = JSON.parse(response) as {
|
||||
success: boolean
|
||||
content?: string
|
||||
size?: number
|
||||
modified?: number
|
||||
}
|
||||
|
||||
expect(parsed.success).toBe(true)
|
||||
expect(parsed.content).toBe(Buffer.from('# test').toString('base64'))
|
||||
expect(parsed.size).toBe(expectedStats.size)
|
||||
expect(parsed.modified).toBe(expectedStats.mtime.getTime())
|
||||
})
|
||||
})
|
||||
@@ -44,9 +44,15 @@ export function registerFileHandlers(rpcHandlerManager: RpcHandlerManager, worki
|
||||
|
||||
try {
|
||||
const resolvedPath = resolve(workingDirectory, data.path)
|
||||
const stats = await stat(resolvedPath)
|
||||
const buffer = await readFile(resolvedPath)
|
||||
const content = buffer.toString('base64')
|
||||
return { success: true, content }
|
||||
return {
|
||||
success: true,
|
||||
content,
|
||||
size: stats.size,
|
||||
modified: stats.mtime.getTime()
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug('Failed to read file:', error)
|
||||
return rpcError(getErrorMessage(error, 'Failed to read file'))
|
||||
|
||||
@@ -627,6 +627,8 @@ export type GitCommandResponse = CommandResponse
|
||||
export type FileReadResponse = {
|
||||
success: boolean
|
||||
content?: string
|
||||
size?: number
|
||||
modified?: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { I18nProvider } from '@/lib/i18n-context'
|
||||
import { formatFileMetadata } from '@/lib/file-metadata'
|
||||
import { encodeBase64 } from '@/lib/utils'
|
||||
import FilePage from './file'
|
||||
|
||||
@@ -12,6 +13,8 @@ const sampleMarkdown = '# Heading\n\n| Col A | Col B |\n| --- | --- |\n| one | t
|
||||
const filePath = 'docs/README.md'
|
||||
const encodedPath = encodeBase64(filePath)
|
||||
const encodedContent = encodeBase64(sampleMarkdown)
|
||||
const fileSize = 1024
|
||||
const fileModified = 1_784_175_060_000
|
||||
|
||||
vi.mock('@tanstack/react-router', () => ({
|
||||
useParams: () => ({ sessionId: 'session-1' }),
|
||||
@@ -28,6 +31,8 @@ vi.mock('@/lib/app-context', () => ({
|
||||
readSessionFile: vi.fn(async () => ({
|
||||
success: true,
|
||||
content: encodedContent,
|
||||
size: fileSize,
|
||||
modified: fileModified,
|
||||
})),
|
||||
},
|
||||
}),
|
||||
@@ -82,6 +87,8 @@ describe('FilePage markdown preview', () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('markdown-preview')).toHaveTextContent('# Heading')
|
||||
})
|
||||
expect(screen.getByText(formatFileMetadata(fileSize, fileModified, 'en')!)).toBeInTheDocument()
|
||||
expect(screen.getAllByText(filePath)).toHaveLength(1)
|
||||
const previewCopyButton = screen.getByRole('button', { name: 'Copy file content' })
|
||||
expect(previewCopyButton.closest('[data-hapi-file-content-header="true"]')).not.toBeNull()
|
||||
expect(previewCopyButton).not.toHaveClass('absolute')
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useTranslation } from '@/lib/use-translation'
|
||||
import { decodeBase64 } from '@/lib/utils'
|
||||
import { ImagePreview } from '@/components/ImagePreview'
|
||||
import { MarkdownRenderer } from '@/components/MarkdownRenderer'
|
||||
import { formatFileMetadata } from '@/lib/file-metadata'
|
||||
import {
|
||||
getInitialMarkdownPreviewMode,
|
||||
isMarkdownFile,
|
||||
@@ -199,7 +200,7 @@ function extractCommandError(result: GitCommandResponse | undefined): string | n
|
||||
|
||||
export default function FilePage() {
|
||||
const { api } = useAppContext()
|
||||
const { t } = useTranslation()
|
||||
const { t, locale } = useTranslation()
|
||||
const { copied: pathCopied, copy: copyPath } = useCopyToClipboard()
|
||||
const { copied: contentCopied, copy: copyContent } = useCopyToClipboard()
|
||||
const goBack = useAppGoBack()
|
||||
@@ -298,6 +299,7 @@ export default function FilePage() {
|
||||
const missingPath = !filePath
|
||||
const diffErrorMessage = diffError ? formatDiffError(diffError, t) : null
|
||||
const fileErrorMessage = fileError ? formatReadFileError(fileError, t) : null
|
||||
const fileMetadata = formatFileMetadata(fileContentResult?.size, fileContentResult?.modified, locale)
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
@@ -312,7 +314,7 @@ export default function FilePage() {
|
||||
</button>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate font-semibold">{fileName}</div>
|
||||
<div className="truncate text-xs text-[var(--app-hint)]">{filePath || t('file.page.unknownPath')}</div>
|
||||
<div className="truncate text-xs text-[var(--app-hint)]">{fileMetadata ?? (filePath || t('file.page.unknownPath'))}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user