mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-11 22:51:04 +00:00
feat(web): add install method tabs
Signed-off-by: dongmucat <1127093059@qq.com>
This commit is contained in:
parent
1ec93db0d6
commit
89cb8e2999
7 changed files with 125 additions and 8 deletions
|
|
@ -11,6 +11,10 @@ function latestSeed(seed: PreparedSearchSeed) {
|
|||
}
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
let seeded: PreparedSearchSeed | undefined
|
||||
|
||||
test.describe('Public Skill Detail Anonymous Access (Real API)', () => {
|
||||
|
|
@ -40,7 +44,21 @@ test.describe('Public Skill Detail Anonymous Access (Real API)', () => {
|
|||
await expect(page).not.toHaveURL(/\/login\?returnTo=/)
|
||||
await expect(page.getByRole('heading', { name: current.skillName, exact: true })).toBeVisible()
|
||||
await expect(page.getByText('Install', { exact: true })).toBeVisible()
|
||||
await expect(page.getByText(new RegExp(`npx clawhub install ${current.skill.slug}`))).toBeVisible()
|
||||
const clawhubTarget = current.skill.namespace === 'global'
|
||||
? current.skill.slug
|
||||
: `${current.skill.namespace}--${current.skill.slug}`
|
||||
const skillhubNamespace = current.skill.namespace === 'global'
|
||||
? ''
|
||||
: ` --namespace ${current.skill.namespace}`
|
||||
|
||||
await expect(page.getByRole('tab', { name: 'ClawHub CLI' })).toHaveAttribute('aria-selected', 'true')
|
||||
await expect(page.getByText(new RegExp(`npx clawhub install ${escapeRegExp(clawhubTarget)} --registry`))).toBeVisible()
|
||||
await expect(page.getByRole('tab', { name: 'SkillHub CLI' })).toBeVisible()
|
||||
|
||||
await page.getByRole('tab', { name: 'SkillHub CLI' }).click()
|
||||
|
||||
await expect(page.getByRole('tab', { name: 'SkillHub CLI' })).toHaveAttribute('aria-selected', 'true')
|
||||
await expect(page.getByText(new RegExp(`npx @astron-team/skillhub@latest install ${escapeRegExp(current.skill.slug)}${escapeRegExp(skillhubNamespace)} --registry`))).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: 'Copy' }).first()).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import { createElement } from 'react'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { InstallCommand, buildInstallCommand, buildInstallTarget, getBaseUrl } from './install-command'
|
||||
import {
|
||||
InstallCommand,
|
||||
buildInstallCommand,
|
||||
buildInstallTarget,
|
||||
buildSkillhubInstallCommand,
|
||||
getBaseUrl,
|
||||
} from './install-command'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
|
|
@ -62,6 +68,18 @@ describe('install-command', () => {
|
|||
)
|
||||
})
|
||||
|
||||
it('builds a one-line SkillHub npx command for the global namespace', () => {
|
||||
expect(buildSkillhubInstallCommand('global', 'my-skill', 'https://skill.xfyun.cn')).toBe(
|
||||
'npx @astron-team/skillhub@latest install my-skill --registry https://skill.xfyun.cn',
|
||||
)
|
||||
})
|
||||
|
||||
it('builds a one-line SkillHub npx command with namespace for team skills', () => {
|
||||
expect(buildSkillhubInstallCommand('team-alpha', 'my-skill', 'https://skill.xfyun.cn')).toBe(
|
||||
'npx @astron-team/skillhub@latest install my-skill --namespace team-alpha --registry https://skill.xfyun.cn',
|
||||
)
|
||||
})
|
||||
|
||||
it('uses the runtime app base url when available', () => {
|
||||
setMockWindow('https://app.example.com')
|
||||
|
||||
|
|
@ -92,4 +110,19 @@ describe('install-command', () => {
|
|||
expect(html).toContain('leading-relaxed')
|
||||
expect(html).toContain('break-all')
|
||||
})
|
||||
|
||||
it('renders ClawHub CLI as the default install method', () => {
|
||||
setMockWindow('https://app.example.com')
|
||||
|
||||
const html = renderToStaticMarkup(createElement(InstallCommand, {
|
||||
namespace: 'team-alpha',
|
||||
slug: 'meeting-minutes-generator',
|
||||
}))
|
||||
|
||||
expect(html).toContain('skillDetail.installMethodClawhub')
|
||||
expect(html).toContain('skillDetail.installMethodSkillhub')
|
||||
expect(html).toContain('aria-selected="true"')
|
||||
expect(html).toContain('npx clawhub install team-alpha--meeting-minutes-generator --registry https://app.example.com')
|
||||
expect(html).not.toContain('npx @astron-team/skillhub@latest install meeting-minutes-generator --namespace team-alpha --registry https://app.example.com')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useMemo } from 'react'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { Check, Copy } from 'lucide-react'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/ui/tabs'
|
||||
import { useCopyToClipboard } from '@/shared/lib/clipboard'
|
||||
|
||||
interface InstallCommandProps {
|
||||
|
|
@ -33,14 +34,19 @@ export function buildInstallCommand(namespace: string, slug: string, baseUrl: st
|
|||
return `npx clawhub install ${installTarget} --registry ${baseUrl}`
|
||||
}
|
||||
|
||||
export function InstallCommand({ namespace, slug }: InstallCommandProps) {
|
||||
export function buildSkillhubInstallCommand(namespace: string, slug: string, baseUrl: string): string {
|
||||
const namespaceArg = namespace === 'global' ? '' : ` --namespace ${namespace}`
|
||||
return `npx @astron-team/skillhub@latest install ${slug}${namespaceArg} --registry ${baseUrl}`
|
||||
}
|
||||
|
||||
interface CommandBlockProps {
|
||||
command: string
|
||||
}
|
||||
|
||||
function CommandBlock({ command }: CommandBlockProps) {
|
||||
const { t } = useTranslation()
|
||||
const [copied, copy] = useCopyToClipboard()
|
||||
|
||||
const baseUrl = useMemo(() => getBaseUrl(), [])
|
||||
|
||||
const command = useMemo(() => buildInstallCommand(namespace, slug, baseUrl), [baseUrl, namespace, slug])
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await copy(command)
|
||||
|
|
@ -70,3 +76,29 @@ export function InstallCommand({ namespace, slug }: InstallCommandProps) {
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function InstallCommand({ namespace, slug }: InstallCommandProps) {
|
||||
const { t } = useTranslation()
|
||||
const baseUrl = useMemo(() => getBaseUrl(), [])
|
||||
const clawhubCommand = useMemo(() => buildInstallCommand(namespace, slug, baseUrl), [baseUrl, namespace, slug])
|
||||
const skillhubCommand = useMemo(() => buildSkillhubInstallCommand(namespace, slug, baseUrl), [baseUrl, namespace, slug])
|
||||
|
||||
return (
|
||||
<Tabs defaultValue="clawhub" className="space-y-3">
|
||||
<TabsList className="w-full gap-2 rounded-lg border bg-background/80 p-1 text-xs">
|
||||
<TabsTrigger value="clawhub" className="flex-1 rounded-md px-3 py-2 text-xs">
|
||||
{t('skillDetail.installMethodClawhub')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="skillhub" className="flex-1 rounded-md px-3 py-2 text-xs">
|
||||
{t('skillDetail.installMethodSkillhub')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="clawhub">
|
||||
<CommandBlock command={clawhubCommand} />
|
||||
</TabsContent>
|
||||
<TabsContent value="skillhub">
|
||||
<CommandBlock command={skillhubCommand} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -801,6 +801,8 @@
|
|||
"namespaceLabel": "Namespace",
|
||||
"loginToRate": "Login to star and rate",
|
||||
"install": "Install",
|
||||
"installMethodClawhub": "ClawHub CLI",
|
||||
"installMethodSkillhub": "SkillHub CLI",
|
||||
"download": "Download",
|
||||
"labelsSectionTitle": "Labels",
|
||||
"labelsSectionDescription": "Attach or remove recommended labels that help users filter and discover this skill.",
|
||||
|
|
|
|||
|
|
@ -801,6 +801,8 @@
|
|||
"namespaceLabel": "命名空间",
|
||||
"loginToRate": "登录后可以收藏和评分",
|
||||
"install": "安装",
|
||||
"installMethodClawhub": "ClawHub CLI",
|
||||
"installMethodSkillhub": "SkillHub CLI",
|
||||
"download": "下载",
|
||||
"labelsSectionTitle": "标签管理",
|
||||
"labelsSectionDescription": "为这个技能挂载或移除推荐标签,帮助用户筛选和发现。",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { createElement } from 'react'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs'
|
||||
|
||||
|
|
@ -28,4 +30,29 @@ describe('Tabs components', () => {
|
|||
expect(typeof TabsContent).toBe('function')
|
||||
expect(TabsContent.name).toBe('TabsContent')
|
||||
})
|
||||
|
||||
it('renders semantic tablist, tab, and tabpanel roles', () => {
|
||||
const html = renderToStaticMarkup(
|
||||
createElement(Tabs, {
|
||||
defaultValue: 'clawhub',
|
||||
children: [
|
||||
createElement(TabsList, {
|
||||
key: 'list',
|
||||
children: [
|
||||
createElement(TabsTrigger, { key: 'clawhub', value: 'clawhub', children: 'ClawHub CLI' }),
|
||||
createElement(TabsTrigger, { key: 'skillhub', value: 'skillhub', children: 'SkillHub CLI' }),
|
||||
],
|
||||
}),
|
||||
createElement(TabsContent, { key: 'clawhub-content', value: 'clawhub', children: 'clawhub command' }),
|
||||
createElement(TabsContent, { key: 'skillhub-content', value: 'skillhub', children: 'skillhub command' }),
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
expect(html).toContain('role="tablist"')
|
||||
expect(html).toContain('role="tab"')
|
||||
expect(html).toContain('aria-selected="true"')
|
||||
expect(html).toContain('aria-selected="false"')
|
||||
expect(html).toContain('role="tabpanel"')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ interface TabsListProps {
|
|||
export function TabsList({ children, className }: TabsListProps) {
|
||||
return (
|
||||
<div
|
||||
role="tablist"
|
||||
className={cn(
|
||||
'inline-flex items-center gap-6 border-b text-sm',
|
||||
className
|
||||
|
|
@ -69,6 +70,8 @@ export function TabsTrigger({ value, children, className }: TabsTriggerProps) {
|
|||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={isActive}
|
||||
onClick={() => context.setValue(value)}
|
||||
data-state={isActive ? 'active' : 'inactive'}
|
||||
className={cn(
|
||||
|
|
@ -96,5 +99,5 @@ export function TabsContent({ value, children, className }: TabsContentProps) {
|
|||
|
||||
if (context.value !== value) return null
|
||||
|
||||
return <div className={cn('animate-fade-in', className)}>{children}</div>
|
||||
return <div role="tabpanel" className={cn('animate-fade-in', className)}>{children}</div>
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue