feat(skill): add lifecycle management UI

This commit is contained in:
yun-zhi-ztl 2026-03-15 17:02:03 +08:00
parent 411e592362
commit 04eb468a19
12 changed files with 427 additions and 9 deletions

View file

@ -8,6 +8,7 @@ public record SkillSummaryResponse(
String slug,
String displayName,
String summary,
String status,
Long downloadCount,
Integer starCount,
BigDecimal ratingAvg,

View file

@ -124,6 +124,7 @@ public class MySkillAppService {
skill.getSlug(),
skill.getDisplayName(),
skill.getSummary(),
skill.getStatus().name(),
skill.getDownloadCount(),
skill.getStarCount(),
skill.getRatingAvg(),

View file

@ -147,6 +147,7 @@ public class SkillSearchAppService {
skill.getSlug(),
skill.getDisplayName(),
skill.getSummary(),
skill.getStatus().name(),
skill.getDownloadCount(),
skill.getStarCount(),
skill.getRatingAvg(),

View file

@ -56,6 +56,7 @@ class ClawHubCompatControllerTest {
"my-skill",
"My Skill",
"test summary",
"ACTIVE",
10L,
5,
BigDecimal.valueOf(4.5),

View file

@ -75,6 +75,7 @@ class ClawHubRegistryControllerTest {
"global-skill",
"Global Skill",
"global summary",
"ACTIVE",
10L,
5,
BigDecimal.ZERO,
@ -89,6 +90,7 @@ class ClawHubRegistryControllerTest {
"team-skill",
"Team Skill",
"team summary",
"ACTIVE",
20L,
8,
BigDecimal.ONE,

View file

@ -412,6 +412,35 @@ export const accountApi = {
},
}
export const skillLifecycleApi = {
async archiveSkill(namespace: string, slug: string, reason?: string): Promise<void> {
const cleanNamespace = namespace.startsWith('@') ? namespace.slice(1) : namespace
await fetchJson<void>(`${WEB_API_PREFIX}/skills/${cleanNamespace}/${slug}/archive`, {
method: 'POST',
headers: await ensureCsrfHeaders({
'Content-Type': 'application/json',
}),
body: JSON.stringify(reason?.trim() ? { reason: reason.trim() } : {}),
})
},
async unarchiveSkill(namespace: string, slug: string): Promise<void> {
const cleanNamespace = namespace.startsWith('@') ? namespace.slice(1) : namespace
await fetchJson<void>(`${WEB_API_PREFIX}/skills/${cleanNamespace}/${slug}/unarchive`, {
method: 'POST',
headers: await ensureCsrfHeaders(),
})
},
async deleteVersion(namespace: string, slug: string, version: string): Promise<void> {
const cleanNamespace = namespace.startsWith('@') ? namespace.slice(1) : namespace
await fetchJson<void>(`${WEB_API_PREFIX}/skills/${cleanNamespace}/${slug}/versions/${encodeURIComponent(version)}`, {
method: 'DELETE',
headers: await ensureCsrfHeaders(),
})
},
}
export const tokenApi = {
async getTokens(params?: { page?: number, size?: number }): Promise<{ items: ApiToken[], total: number, page: number, size: number }> {
const page = await unwrap<{ items: ApiToken[], total: number, page: number, size: number }>(client.GET('/api/v1/tokens', {

View file

@ -107,6 +107,7 @@ export interface SkillSummary {
slug: string
displayName: string
summary?: string
status?: string
downloadCount: number
starCount: number
ratingAvg?: number

View file

@ -190,8 +190,21 @@
"title": "My Skills",
"subtitle": "Manage your published skills",
"publishNew": "Publish New Skill",
"archive": "Archive",
"unarchive": "Restore",
"statusArchived": "Archived",
"statusPendingReview": "Pending Review",
"statusPublished": "Published",
"archiveConfirmTitle": "Archive skill",
"archiveConfirmDescription": "After archiving, regular users will no longer be able to view or download \"{{skill}}\". Continue?",
"unarchiveConfirmTitle": "Restore skill",
"unarchiveConfirmDescription": "\"{{skill}}\" will become visible again and can publish new versions after being restored.",
"archiveSuccessTitle": "Skill archived",
"archiveSuccessDescription": "\"{{skill}}\" has been archived.",
"archiveErrorTitle": "Failed to archive skill",
"unarchiveSuccessTitle": "Skill restored",
"unarchiveSuccessDescription": "\"{{skill}}\" has been restored and can publish new versions again.",
"unarchiveErrorTitle": "Failed to restore skill",
"emptyTitle": "No skills yet",
"emptyDescription": "Start publishing your first skill",
"publishSkill": "Publish Skill"
@ -372,10 +385,34 @@
"loginToRate": "Login to star and rate",
"install": "Install",
"download": "Download",
"lifecycle": "Lifecycle",
"lifecycleHint": "You can archive this skill. Archived skills are hidden from regular users and cannot be downloaded.",
"archivedPublishHint": "This skill is archived. Restore it before publishing a new version.",
"archivedInstallHint": "This skill is archived and not available for public download.",
"statusActive": "Active",
"statusArchived": "Archived",
"statusHidden": "Hidden",
"governance": "Governance",
"processing": "Processing...",
"archiveSkill": "Archive Skill",
"hideSkill": "Hide Skill",
"unhideSkill": "Unhide Skill",
"archiveConfirmTitle": "Archive skill",
"archiveConfirmDescription": "After archiving, regular users will no longer be able to view or download \"{{skill}}\". Continue?",
"unarchiveConfirmTitle": "Restore skill",
"unarchiveConfirmDescription": "\"{{skill}}\" will become visible again and can publish new versions after being restored.",
"archiveSuccessTitle": "Skill archived",
"archiveSuccessDescription": "\"{{skill}}\" has been archived.",
"archiveErrorTitle": "Failed to archive skill",
"unarchiveSuccessTitle": "Skill restored",
"unarchiveSuccessDescription": "\"{{skill}}\" has been restored.",
"unarchiveErrorTitle": "Failed to restore skill",
"deleteVersion": "Delete Version",
"deleteVersionConfirmTitle": "Delete version",
"deleteVersionConfirmDescription": "Version {{version}} cannot be recovered after deletion. Continue?",
"deleteVersionSuccessTitle": "Version deleted",
"deleteVersionSuccessDescription": "Version {{version}} has been deleted.",
"deleteVersionErrorTitle": "Failed to delete version",
"yankVersion": "Yank Current Version",
"reportSkill": "Report Skill",
"reportDialogTitle": "Report skill",

View file

@ -190,8 +190,21 @@
"title": "我的技能",
"subtitle": "管理你发布的技能",
"publishNew": "发布新技能",
"archive": "归档",
"unarchive": "恢复",
"statusArchived": "已归档",
"statusPendingReview": "审核中",
"statusPublished": "已发布",
"archiveConfirmTitle": "确认归档技能",
"archiveConfirmDescription": "归档后普通用户将无法看到或下载“{{skill}}”,确定继续吗?",
"unarchiveConfirmTitle": "确认恢复技能",
"unarchiveConfirmDescription": "恢复后“{{skill}}”会重新对外可见,并允许继续发布新版本。",
"archiveSuccessTitle": "技能已归档",
"archiveSuccessDescription": "“{{skill}}”已归档。",
"archiveErrorTitle": "归档技能失败",
"unarchiveSuccessTitle": "技能已恢复",
"unarchiveSuccessDescription": "“{{skill}}”已恢复,可继续发布新版本。",
"unarchiveErrorTitle": "恢复技能失败",
"emptyTitle": "还没有技能",
"emptyDescription": "开始发布你的第一个技能吧",
"publishSkill": "发布技能"
@ -372,10 +385,34 @@
"loginToRate": "登录后可以收藏和评分",
"install": "安装",
"download": "下载",
"lifecycle": "生命周期管理",
"lifecycleHint": "你可以归档这个技能,归档后普通用户将无法查看或下载。",
"archivedPublishHint": "该技能已归档,请先恢复后再继续发布新版本。",
"archivedInstallHint": "该技能已归档,普通用户不可下载。",
"statusActive": "正常",
"statusArchived": "已归档",
"statusHidden": "已隐藏",
"governance": "治理操作",
"processing": "处理中...",
"archiveSkill": "归档技能",
"hideSkill": "隐藏技能",
"unhideSkill": "恢复技能",
"archiveConfirmTitle": "确认归档技能",
"archiveConfirmDescription": "归档后普通用户将无法看到或下载“{{skill}}”,确定继续吗?",
"unarchiveConfirmTitle": "确认恢复技能",
"unarchiveConfirmDescription": "恢复后“{{skill}}”会重新对外可见,并允许继续发布新版本。",
"archiveSuccessTitle": "技能已归档",
"archiveSuccessDescription": "“{{skill}}”已归档。",
"archiveErrorTitle": "归档技能失败",
"unarchiveSuccessTitle": "技能已恢复",
"unarchiveSuccessDescription": "“{{skill}}”已恢复。",
"unarchiveErrorTitle": "恢复技能失败",
"deleteVersion": "删除版本",
"deleteVersionConfirmTitle": "确认删除版本",
"deleteVersionConfirmDescription": "版本 {{version}} 删除后无法恢复,确定继续吗?",
"deleteVersionSuccessTitle": "版本已删除",
"deleteVersionSuccessDescription": "版本 {{version}} 已删除。",
"deleteVersionErrorTitle": "删除版本失败",
"yankVersion": "撤回当前版本",
"reportSkill": "举报技能",
"reportDialogTitle": "举报技能",

View file

@ -1,22 +1,32 @@
import { useState } from 'react'
import { useNavigate } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import { Button } from '@/shared/ui/button'
import { Card } from '@/shared/ui/card'
import { EmptyState } from '@/shared/components/empty-state'
import { ConfirmDialog } from '@/shared/components/confirm-dialog'
import { DashboardPageHeader } from '@/shared/components/dashboard-page-header'
import { useMySkills } from '@/shared/hooks/use-skill-queries'
import { useArchiveSkill, useMySkills, useUnarchiveSkill } from '@/shared/hooks/use-skill-queries'
import { formatCompactCount } from '@/shared/lib/number-format'
import { toast } from '@/shared/lib/toast'
export function MySkillsPage() {
const navigate = useNavigate()
const { t } = useTranslation()
const [archiveTarget, setArchiveTarget] = useState<{ namespace: string; slug: string; name: string } | null>(null)
const [unarchiveTarget, setUnarchiveTarget] = useState<{ namespace: string; slug: string; name: string } | null>(null)
const { data: skills, isLoading } = useMySkills()
const archiveMutation = useArchiveSkill()
const unarchiveMutation = useUnarchiveSkill()
const handleSkillClick = (namespace: string, slug: string) => {
navigate({ to: `/space/${namespace}/${slug}` })
}
const resolveStatusLabel = (status?: string) => {
if (status === 'ARCHIVED') {
return t('mySkills.statusArchived')
}
if (status === 'PENDING_REVIEW') {
return t('mySkills.statusPendingReview')
}
@ -27,6 +37,9 @@ export function MySkillsPage() {
}
const resolveStatusClassName = (status?: string) => {
if (status === 'ARCHIVED') {
return 'bg-slate-500/10 text-slate-500 border-slate-500/20'
}
if (status === 'PENDING_REVIEW') {
return 'bg-amber-500/10 text-amber-500 border-amber-500/20'
}
@ -36,6 +49,46 @@ export function MySkillsPage() {
return 'bg-secondary/60 text-muted-foreground border-border/40'
}
const handleArchiveSkill = async () => {
if (!archiveTarget) {
return
}
try {
await archiveMutation.mutateAsync({
namespace: archiveTarget.namespace,
slug: archiveTarget.slug,
})
toast.success(
t('mySkills.archiveSuccessTitle'),
t('mySkills.archiveSuccessDescription', { skill: archiveTarget.name }),
)
setArchiveTarget(null)
} catch (error) {
toast.error(t('mySkills.archiveErrorTitle'), error instanceof Error ? error.message : '')
throw error
}
}
const handleUnarchiveSkill = async () => {
if (!unarchiveTarget) {
return
}
try {
await unarchiveMutation.mutateAsync({
namespace: unarchiveTarget.namespace,
slug: unarchiveTarget.slug,
})
toast.success(
t('mySkills.unarchiveSuccessTitle'),
t('mySkills.unarchiveSuccessDescription', { skill: unarchiveTarget.name }),
)
setUnarchiveTarget(null)
} catch (error) {
toast.error(t('mySkills.unarchiveErrorTitle'), error instanceof Error ? error.message : '')
throw error
}
}
if (isLoading) {
return (
<div className="space-y-4 animate-fade-up">
@ -79,6 +132,11 @@ export function MySkillsPage() {
{skill.latestVersion && (
<span className="font-mono text-xs">v{skill.latestVersion}</span>
)}
{skill.status ? (
<span className={`rounded-full border px-2.5 py-0.5 text-xs ${resolveStatusClassName(skill.status)}`}>
{resolveStatusLabel(skill.status)}
</span>
) : null}
{skill.latestVersionStatus ? (
<span className={`rounded-full border px-2.5 py-0.5 text-xs ${resolveStatusClassName(skill.latestVersionStatus)}`}>
{resolveStatusLabel(skill.latestVersionStatus)}
@ -92,9 +150,42 @@ export function MySkillsPage() {
</span>
</div>
</div>
<svg className="w-5 h-5 text-muted-foreground group-hover:text-primary transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
<div className="flex items-center gap-2 pl-4">
{skill.status === 'ARCHIVED' ? (
<Button
size="sm"
variant="outline"
onClick={(event) => {
event.stopPropagation()
setUnarchiveTarget({
namespace: skill.namespace,
slug: skill.slug,
name: skill.displayName,
})
}}
>
{t('mySkills.unarchive')}
</Button>
) : (
<Button
size="sm"
variant="outline"
onClick={(event) => {
event.stopPropagation()
setArchiveTarget({
namespace: skill.namespace,
slug: skill.slug,
name: skill.displayName,
})
}}
>
{t('mySkills.archive')}
</Button>
)}
<svg className="w-5 h-5 text-muted-foreground group-hover:text-primary transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</Card>
))}
@ -110,6 +201,32 @@ export function MySkillsPage() {
}
/>
)}
<ConfirmDialog
open={!!archiveTarget}
onOpenChange={(open) => {
if (!open) {
setArchiveTarget(null)
}
}}
title={t('mySkills.archiveConfirmTitle')}
description={archiveTarget ? t('mySkills.archiveConfirmDescription', { skill: archiveTarget.name }) : ''}
confirmText={t('mySkills.archive')}
onConfirm={handleArchiveSkill}
/>
<ConfirmDialog
open={!!unarchiveTarget}
onOpenChange={(open) => {
if (!open) {
setUnarchiveTarget(null)
}
}}
title={t('mySkills.unarchiveConfirmTitle')}
description={unarchiveTarget ? t('mySkills.unarchiveConfirmDescription', { skill: unarchiveTarget.name }) : ''}
confirmText={t('mySkills.unarchive')}
onConfirm={handleUnarchiveSkill}
/>
</div>
)
}

View file

@ -17,6 +17,7 @@ import { NamespaceBadge } from '@/shared/components/namespace-badge'
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/shared/ui/tabs'
import { Button } from '@/shared/ui/button'
import { Card } from '@/shared/ui/card'
import { ConfirmDialog } from '@/shared/components/confirm-dialog'
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/shared/ui/dialog'
import { Input } from '@/shared/ui/input'
import { Textarea } from '@/shared/ui/textarea'
@ -26,6 +27,9 @@ import {
useSkillVersions,
useSkillFiles,
useSkillReadme,
useArchiveSkill,
useDeleteSkillVersion,
useUnarchiveSkill,
} from '@/shared/hooks/use-skill-queries'
export function SkillDetailPage() {
@ -36,6 +40,9 @@ export function SkillDetailPage() {
const [reportDialogOpen, setReportDialogOpen] = useState(false)
const [reportReason, setReportReason] = useState('')
const [reportDetails, setReportDetails] = useState('')
const [archiveConfirmOpen, setArchiveConfirmOpen] = useState(false)
const [unarchiveConfirmOpen, setUnarchiveConfirmOpen] = useState(false)
const [deleteVersionTarget, setDeleteVersionTarget] = useState<string | null>(null)
const { namespace, slug } = useParams({ from: '/space/$namespace/$slug' })
const { user, hasRole } = useAuth()
@ -66,6 +73,9 @@ export function SkillDetailPage() {
mutationFn: () => adminApi.yankVersion(latestVersion!.id),
onSuccess: refreshSkill,
})
const archiveMutation = useArchiveSkill()
const unarchiveMutation = useUnarchiveSkill()
const deleteVersionMutation = useDeleteSkillVersion()
const reportMutation = useSubmitSkillReport(namespace, slug)
const handleDownload = () => {
@ -126,6 +136,66 @@ export function SkillDetailPage() {
navigate({ to: '/search', search: { q: '', sort: 'relevance', page: 0, starredOnly: false } })
}
const resolveSkillStatusLabel = (status?: string) => {
if (status === 'ARCHIVED') {
return t('skillDetail.statusArchived')
}
if (status === 'ACTIVE') {
return t('skillDetail.statusActive')
}
if (status === 'HIDDEN') {
return t('skillDetail.statusHidden')
}
return status ?? ''
}
const canDeleteVersion = (status?: string) => status === 'DRAFT' || status === 'REJECTED'
const handleArchive = async () => {
try {
await archiveMutation.mutateAsync({ namespace, slug })
toast.success(
t('skillDetail.archiveSuccessTitle'),
t('skillDetail.archiveSuccessDescription', { skill: skill?.displayName ?? slug }),
)
setArchiveConfirmOpen(false)
} catch (error) {
toast.error(t('skillDetail.archiveErrorTitle'), error instanceof Error ? error.message : '')
throw error
}
}
const handleUnarchive = async () => {
try {
await unarchiveMutation.mutateAsync({ namespace, slug })
toast.success(
t('skillDetail.unarchiveSuccessTitle'),
t('skillDetail.unarchiveSuccessDescription', { skill: skill?.displayName ?? slug }),
)
setUnarchiveConfirmOpen(false)
} catch (error) {
toast.error(t('skillDetail.unarchiveErrorTitle'), error instanceof Error ? error.message : '')
throw error
}
}
const handleDeleteVersion = async () => {
if (!deleteVersionTarget) {
return
}
try {
await deleteVersionMutation.mutateAsync({ namespace, slug, version: deleteVersionTarget })
toast.success(
t('skillDetail.deleteVersionSuccessTitle'),
t('skillDetail.deleteVersionSuccessDescription', { version: deleteVersionTarget }),
)
setDeleteVersionTarget(null)
} catch (error) {
toast.error(t('skillDetail.deleteVersionErrorTitle'), error instanceof Error ? error.message : '')
throw error
}
}
if (isLoadingSkill) {
return (
<div className="space-y-6 animate-fade-up">
@ -182,6 +252,11 @@ export function SkillDetailPage() {
</Button>
<div className="flex items-center gap-3 mb-1">
<NamespaceBadge type="GLOBAL" name={namespace} />
{skill.status && (
<span className="rounded-full border border-border/60 bg-secondary/40 px-2.5 py-0.5 text-xs text-muted-foreground">
{resolveSkillStatusLabel(skill.status)}
</span>
)}
</div>
<h1 className="text-4xl font-bold font-heading text-foreground">{skill.displayName}</h1>
{skill.summary && (
@ -229,10 +304,26 @@ export function SkillDetailPage() {
<span className="px-2.5 py-0.5 rounded-full bg-primary/10 text-primary text-sm font-mono">
v{version.version}
</span>
{version.status && (
<span className="rounded-full border border-border/60 bg-secondary/40 px-2.5 py-0.5 text-xs text-muted-foreground">
{version.status}
</span>
)}
</span>
<span className="text-sm text-muted-foreground">
{formatLocalDateTime(version.publishedAt, i18n.language)}
</span>
<div className="flex items-center gap-3">
<span className="text-sm text-muted-foreground">
{formatLocalDateTime(version.publishedAt, i18n.language)}
</span>
{canDeleteVersion(version.status) && (
<Button
size="sm"
variant="outline"
onClick={() => setDeleteVersionTarget(version.version)}
>
{t('skillDetail.deleteVersion')}
</Button>
)}
</div>
</div>
{version.changelog && (
<p className="text-sm text-muted-foreground leading-relaxed">{version.changelog}</p>
@ -303,6 +394,9 @@ export function SkillDetailPage() {
{skill.latestVersion && (
<Card className="p-5 space-y-4">
<div className="text-sm font-semibold font-heading text-foreground">{t('skillDetail.install')}</div>
{skill.status === 'ARCHIVED' && (
<p className="text-sm text-muted-foreground">{t('skillDetail.archivedInstallHint')}</p>
)}
<InstallCommand
namespace={namespace}
slug={slug}
@ -316,7 +410,7 @@ export function SkillDetailPage() {
variant="outline"
size="lg"
onClick={handleDownload}
disabled={!latestVersion}
disabled={!latestVersion || skill.status === 'ARCHIVED'}
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" />
@ -324,6 +418,26 @@ export function SkillDetailPage() {
{t('skillDetail.download')}
</Button>
{user && (
<Card className="p-5 space-y-3">
<div className="text-sm font-semibold font-heading text-foreground">{t('skillDetail.lifecycle')}</div>
<p className="text-sm text-muted-foreground">
{skill.status === 'ARCHIVED'
? t('skillDetail.archivedPublishHint')
: t('skillDetail.lifecycleHint')}
</p>
{skill.status === 'ARCHIVED' ? (
<Button variant="outline" onClick={() => setUnarchiveConfirmOpen(true)} disabled={unarchiveMutation.isPending}>
{unarchiveMutation.isPending ? t('skillDetail.processing') : t('skillDetail.unarchiveSkill')}
</Button>
) : (
<Button variant="outline" onClick={() => setArchiveConfirmOpen(true)} disabled={archiveMutation.isPending}>
{archiveMutation.isPending ? t('skillDetail.processing') : t('skillDetail.archiveSkill')}
</Button>
)}
</Card>
)}
{governanceVisible && (
<Card className="p-5 space-y-3">
<div className="text-sm font-semibold font-heading text-foreground">{t('skillDetail.governance')}</div>
@ -377,6 +491,38 @@ export function SkillDetailPage() {
</DialogFooter>
</DialogContent>
</Dialog>
<ConfirmDialog
open={archiveConfirmOpen}
onOpenChange={setArchiveConfirmOpen}
title={t('skillDetail.archiveConfirmTitle')}
description={t('skillDetail.archiveConfirmDescription', { skill: skill.displayName })}
confirmText={t('skillDetail.archiveSkill')}
onConfirm={handleArchive}
/>
<ConfirmDialog
open={unarchiveConfirmOpen}
onOpenChange={setUnarchiveConfirmOpen}
title={t('skillDetail.unarchiveConfirmTitle')}
description={t('skillDetail.unarchiveConfirmDescription', { skill: skill.displayName })}
confirmText={t('skillDetail.unarchiveSkill')}
onConfirm={handleUnarchive}
/>
<ConfirmDialog
open={!!deleteVersionTarget}
onOpenChange={(open) => {
if (!open) {
setDeleteVersionTarget(null)
}
}}
title={t('skillDetail.deleteVersionConfirmTitle')}
description={deleteVersionTarget ? t('skillDetail.deleteVersionConfirmDescription', { version: deleteVersionTarget }) : ''}
confirmText={t('skillDetail.deleteVersion')}
variant="destructive"
onConfirm={handleDeleteVersion}
/>
</div>
)
}

View file

@ -1,6 +1,6 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import type { SkillSummary, SkillDetail, SkillVersion, SkillFile, SearchParams, PagedResponse, PublishResult, Namespace, NamespaceMember } from '@/api/types'
import { fetchJson, fetchText, getCsrfHeaders, meApi, WEB_API_PREFIX } from '@/api/client'
import { fetchJson, fetchText, getCsrfHeaders, meApi, skillLifecycleApi, WEB_API_PREFIX } from '@/api/client'
const PUBLISH_REQUEST_TIMEOUT_MS = 60_000
@ -170,3 +170,48 @@ export function usePublishSkill() {
},
})
}
export function useArchiveSkill() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ namespace, slug, reason }: { namespace: string; slug: string; reason?: string }) =>
skillLifecycleApi.archiveSkill(namespace, slug, reason),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ['skills', 'my'] })
queryClient.invalidateQueries({ queryKey: ['skills', variables.namespace, variables.slug] })
queryClient.invalidateQueries({ queryKey: ['skills', variables.namespace, variables.slug, 'versions'] })
queryClient.invalidateQueries({ queryKey: ['skills'] })
},
})
}
export function useUnarchiveSkill() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ namespace, slug }: { namespace: string; slug: string }) =>
skillLifecycleApi.unarchiveSkill(namespace, slug),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ['skills', 'my'] })
queryClient.invalidateQueries({ queryKey: ['skills', variables.namespace, variables.slug] })
queryClient.invalidateQueries({ queryKey: ['skills', variables.namespace, variables.slug, 'versions'] })
queryClient.invalidateQueries({ queryKey: ['skills'] })
},
})
}
export function useDeleteSkillVersion() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ namespace, slug, version }: { namespace: string; slug: string; version: string }) =>
skillLifecycleApi.deleteVersion(namespace, slug, version),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ['skills', 'my'] })
queryClient.invalidateQueries({ queryKey: ['skills', variables.namespace, variables.slug] })
queryClient.invalidateQueries({ queryKey: ['skills', variables.namespace, variables.slug, 'versions'] })
queryClient.invalidateQueries({ queryKey: ['skills'] })
},
})
}