mirror of
https://github.com/iflytek/skillhub.git
synced 2026-11-15 09:11:18 +00:00
fix(web): address follow-up review feedback
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
parent
66086a8bdb
commit
90be27e1a7
4 changed files with 95 additions and 9 deletions
|
|
@ -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',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
|
|
|
|||
|
|
@ -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 }} />,
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue