fix(web): address follow-up review feedback

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
XiaoSeS 2026-09-09 18:07:23 +08:00
parent 66086a8bdb
commit 90be27e1a7
4 changed files with 95 additions and 9 deletions

View file

@ -1,6 +1,22 @@
import { describe, expect, it } from 'vitest'
// @vitest-environment jsdom
import { createElement } from 'react'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import * as mod from './landing-quick-start'
vi.mock('react-i18next', async () => {
const actual = await vi.importActual<typeof import('react-i18next')>('react-i18next')
return {
...actual,
useTranslation: () => ({
t: (key: string, values?: Record<string, string>) => values?.url
? `${key} ${values.url}`
: key,
}),
}
})
/**
* LandingQuickStartSection is a React component that renders a tabbed quick-start
* section with agent/human tabs and copy-to-clipboard commands.
@ -11,7 +27,49 @@ import * as mod from './landing-quick-start'
* if the export contract changes.
*/
describe('landing-quick-start module exports', () => {
const originalRuntimeConfig = window.__SKILLHUB_RUNTIME_CONFIG__
afterEach(() => {
vi.restoreAllMocks()
window.__SKILLHUB_RUNTIME_CONFIG__ = originalRuntimeConfig
})
it('exports the LandingQuickStartSection component', () => {
expect(mod.LandingQuickStartSection).toBeTypeOf('function')
})
it('uses the self-hosted Registry URL in displayed and copied CLI commands', async () => {
const writeText = vi.fn().mockResolvedValue(undefined)
Object.defineProperty(globalThis.navigator, 'clipboard', {
configurable: true,
value: { writeText },
})
window.__SKILLHUB_RUNTIME_CONFIG__ = {
appBaseUrl: 'https://registry.internal.example/skills',
}
render(createElement(mod.LandingQuickStartSection))
const cliLabel = screen.getByText('landing.experience.quickStart.modes.cli.title')
await act(async () => fireEvent.click(cliLabel.closest('button')!))
expect(screen.getAllByText('https://registry.internal.example/skills')).toHaveLength(2)
expect(screen.getByText('registry: https://registry.internal.example/skills')).toBeTruthy()
const copyButtons = screen.getAllByRole('button', {
name: 'landing.experience.quickStart.copy',
})
await act(async () => fireEvent.click(copyButtons[1]))
await act(async () => fireEvent.click(copyButtons[2]))
await waitFor(() => {
expect(writeText).toHaveBeenNthCalledWith(
1,
'npx -y @astron-team/skillhub@0.1.11 search weather --registry https://registry.internal.example/skills --limit 5',
)
expect(writeText).toHaveBeenNthCalledWith(
2,
'npx -y @astron-team/skillhub@0.1.11 install @global/weather --dir ./skills --registry https://registry.internal.example/skills',
)
})
})
})

View file

@ -131,6 +131,7 @@ function AgentAccessPanel() {
function CliAccessPanel() {
const { t } = useTranslation()
const [copiedIdx, setCopiedIdx] = useState(-1)
const registryUrl = useMemo(getRegistryUrl, [])
const handleCopy = (text: string, idx: number) => {
void copyToClipboard(text)
@ -191,7 +192,7 @@ function CliAccessPanel() {
<span className="text-neutral-800 dark:text-neutral-200">npx -y @astron-team/skillhub@0.1.11 search weather \</span>
<button
type="button"
onClick={() => handleCopy('npx -y @astron-team/skillhub@0.1.11 search weather --registry https://skill.xfyun.cn --limit 5', 1)}
onClick={() => handleCopy(`npx -y @astron-team/skillhub@0.1.11 search weather --registry ${registryUrl} --limit 5`, 1)}
className="ml-2 inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 text-[10px] text-muted-foreground align-middle transition-[color,background-color,transform] hover:bg-neutral-100 hover:text-foreground active:scale-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring dark:hover:bg-neutral-800"
>
{copiedIdx === 1 ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
@ -201,7 +202,7 @@ function CliAccessPanel() {
</div>
<div className="pl-5 text-neutral-600 dark:text-neutral-400">
<span className="text-muted-foreground">{' --registry'} </span>
<span className="text-blue-600 dark:text-blue-400">https://skill.xfyun.cn</span> \
<span className="break-all text-blue-600 dark:text-blue-400">{registryUrl}</span> \
</div>
<div className="pl-5 text-neutral-600 dark:text-neutral-400">
<span className="text-muted-foreground">{' --limit'} </span>
@ -230,7 +231,7 @@ function CliAccessPanel() {
<span className="text-neutral-800 dark:text-neutral-200">npx -y @astron-team/skillhub@0.1.11 install @global/weather \</span>
<button
type="button"
onClick={() => handleCopy('npx -y @astron-team/skillhub@0.1.11 install @global/weather --dir ./skills --registry https://skill.xfyun.cn', 2)}
onClick={() => handleCopy(`npx -y @astron-team/skillhub@0.1.11 install @global/weather --dir ./skills --registry ${registryUrl}`, 2)}
className="ml-2 inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 text-[10px] text-muted-foreground align-middle transition-[color,background-color,transform] hover:bg-neutral-100 hover:text-foreground active:scale-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring dark:hover:bg-neutral-800"
>
{copiedIdx === 2 ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
@ -244,7 +245,7 @@ function CliAccessPanel() {
</div>
<div className="pl-5 text-neutral-600 dark:text-neutral-400">
<span className="text-muted-foreground">{' --registry'} </span>
<span className="text-blue-600 dark:text-blue-400">https://skill.xfyun.cn</span>
<span className="break-all text-blue-600 dark:text-blue-400">{registryUrl}</span>
</div>
<div className="mt-1 flex items-center gap-1.5 pl-5 text-[11px] text-emerald-600 dark:text-emerald-400">
<Check className="h-3 w-3" strokeWidth={2.5} />
@ -260,14 +261,14 @@ function CliAccessPanel() {
</div>
{/* Bottom status bar */}
<div className="flex items-center justify-between border-t border-border/60 bg-white px-4 py-2.5 text-[11px] dark:bg-neutral-900">
<div className="flex items-center gap-3 text-muted-foreground">
<div className="flex flex-wrap items-center justify-between gap-2 border-t border-border/60 bg-white px-4 py-2.5 text-[11px] dark:bg-neutral-900">
<div className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1 text-muted-foreground">
<span className="flex items-center gap-1">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
{t('landing.experience.quickStart.cli.connected')}
</span>
<span className="text-border">·</span>
<span className="font-mono">registry: skill.xfyun.cn</span>
<span className="break-all font-mono">registry: {registryUrl}</span>
</div>
<a href="https://github.com/iflytek/skillhub/tree/main/cli" target="_blank" rel="noreferrer" className="group flex items-center gap-1 font-medium text-blue-600 transition-colors hover:text-blue-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring dark:text-blue-400 dark:hover:text-blue-300">
{t('landing.experience.quickStart.cli.docs')}

View file

@ -108,6 +108,26 @@ describe('UserMenu navigation', () => {
expect(html).toContain('user.menu.auditLog')
})
it.each(['SKILL_ADMIN', 'USER_ADMIN', 'SUPER_ADMIN'])(
'keeps the review center available to %s users',
(role) => {
const html = renderToStaticMarkup(
<UserMenu user={{ displayName: 'Reviewer', platformRoles: [role] }} />,
)
expect(html).toContain('href="/dashboard/reviews"')
expect(html).toContain('user.menu.reviews')
},
)
it('does not show the global review center to regular users', () => {
const html = renderToStaticMarkup(
<UserMenu user={{ displayName: 'Regular User', platformRoles: ['USER'] }} />,
)
expect(html).not.toContain('href="/dashboard/reviews"')
})
it('shows security settings only for accounts that can change a password', () => {
const enabledHtml = renderToStaticMarkup(
<UserMenu user={{ displayName: 'Local User', platformRoles: ['USER'], canChangePassword: true }} />,

View file

@ -4,6 +4,7 @@ import { Link } from '@tanstack/react-router'
import { useQueryClient } from '@tanstack/react-query'
import { authApi } from '@/api/client'
import { clearSessionScopedQueries } from '@/features/notification/notification-session'
import { canAccessGlobalReviewCenter } from '@/features/review/review-paths'
import { withBasePath } from '@/shared/lib/base-path'
import { cn } from '@/shared/lib/utils'
@ -32,6 +33,7 @@ export function UserMenu({ user, triggerClassName }: UserMenuProps) {
const isUserAdmin = hasRole('USER_ADMIN') || hasRole('SUPER_ADMIN')
const isAuditor = hasRole('AUDITOR') || hasRole('SUPER_ADMIN')
const isSuperAdmin = hasRole('SUPER_ADMIN')
const canReview = canAccessGlobalReviewCenter(user.platformRoles)
const open = isHovered || isClickOpen
const clearCloseTimer = () => {
@ -148,7 +150,12 @@ export function UserMenu({ user, triggerClassName }: UserMenuProps) {
{t('user.menu.security')}
</Link>
) : null}
{isUserAdmin || isAuditor || isSuperAdmin ? <div className="-mx-1 my-1 h-px bg-muted" /> : null}
{canReview ? (
<Link to="/dashboard/reviews" className={menuItemClassName} onClick={closeMenu}>
{t('user.menu.reviews')}
</Link>
) : null}
{canReview || isUserAdmin || isAuditor || isSuperAdmin ? <div className="-mx-1 my-1 h-px bg-muted" /> : null}
{isUserAdmin ? (
<Link to="/admin/users" className={menuItemClassName} onClick={closeMenu}>
{t('user.menu.users')}