mirror of
https://github.com/iflytek/skillhub.git
synced 2026-11-15 09:11:18 +00:00
fix(web): report skill deletion errors once
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
parent
0c63067acf
commit
7dbee64360
2 changed files with 98 additions and 17 deletions
|
|
@ -611,23 +611,18 @@ export function SkillDetailPage() {
|
|||
if (!skill || !isDeleteSlugConfirmationValid(skill.slug, deleteSkillInput)) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await deleteSkillMutation.mutateAsync({ namespace, slug, ownerId: skill.ownerId })
|
||||
setSkillDeleted(true)
|
||||
toast.success(
|
||||
t('skillDetail.deleteSkillSuccessTitle'),
|
||||
t('skillDetail.deleteSkillSuccessDescription', { skill: skill.displayName }),
|
||||
)
|
||||
setDeleteSkillInputOpen(false)
|
||||
navigateAfterOverlays(() => {
|
||||
navigate({ to: resolveDeletedSkillReturnTo(search.returnTo) })
|
||||
})
|
||||
queryClient.removeQueries({ queryKey: ['skills', namespace, slug] })
|
||||
queryClient.invalidateQueries({ queryKey: ['skills', 'my'] })
|
||||
} catch (error) {
|
||||
toast.error(t('skillDetail.deleteSkillErrorTitle'), error instanceof Error ? error.message : '')
|
||||
throw error
|
||||
}
|
||||
await deleteSkillMutation.mutateAsync({ namespace, slug, ownerId: skill.ownerId })
|
||||
setSkillDeleted(true)
|
||||
toast.success(
|
||||
t('skillDetail.deleteSkillSuccessTitle'),
|
||||
t('skillDetail.deleteSkillSuccessDescription', { skill: skill.displayName }),
|
||||
)
|
||||
setDeleteSkillInputOpen(false)
|
||||
navigateAfterOverlays(() => {
|
||||
navigate({ to: resolveDeletedSkillReturnTo(search.returnTo) })
|
||||
})
|
||||
queryClient.removeQueries({ queryKey: ['skills', namespace, slug] })
|
||||
queryClient.invalidateQueries({ queryKey: ['skills', 'my'] })
|
||||
}
|
||||
|
||||
const handleDeleteVersion = async () => {
|
||||
|
|
|
|||
86
web/src/shared/hooks/use-delete-skill.test.tsx
Normal file
86
web/src/shared/hooks/use-delete-skill.test.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// @vitest-environment jsdom
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { App } from '@/app/providers'
|
||||
import { skillLifecycleApi } from '@/api/client'
|
||||
import { ApiError } from '@/shared/lib/api-error'
|
||||
import { toast } from '@/shared/lib/toast'
|
||||
import { useDeleteSkill } from './use-skill-queries'
|
||||
|
||||
vi.mock('@/app/router', () => ({ router: {} }))
|
||||
vi.mock('@tanstack/react-router', () => ({ RouterProvider: () => <DeleteHarness /> }))
|
||||
vi.mock('@/shared/components/toaster', () => ({ Toaster: () => null }))
|
||||
vi.mock('@/shared/lib/toast', () => ({ toast: { error: vi.fn(), success: vi.fn() } }))
|
||||
|
||||
const blockedError = new ApiError('revoke global publication first', 409, 'revoke global publication first')
|
||||
|
||||
// Exercise the real application MutationCache. Like skill-detail, the caller
|
||||
// reports success only; the global handler owns errors and authentication redirects.
|
||||
function DeleteHarness() {
|
||||
const deletion = useDeleteSkill()
|
||||
const unrelated = useMutation({ mutationFn: async () => { throw blockedError } })
|
||||
const deleteSkill = async () => {
|
||||
try {
|
||||
await deletion.mutateAsync({ namespace: 'team', slug: 'promoted', ownerId: 'owner' })
|
||||
toast.success('deleted')
|
||||
} catch {
|
||||
// The click boundary consumes rejection; MutationCache already handled it.
|
||||
}
|
||||
}
|
||||
return <>
|
||||
<button onClick={() => { void deleteSkill() }} disabled={deletion.isPending}>Delete</button>
|
||||
<button onClick={() => { void unrelated.mutateAsync().catch(() => undefined) }}>Other request</button>
|
||||
</>
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.restoreAllMocks()
|
||||
vi.clearAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe('skill deletion feedback ownership', () => {
|
||||
it('shows one actionable error per failed deletion, including a new retry', async () => {
|
||||
const request = vi.spyOn(skillLifecycleApi, 'deleteSkill').mockRejectedValue(blockedError)
|
||||
render(<App />)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Delete' }))
|
||||
await waitFor(() => expect(toast.error).toHaveBeenCalledTimes(1))
|
||||
expect(toast.error).toHaveBeenLastCalledWith(blockedError.serverMessage)
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: 'Delete' }).hasAttribute('disabled')).toBe(false))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Delete' }))
|
||||
await waitFor(() => expect(request).toHaveBeenCalledTimes(2))
|
||||
await waitFor(() => expect(toast.error).toHaveBeenCalledTimes(2))
|
||||
expect(toast.success).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reports a successful deletion once without an error notification', async () => {
|
||||
vi.spyOn(skillLifecycleApi, 'deleteSkill').mockResolvedValue({ skillId: 123 })
|
||||
render(<App />)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Delete' }))
|
||||
await waitFor(() => expect(toast.success).toHaveBeenCalledTimes(1))
|
||||
expect(toast.error).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
|
||||
it('keeps the existing login redirect and one notification when the session expires', async () => {
|
||||
vi.spyOn(skillLifecycleApi, 'deleteSkill').mockRejectedValue(new ApiError('session expired', 401))
|
||||
render(<App />)
|
||||
const location = { href: '' }
|
||||
// Override only location on a delegated window; React still sees the real DOM.
|
||||
const testWindow = Object.create(window)
|
||||
Object.defineProperty(testWindow, 'location', { value: location })
|
||||
vi.stubGlobal('window', testWindow)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Delete' }))
|
||||
await waitFor(() => expect(location.href).toBe('/login'))
|
||||
expect(toast.error).toHaveBeenCalledTimes(1)
|
||||
expect(toast.success).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('retains global error feedback for mutations without local ownership', async () => {
|
||||
render(<App />)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Other request' }))
|
||||
await waitFor(() => expect(toast.error).toHaveBeenCalledExactlyOnceWith(blockedError.serverMessage))
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Reference in a new issue