feat(suite): enrich overview with entry skill guidance

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
XiaoSeS 2026-09-09 16:39:34 +08:00
parent b92d8da70f
commit ff0a1cd4cb
5 changed files with 73 additions and 11 deletions

View file

@ -1778,6 +1778,9 @@
"viewMember": "View skill: {{name}}",
"pinnedVersion": "Pinned version v{{version}}",
"entrySkill": "Entry skill",
"startWithEntry": "Start with the entry skill",
"startWithEntryDescription": "Installing this suite gives you {{count}} version-pinned skills. Open the entry skill first to understand the primary way to use them together.",
"viewEntrySkill": "View entry skill",
"versionHistory": "Version history",
"validationRequired": "Complete the required fields and select at least one skill",
"draftUpdated": "Suite draft updated",

View file

@ -1775,6 +1775,9 @@
"viewMember": "Открыть навык: {{name}}",
"pinnedVersion": "Зафиксированная версия v{{version}}",
"entrySkill": "Входной навык",
"startWithEntry": "Начните с входного навыка",
"startWithEntryDescription": "После установки набора вы получите {{count}} навыков с зафиксированными версиями. Сначала откройте входной навык, чтобы понять основной сценарий их совместного использования.",
"viewEntrySkill": "Открыть входной навык",
"versionHistory": "История версий",
"validationRequired": "Заполните обязательные поля и выберите хотя бы один навык",
"draftUpdated": "Черновик набора обновлён",

View file

@ -1777,6 +1777,9 @@
"viewMember": "查看技能:{{name}}",
"pinnedVersion": "固定版本 v{{version}}",
"entrySkill": "入口技能",
"startWithEntry": "从入口技能开始",
"startWithEntryDescription": "安装套件后将获得 {{count}} 个固定版本的技能。先打开入口技能,了解这组技能的主要使用方式。",
"viewEntrySkill": "查看入口技能",
"versionHistory": "版本历史",
"validationRequired": "请填写必填项并至少选择一个技能",
"draftUpdated": "套件草稿已更新",

View file

@ -93,13 +93,19 @@ describe('SuiteDetailPage', () => {
vi.clearAllMocks()
})
it('separates the Markdown overview from browsable member skills', () => {
it('adds entry skill guidance to the overview and keeps the full member grid separate', () => {
mocks.detail = { data: suite(), isLoading: false, error: null }
render(<SuiteDetailPage />)
expect(screen.getByTestId('suite-overview').textContent).toContain('Run the entry skill first.')
expect(screen.queryByText('Medical Records')).toBeNull()
expect(screen.getByText('suite.startWithEntry')).not.toBeNull()
expect(screen.getByText('suite.startWithEntryDescription')).not.toBeNull()
expect(screen.getByText('Medical Records')).not.toBeNull()
expect(screen.getByText('@global/medical-records@1.0.0')).not.toBeNull()
expect(screen.getByRole('link', { name: 'suite.viewEntrySkill' }).getAttribute('href'))
.toContain('/space/global/medical-records')
expect(screen.queryByText('@global/deleted-helper')).toBeNull()
fireEvent.click(screen.getByRole('tab', { name: 'suite.membersTab' }))

View file

@ -7,7 +7,7 @@ import { suiteBlockingReasonLabel, suiteStatusLabel, suiteVisibilityLabel } from
import { SuiteManagementActions } from '@/features/suite/suite-management-actions'
import { MarkdownRenderer } from '@/features/skill/markdown-renderer'
import { Card } from '@/shared/ui/card'
import { Button } from '@/shared/ui/button'
import { Button, buttonVariants } from '@/shared/ui/button'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/ui/tabs'
import { SkeletonList } from '@/shared/components/skeleton-loader'
import { NamespaceBadge } from '@/shared/components/namespace-badge'
@ -53,6 +53,7 @@ export function SuiteDetailPage() {
const hasPrimaryActions = suite.allowedActions.includes('EDIT')
|| suite.allowedActions.includes('SUBMIT')
|| suite.allowedActions.includes('PUBLISH_PRIVATE')
const entryMember = suite.members.find(member => member.entry)
return (
<div className={cn(APP_SHELL_PAGE_CLASS_NAME, 'mx-auto max-w-6xl')}>
@ -94,14 +95,60 @@ export function SuiteDetailPage() {
</TabsList>
<TabsContent value="overview" className="mt-6">
<Card className="p-8">
{suite.overview ? (
<MarkdownRenderer content={suite.overview} />
) : (
<p className="text-sm leading-7 text-muted-foreground">
{suite.summary || t('suite.noOverview')}
</p>
)}
<Card className="overflow-hidden">
<div className="p-6 sm:p-8">
{suite.overview ? (
<MarkdownRenderer content={suite.overview} />
) : (
<p className="text-sm leading-7 text-muted-foreground">
{suite.summary || t('suite.noOverview')}
</p>
)}
</div>
{entryMember ? (
<section className="border-t border-border/60 bg-secondary/30 p-6 sm:p-8">
<div className="flex flex-col gap-5 sm:flex-row sm:items-center sm:justify-between">
<div className="min-w-0">
<h2 className="font-heading text-lg font-semibold text-foreground">
{t('suite.startWithEntry')}
</h2>
<p className="mt-1 max-w-2xl text-sm leading-6 text-muted-foreground">
{t('suite.startWithEntryDescription', { count: suite.members.length })}
</p>
</div>
{entryMember.browsable && !entryMember.blockingReason
&& entryMember.skillId && entryMember.skillVersionId ? (
<Link
to="/space/$namespace/$slug"
params={{ namespace: entryMember.namespace, slug: entryMember.slug }}
search={{ returnTo }}
className={cn(buttonVariants({ variant: 'outline', size: 'sm' }), 'shrink-0 gap-1.5')}
>
{t('suite.viewEntrySkill')}
<ArrowUpRight className="h-3.5 w-3.5" aria-hidden="true" />
</Link>
) : null}
</div>
<div className="mt-5 flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
<p className="flex min-w-0 items-center gap-2 font-semibold text-foreground">
<Wrench className="h-4 w-4 shrink-0 text-primary" aria-hidden="true" />
<span className="truncate">
{entryMember.displayName || `@${entryMember.namespace}/${entryMember.slug}`}
</span>
</p>
<p className="break-all font-mono text-xs text-muted-foreground">
@{entryMember.namespace}/{entryMember.slug}@{entryMember.version}
</p>
{entryMember.blockingReason ? (
<span className="text-xs font-medium text-destructive">
{suiteBlockingReasonLabel(t, entryMember.blockingReason)}
</span>
) : null}
</div>
</section>
) : null}
</Card>
</TabsContent>