mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-10 03:27:54 +00:00
feat(dashboard): add Update button to My Skills with publish prefill
Add an "Update" button to each skill card on the My Skills dashboard page. Clicking it navigates to the Publish page with the skill's namespace and visibility pre-selected, reducing manual steps when re-publishing a skill package. - Add visibility field to SkillSummaryResponse so the list API exposes each skill's current visibility setting - Add publish-prefill module to normalize and validate URL search params - Wire TanStack Router validateSearch on the publish route - Add E2E tests covering the prefill flow and invalid-param fallback
This commit is contained in:
parent
60a30190bf
commit
230b915194
18 changed files with 510 additions and 18 deletions
|
|
@ -8,6 +8,7 @@ public record SkillSummaryResponse(
|
|||
String slug,
|
||||
String displayName,
|
||||
String summary,
|
||||
String visibility,
|
||||
String status,
|
||||
Long downloadCount,
|
||||
Integer starCount,
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ public class JpaMySkillQueryRepository implements MySkillQueryRepository {
|
|||
skill.getSlug(),
|
||||
skill.getDisplayName(),
|
||||
skill.getSummary(),
|
||||
skill.getVisibility().name(),
|
||||
skill.getStatus().name(),
|
||||
skill.getDownloadCount(),
|
||||
skill.getStarCount(),
|
||||
|
|
|
|||
|
|
@ -198,6 +198,7 @@ public class SkillSearchAppService {
|
|||
skill.getSlug(),
|
||||
skill.getDisplayName(),
|
||||
skill.getSummary(),
|
||||
skill.getVisibility().name(),
|
||||
skill.getStatus().name(),
|
||||
skill.getDownloadCount(),
|
||||
skill.getStarCount(),
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ class ClawHubCompatControllerTest {
|
|||
"my-skill",
|
||||
"My Skill",
|
||||
"test summary",
|
||||
"PUBLIC",
|
||||
"ACTIVE",
|
||||
10L,
|
||||
5,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ class ClawHubRegistryFacadeTest {
|
|||
"time-skill",
|
||||
"Time Skill",
|
||||
"summary",
|
||||
"PUBLIC",
|
||||
"ACTIVE",
|
||||
12L,
|
||||
3,
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ class MeControllerTest {
|
|||
"copilot",
|
||||
"Copilot",
|
||||
"Assist with code review",
|
||||
"PUBLIC",
|
||||
"ACTIVE",
|
||||
12L,
|
||||
3,
|
||||
|
|
|
|||
57
web/e2e/my-skills-update-prefill.spec.ts
Normal file
57
web/e2e/my-skills-update-prefill.spec.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { expect, test } from '@playwright/test'
|
||||
import { setEnglishLocale } from './helpers/auth-fixtures'
|
||||
import { registerSession } from './helpers/session'
|
||||
import { E2eTestDataBuilder } from './helpers/test-data-builder'
|
||||
|
||||
test.describe('My Skills Update Prefill (Real API)', () => {
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
await setEnglishLocale(page)
|
||||
await registerSession(page, testInfo)
|
||||
})
|
||||
|
||||
test('opens publish page with namespace and visibility prefilled from my skills card', async ({ page }, testInfo) => {
|
||||
const builder = new E2eTestDataBuilder(page, testInfo)
|
||||
await builder.init()
|
||||
|
||||
try {
|
||||
const namespace = await builder.ensureWritableNamespace()
|
||||
const skillName = `update-ui-${Date.now().toString(36)}`
|
||||
await builder.publishSkill(namespace.slug, { name: skillName })
|
||||
|
||||
await page.goto('/dashboard/skills')
|
||||
await expect(page.getByRole('heading', { name: 'My Skills' })).toBeVisible()
|
||||
|
||||
const skillCard = page.locator('.group').filter({
|
||||
has: page.getByRole('heading', { name: skillName, exact: true }),
|
||||
}).first()
|
||||
await expect(skillCard).toBeVisible()
|
||||
await skillCard.getByRole('button', { name: 'Update' }).click()
|
||||
|
||||
await expect(page).toHaveURL(/\/dashboard\/publish/)
|
||||
|
||||
const currentUrl = new URL(page.url())
|
||||
expect(currentUrl.searchParams.get('namespace')).toBe(namespace.slug)
|
||||
expect(currentUrl.searchParams.get('visibility')).toBe('PUBLIC')
|
||||
await expect(page.locator('#namespace')).toContainText(`@${namespace.slug}`)
|
||||
await expect(page.locator('#visibility')).toContainText('Public')
|
||||
} finally {
|
||||
await builder.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test('falls back to public visibility when publish search params are invalid', async ({ page }, testInfo) => {
|
||||
const builder = new E2eTestDataBuilder(page, testInfo)
|
||||
await builder.init()
|
||||
|
||||
try {
|
||||
const namespace = await builder.ensureWritableNamespace()
|
||||
|
||||
await page.goto(`/dashboard/publish?namespace=${encodeURIComponent(namespace.slug)}&visibility=internal`)
|
||||
await expect(page.getByRole('heading', { name: 'Publish Skill' })).toBeVisible()
|
||||
await expect(page.locator('#namespace')).toContainText(`@${namespace.slug}`)
|
||||
await expect(page.locator('#visibility')).toContainText('Public')
|
||||
} finally {
|
||||
await builder.cleanup()
|
||||
}
|
||||
})
|
||||
})
|
||||
180
web/src/api/generated/schema.d.ts
vendored
180
web/src/api/generated/schema.d.ts
vendored
|
|
@ -468,6 +468,38 @@ export interface paths {
|
|||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/skills/{namespace}/{slug}/submit-review": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["submitForReview"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/web/skills/{namespace}/{slug}/submit-review": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["submitForReview_1"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/skills/{namespace}/{slug}/reports": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
@ -500,6 +532,38 @@ export interface paths {
|
|||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/web/skills/{namespace}/{slug}/confirm-publish": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["confirmPublish"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/skills/{namespace}/{slug}/confirm-publish": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["confirmPublish_1"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/skills/{namespace}/{slug}/archive": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
@ -3239,6 +3303,10 @@ export interface components {
|
|||
targetVersion: string;
|
||||
confirmWarnings?: boolean;
|
||||
};
|
||||
SubmitReviewRequest: {
|
||||
version: string;
|
||||
targetVisibility: string;
|
||||
};
|
||||
SkillReportSubmitRequest: {
|
||||
reason?: string;
|
||||
details?: string;
|
||||
|
|
@ -3257,6 +3325,9 @@ export interface components {
|
|||
reportId?: number;
|
||||
status?: string;
|
||||
};
|
||||
ConfirmPublishRequest: {
|
||||
version: string;
|
||||
};
|
||||
AdminSkillActionRequest: {
|
||||
reason?: string;
|
||||
};
|
||||
|
|
@ -3648,6 +3719,7 @@ export interface components {
|
|||
slug?: string;
|
||||
displayName?: string;
|
||||
summary?: string;
|
||||
visibility?: string;
|
||||
status?: string;
|
||||
/** Format: int64 */
|
||||
downloadCount?: number;
|
||||
|
|
@ -5569,6 +5641,60 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
submitForReview: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
namespace: string;
|
||||
slug: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["SubmitReviewRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["ApiResponseSkillLifecycleMutationResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
submitForReview_1: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
namespace: string;
|
||||
slug: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["SubmitReviewRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["ApiResponseSkillLifecycleMutationResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
submitReport: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
@ -5623,6 +5749,60 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
confirmPublish: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
namespace: string;
|
||||
slug: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ConfirmPublishRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["ApiResponseSkillLifecycleMutationResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
confirmPublish_1: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
namespace: string;
|
||||
slug: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ConfirmPublishRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["ApiResponseSkillLifecycleMutationResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
archiveSkill: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ export interface SkillSummary {
|
|||
slug: string
|
||||
displayName: string
|
||||
summary?: string
|
||||
visibility?: string
|
||||
status?: string
|
||||
downloadCount: number
|
||||
starCount: number
|
||||
|
|
|
|||
|
|
@ -248,6 +248,10 @@ const dashboardPublishRoute = createRoute({
|
|||
getParentRoute: () => rootRoute,
|
||||
path: 'dashboard/publish',
|
||||
beforeLoad: requireAuth,
|
||||
validateSearch: (search: Record<string, unknown>): { namespace?: string; visibility?: string } => ({
|
||||
namespace: typeof search.namespace === 'string' && search.namespace ? search.namespace : undefined,
|
||||
visibility: typeof search.visibility === 'string' && search.visibility ? search.visibility : undefined,
|
||||
}),
|
||||
component: PublishPage,
|
||||
})
|
||||
|
||||
|
|
|
|||
33
web/src/features/publish/publish-prefill.test.ts
Normal file
33
web/src/features/publish/publish-prefill.test.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { normalizePublishPrefill } from './publish-prefill'
|
||||
|
||||
describe('normalizePublishPrefill', () => {
|
||||
it('keeps namespace and normalizes visibility for valid route search params', () => {
|
||||
expect(normalizePublishPrefill({
|
||||
namespace: 'team-ai',
|
||||
visibility: 'private',
|
||||
})).toEqual({
|
||||
namespace: 'team-ai',
|
||||
visibility: 'PRIVATE',
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to PUBLIC when visibility is missing or invalid', () => {
|
||||
expect(normalizePublishPrefill({
|
||||
namespace: 'team-ai',
|
||||
visibility: 'internal',
|
||||
})).toEqual({
|
||||
namespace: 'team-ai',
|
||||
visibility: 'PUBLIC',
|
||||
})
|
||||
})
|
||||
|
||||
it('trims namespace input from search params', () => {
|
||||
expect(normalizePublishPrefill({
|
||||
namespace: ' team-ml ',
|
||||
})).toEqual({
|
||||
namespace: 'team-ml',
|
||||
visibility: 'PUBLIC',
|
||||
})
|
||||
})
|
||||
})
|
||||
23
web/src/features/publish/publish-prefill.ts
Normal file
23
web/src/features/publish/publish-prefill.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
const VALID_VISIBILITIES = new Set(['PUBLIC', 'NAMESPACE_ONLY', 'PRIVATE'])
|
||||
|
||||
interface PublishPrefillSearch {
|
||||
namespace?: string
|
||||
visibility?: string
|
||||
}
|
||||
|
||||
export interface PublishPrefillState {
|
||||
namespace: string
|
||||
visibility: string
|
||||
}
|
||||
|
||||
export function normalizePublishPrefill(search: PublishPrefillSearch): PublishPrefillState {
|
||||
const namespace = typeof search.namespace === 'string' ? search.namespace.trim() : ''
|
||||
const normalizedVisibility = typeof search.visibility === 'string'
|
||||
? search.visibility.trim().toUpperCase()
|
||||
: ''
|
||||
|
||||
return {
|
||||
namespace,
|
||||
visibility: VALID_VISIBILITIES.has(normalizedVisibility) ? normalizedVisibility : 'PUBLIC',
|
||||
}
|
||||
}
|
||||
|
|
@ -340,6 +340,7 @@
|
|||
"HIDDEN": "Hidden"
|
||||
},
|
||||
"publishNew": "Publish New Skill",
|
||||
"update": "Update",
|
||||
"archive": "Archive",
|
||||
"unarchive": "Restore",
|
||||
"statusArchived": "Archived",
|
||||
|
|
|
|||
|
|
@ -340,6 +340,7 @@
|
|||
"HIDDEN": "已隐藏"
|
||||
},
|
||||
"publishNew": "发布新技能",
|
||||
"update": "更新",
|
||||
"archive": "归档",
|
||||
"unarchive": "恢复",
|
||||
"statusArchived": "已归档",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createElement, type ReactNode } from 'react'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const navigateMock = vi.fn()
|
||||
const buttonRecords: Array<{ label: string; onClick?: ((event?: { stopPropagation: () => void }) => void) | undefined }> = []
|
||||
const useMySkillsMock = vi.fn()
|
||||
|
||||
vi.mock('@tanstack/react-router', () => ({
|
||||
useNavigate: () => vi.fn(),
|
||||
useNavigate: () => navigateMock,
|
||||
}))
|
||||
|
||||
vi.mock('react-i18next', async () => {
|
||||
|
|
@ -19,15 +25,25 @@ vi.mock('@/features/auth/use-auth', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('@/shared/ui/button', () => ({
|
||||
Button: ({ children }: { children: unknown }) => children,
|
||||
Button: ({
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
children?: ReactNode
|
||||
onClick?: (event?: { stopPropagation: () => void }) => void
|
||||
}) => {
|
||||
const label = Array.isArray(children) ? children.join('') : String(children ?? '')
|
||||
buttonRecords.push({ label, onClick })
|
||||
return createElement('button', null, children)
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/shared/ui/card', () => ({
|
||||
Card: ({ children }: { children: unknown }) => children,
|
||||
Card: ({ children }: { children: ReactNode }) => createElement('div', null, children),
|
||||
}))
|
||||
|
||||
vi.mock('@/shared/components/empty-state', () => ({
|
||||
EmptyState: () => null,
|
||||
EmptyState: () => createElement('div', null, 'empty-state'),
|
||||
}))
|
||||
|
||||
vi.mock('@/shared/components/confirm-dialog', () => ({
|
||||
|
|
@ -35,7 +51,7 @@ vi.mock('@/shared/components/confirm-dialog', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('@/shared/components/dashboard-page-header', () => ({
|
||||
DashboardPageHeader: () => null,
|
||||
DashboardPageHeader: ({ actions }: { actions?: ReactNode }) => createElement('div', null, actions),
|
||||
}))
|
||||
|
||||
vi.mock('@/shared/components/pagination', () => ({
|
||||
|
|
@ -49,16 +65,13 @@ vi.mock('@/shared/hooks/use-skill-queries', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('@/shared/hooks/use-user-queries', () => ({
|
||||
useMySkills: () => ({
|
||||
data: { items: [], total: 0, page: 0, size: 10 },
|
||||
isLoading: false,
|
||||
}),
|
||||
useMySkills: () => useMySkillsMock(),
|
||||
useSubmitPromotion: () => ({ mutateAsync: vi.fn(), isPending: false }),
|
||||
}))
|
||||
|
||||
vi.mock('@/shared/lib/skill-lifecycle', () => ({
|
||||
getHeadlineVersion: () => null,
|
||||
getPublishedVersion: () => null,
|
||||
getHeadlineVersion: () => ({ id: 11, version: '1.0.0', status: 'PUBLISHED' }),
|
||||
getPublishedVersion: () => ({ id: 11, version: '1.0.0', status: 'PUBLISHED' }),
|
||||
getOwnerPreviewVersion: () => null,
|
||||
hasPendingOwnerPreview: () => false,
|
||||
}))
|
||||
|
|
@ -79,7 +92,120 @@ vi.mock('@/api/client', () => ({
|
|||
|
||||
import { MySkillsPage } from './my-skills'
|
||||
|
||||
function findButton(label: string) {
|
||||
const record = buttonRecords.find((item) => item.label === label)
|
||||
if (!record) {
|
||||
throw new Error(`Missing button: ${label}`)
|
||||
}
|
||||
return record
|
||||
}
|
||||
|
||||
describe('MySkillsPage', () => {
|
||||
beforeEach(() => {
|
||||
navigateMock.mockReset()
|
||||
buttonRecords.length = 0
|
||||
useMySkillsMock.mockReturnValue({
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
displayName: 'Team Agent',
|
||||
summary: 'summary',
|
||||
namespace: 'team-ai',
|
||||
slug: 'team-agent',
|
||||
downloadCount: 42,
|
||||
status: 'PUBLISHED',
|
||||
visibility: 'PRIVATE',
|
||||
canSubmitPromotion: false,
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
page: 0,
|
||||
size: 10,
|
||||
},
|
||||
isLoading: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('navigates to publish page with namespace and visibility when update is clicked', () => {
|
||||
renderToStaticMarkup(createElement(MySkillsPage))
|
||||
|
||||
const stopPropagation = vi.fn()
|
||||
findButton('mySkills.update').onClick?.({ stopPropagation })
|
||||
|
||||
expect(stopPropagation).toHaveBeenCalledTimes(1)
|
||||
expect(navigateMock).toHaveBeenCalledWith({
|
||||
to: '/dashboard/publish',
|
||||
search: {
|
||||
namespace: 'team-ai',
|
||||
visibility: 'PRIVATE',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('does not render update action for archived skills', () => {
|
||||
useMySkillsMock.mockReturnValue({
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
id: 2,
|
||||
displayName: 'Archived Agent',
|
||||
summary: 'summary',
|
||||
namespace: 'team-ai',
|
||||
slug: 'archived-agent',
|
||||
downloadCount: 7,
|
||||
status: 'ARCHIVED',
|
||||
visibility: 'PUBLIC',
|
||||
canSubmitPromotion: false,
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
page: 0,
|
||||
size: 10,
|
||||
},
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
renderToStaticMarkup(createElement(MySkillsPage))
|
||||
|
||||
expect(buttonRecords.some((button) => button.label === 'mySkills.update')).toBe(false)
|
||||
})
|
||||
|
||||
it('falls back to public visibility when the skill card data has no visibility field', () => {
|
||||
useMySkillsMock.mockReturnValue({
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
id: 3,
|
||||
displayName: 'Default Visibility Agent',
|
||||
summary: 'summary',
|
||||
namespace: 'team-ai',
|
||||
slug: 'default-visibility-agent',
|
||||
downloadCount: 9,
|
||||
status: 'PUBLISHED',
|
||||
canSubmitPromotion: false,
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
page: 0,
|
||||
size: 10,
|
||||
},
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
renderToStaticMarkup(createElement(MySkillsPage))
|
||||
|
||||
findButton('mySkills.update').onClick?.({ stopPropagation: vi.fn() })
|
||||
|
||||
expect(navigateMock).toHaveBeenCalledWith({
|
||||
to: '/dashboard/publish',
|
||||
search: {
|
||||
namespace: 'team-ai',
|
||||
visibility: 'PUBLIC',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('exports a named component function', () => {
|
||||
expect(typeof MySkillsPage).toBe('function')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -61,6 +61,13 @@ export function MySkillsPage() {
|
|||
})
|
||||
}
|
||||
|
||||
const handleUpdateSkill = (namespace: string, visibility?: string) => {
|
||||
navigate({
|
||||
to: '/dashboard/publish',
|
||||
search: { namespace, visibility: visibility || 'PUBLIC' },
|
||||
})
|
||||
}
|
||||
|
||||
const resolveStatusLabel = (status?: string) => {
|
||||
if (status === 'HIDDEN') {
|
||||
return t('mySkills.statusHidden')
|
||||
|
|
@ -306,6 +313,18 @@ export function MySkillsPage() {
|
|||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 pl-4">
|
||||
{skill.status !== 'ARCHIVED' && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
handleUpdateSkill(skill.namespace, skill.visibility ?? 'PUBLIC')
|
||||
}}
|
||||
>
|
||||
{t('mySkills.update')}
|
||||
</Button>
|
||||
)}
|
||||
{hasPendingPreview && ownerPreviewVersion ? (
|
||||
<Button
|
||||
size="sm"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createElement } from 'react'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const useSearchMock = vi.fn()
|
||||
const selectRecords: Array<{ value?: string }> = []
|
||||
|
||||
vi.mock('@tanstack/react-router', () => ({
|
||||
useNavigate: () => vi.fn(),
|
||||
useSearch: () => useSearchMock(),
|
||||
}))
|
||||
|
||||
vi.mock('react-i18next', async () => {
|
||||
|
|
@ -23,7 +29,10 @@ vi.mock('@/shared/ui/button', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('@/shared/ui/select', () => ({
|
||||
Select: ({ children }: { children: unknown }) => children,
|
||||
Select: ({ children, value }: { children: unknown; value?: string }) => {
|
||||
selectRecords.push({ value })
|
||||
return children
|
||||
},
|
||||
SelectContent: ({ children }: { children: unknown }) => children,
|
||||
SelectItem: ({ children }: { children: unknown }) => children,
|
||||
SelectTrigger: ({ children }: { children: unknown }) => children,
|
||||
|
|
@ -64,6 +73,30 @@ vi.mock('@/api/client', () => ({
|
|||
import { PublishPage } from './publish'
|
||||
|
||||
describe('PublishPage', () => {
|
||||
beforeEach(() => {
|
||||
selectRecords.length = 0
|
||||
useSearchMock.mockReturnValue({
|
||||
namespace: ' team-ai ',
|
||||
visibility: 'private',
|
||||
})
|
||||
})
|
||||
|
||||
it('prefills namespace and visibility from route search params', () => {
|
||||
renderToStaticMarkup(createElement(PublishPage))
|
||||
|
||||
expect(selectRecords[0]?.value).toBe('team-ai')
|
||||
expect(selectRecords[1]?.value).toBe('PRIVATE')
|
||||
})
|
||||
|
||||
it('falls back to public visibility when search params are missing', () => {
|
||||
useSearchMock.mockReturnValue({})
|
||||
|
||||
renderToStaticMarkup(createElement(PublishPage))
|
||||
|
||||
expect(selectRecords[0]?.value).toBe('__select_namespace__')
|
||||
expect(selectRecords[1]?.value).toBe('PUBLIC')
|
||||
})
|
||||
|
||||
it('exports a named component function', () => {
|
||||
expect(typeof PublishPage).toBe('function')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState } from 'react'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate, useSearch } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { UploadZone } from '@/features/publish/upload-zone'
|
||||
import {
|
||||
|
|
@ -9,6 +9,7 @@ import {
|
|||
isPrecheckFailureMessage,
|
||||
isVersionExistsMessage,
|
||||
} from '@/features/publish/publish-error-utils'
|
||||
import { normalizePublishPrefill } from '@/features/publish/publish-prefill'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import {
|
||||
Select,
|
||||
|
|
@ -32,9 +33,11 @@ const EMPTY_NAMESPACE_VALUE = '__select_namespace__'
|
|||
export function PublishPage() {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const search = useSearch({ from: '/dashboard/publish' })
|
||||
const prefill = normalizePublishPrefill(search)
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||
const [namespaceSlug, setNamespaceSlug] = useState<string>('')
|
||||
const [visibility, setVisibility] = useState<string>('PUBLIC')
|
||||
const [namespaceSlug, setNamespaceSlug] = useState<string>(prefill.namespace)
|
||||
const [visibility, setVisibility] = useState<string>(prefill.visibility)
|
||||
const [warningDialogOpen, setWarningDialogOpen] = useState(false)
|
||||
const [precheckWarnings, setPrecheckWarnings] = useState<string[]>([])
|
||||
|
||||
|
|
@ -45,6 +48,11 @@ export function PublishPage() {
|
|||
? t('publish.visibilityOptions.loggedInUsersOnly')
|
||||
: t('publish.visibilityOptions.namespaceOnly')
|
||||
|
||||
useEffect(() => {
|
||||
setNamespaceSlug(prefill.namespace)
|
||||
setVisibility(prefill.visibility)
|
||||
}, [prefill.namespace, prefill.visibility])
|
||||
|
||||
const handleRemoveSelectedFile = () => {
|
||||
setSelectedFile(null)
|
||||
setPrecheckWarnings([])
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue