Merge pull request #557 from iflytek/fix/relative-link-nested-preview
Some checks failed
Security / Dependency Review (push) Has been cancelled
Deploy Docs / build (push) Has been cancelled
Security / CodeQL (java-kotlin) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Deploy Docs / Deploy (push) Has been cancelled

fix(frontend): support nested preview links
This commit is contained in:
dongmucat 2026-06-30 17:29:08 +08:00 committed by GitHub
commit 06cc523a0c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 69 additions and 7 deletions

View file

@ -28,8 +28,12 @@ test.describe('Skill Detail Relative Links (Real API)', () => {
extraFiles: [
{
path: 'docs/usage.md',
content: '# Usage\n\nThis is linked documentation.',
content: '# Usage\n\nThis is linked documentation.\n\n[Nested](nested.md)',
},
{
path: 'docs/nested.md',
content: '# Nested\n\nSecond-level linked documentation.',
}
],
})
@ -40,6 +44,11 @@ test.describe('Skill Detail Relative Links (Real API)', () => {
await page.getByRole('link', { name: 'Usage' }).click()
await expect(page.getByRole('dialog')).toContainText('usage.md')
await expect(page.getByRole('dialog')).toContainText('This is linked documentation.')
await expect(page.getByRole('dialog').getByRole('link', { name: 'Nested' })).toBeVisible()
await page.getByRole('dialog').getByRole('link', { name: 'Nested' }).click()
await expect(page.getByRole('dialog')).toContainText('nested.md')
await expect(page.getByRole('dialog')).toContainText('Second-level linked documentation.')
await page.getByRole('button', { name: 'Close' }).click()
await expect(page.getByRole('dialog')).toBeHidden()

View file

@ -1,4 +1,4 @@
import { useState } from 'react'
import { useState, type MouseEvent } from 'react'
import { Copy, Check, Download, X } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Dialog, DialogContent } from '@/shared/ui/dialog'
@ -18,6 +18,7 @@ interface FilePreviewDialogProps {
isLoading: boolean
error: Error | null
onDownload: () => void
onLinkClick?: (href: string, event: MouseEvent<HTMLAnchorElement>) => void
}
/**
@ -33,6 +34,7 @@ export function FilePreviewDialog({
isLoading,
error,
onDownload,
onLinkClick,
}: FilePreviewDialogProps) {
const { t } = useTranslation()
// Tracks the copy animation state: idle → spinning → done
@ -143,7 +145,7 @@ export function FilePreviewDialog({
<Button onClick={onDownload}>{t('filePreview.downloadHint', { name: node.name })}</Button>
</div>
) : content && isMarkdown ? (
<MarkdownRenderer content={content} />
<MarkdownRenderer content={content} onLinkClick={onLinkClick} />
) : content && shouldHighlight ? (
<CodeRenderer code={content} language={language} />
) : content ? (

View file

@ -127,8 +127,27 @@ vi.mock('@/features/skill/markdown-renderer', () => ({
}))
vi.mock('@/features/skill/file-preview-dialog', () => ({
FilePreviewDialog: ({ open, node }: { open: boolean; node: { path: string } | null }) => (
open && node ? <div role="dialog">preview:{node.path}</div> : null
FilePreviewDialog: ({
open,
node,
onLinkClick,
}: {
open: boolean
node: { path: string } | null
onLinkClick?: (href: string, event: MouseEvent<HTMLAnchorElement>) => void
}) => (
open && node
? (
<div role="dialog">
preview:{node.path}
{onLinkClick && (
<a href="nested.md" onClick={(event) => onLinkClick('nested.md', event)}>
Nested
</a>
)}
</div>
)
: null
),
}))
@ -490,6 +509,25 @@ describe('SkillDetailPage', () => {
expect(toastMocks.error).not.toHaveBeenCalled()
})
it('resolves links inside previewed markdown files against the previewed file path', () => {
useSkillFilesMock.mockReturnValue({
data: [
createSkillFile('README.md'),
createSkillFile('docs/usage.md'),
createSkillFile('docs/nested.md'),
],
})
render(<SkillDetailPage />)
fireEvent.click(screen.getByRole('link', { name: 'Usage' }))
expect(screen.getByRole('dialog').textContent).toContain('preview:docs/usage.md')
fireEvent.click(screen.getByRole('link', { name: 'Nested' }))
expect(screen.getByRole('dialog').textContent).toContain('preview:docs/nested.md')
expect(toastMocks.error).not.toHaveBeenCalled()
})
it('keeps the viewer on the detail page and shows a toast for missing package files', () => {
useSkillFilesMock.mockReturnValue({
data: [

View file

@ -300,8 +300,12 @@ export function SkillDetailPage() {
setPreviewDialogOpen(true)
}
const handleOverviewLinkClick = (href: string, event: MouseEvent<HTMLAnchorElement>) => {
const resolution = resolvePackageRelativeLink(href, documentationPath, files)
const handlePackageMarkdownLinkClick = (
href: string,
event: MouseEvent<HTMLAnchorElement>,
currentFilePath: string | null | undefined,
) => {
const resolution = resolvePackageRelativeLink(href, currentFilePath, files)
if (resolution.status === 'ignored') {
return
@ -318,6 +322,14 @@ export function SkillDetailPage() {
toast.error(t('skillDetail.packageLinkMissingTitle'), t('skillDetail.packageLinkMissingDescription'))
}
const handleOverviewLinkClick = (href: string, event: MouseEvent<HTMLAnchorElement>) => {
handlePackageMarkdownLinkClick(href, event, documentationPath)
}
const handlePreviewLinkClick = (href: string, event: MouseEvent<HTMLAnchorElement>) => {
handlePackageMarkdownLinkClick(href, event, previewNode?.path)
}
// Download a single file from the skill version
const handleDownloadFile = () => {
const isAnonymousAllowed = namespace === 'global' && skill?.visibility === 'PUBLIC'
@ -1652,6 +1664,7 @@ export function SkillDetailPage() {
isLoading={isLoadingPreview}
error={previewError}
onDownload={handleDownloadFile}
onLinkClick={handlePreviewLinkClick}
/>
</div>
)