mirror of
https://github.com/nowcommunity/nowdatabase.git
synced 2026-10-12 19:13:52 +03:00
Handle reference delete conflict in UI
This commit is contained in:
@@ -16,6 +16,29 @@ import { useNotify } from '@/hooks/notification'
|
||||
import { useEffect } from 'react'
|
||||
import { createReferenceTitle } from './referenceFormatting'
|
||||
import { useReturnNavigation } from '@/hooks/useReturnNavigation'
|
||||
import type { FetchBaseQueryError } from '@reduxjs/toolkit/query'
|
||||
import type { SerializedError } from '@reduxjs/toolkit'
|
||||
|
||||
const REFERENCE_DELETE_CONFLICT_MESSAGE = 'The Reference with associated updates cannot be deleted.'
|
||||
const GENERIC_DELETE_MESSAGE = 'Could not delete item. Error happened.'
|
||||
|
||||
const resolveDeleteErrorMessage = (
|
||||
error: FetchBaseQueryError | SerializedError | undefined
|
||||
): string => {
|
||||
if (error && 'status' in error) {
|
||||
const errorData = (error.data ?? {}) as { message?: string }
|
||||
|
||||
if (typeof errorData.message === 'string' && errorData.message.trim().length > 0) {
|
||||
return errorData.message
|
||||
}
|
||||
|
||||
if (error.status === 409) {
|
||||
return REFERENCE_DELETE_CONFLICT_MESSAGE
|
||||
}
|
||||
}
|
||||
|
||||
return GENERIC_DELETE_MESSAGE
|
||||
}
|
||||
|
||||
export const ReferenceDetails = () => {
|
||||
const { id } = useParams()
|
||||
@@ -25,7 +48,7 @@ export const ReferenceDetails = () => {
|
||||
}
|
||||
const { isFetching, isError, data } = useGetReferenceDetailsQuery(id!, { skip: isNew })
|
||||
const [editReferenceRequest, { isLoading: mutationLoading }] = useEditReferenceMutation()
|
||||
const [deleteMutation, { isSuccess: deleteSuccess, isError: deleteError }] = useDeleteReferenceMutation()
|
||||
const [deleteMutation, { isSuccess: deleteSuccess, error: deleteError }] = useDeleteReferenceMutation()
|
||||
const { notify } = useNotify()
|
||||
const navigate = useNavigate()
|
||||
const { fallbackTarget } = useReturnNavigation({ fallback: '/reference' })
|
||||
@@ -34,10 +57,8 @@ export const ReferenceDetails = () => {
|
||||
if (deleteSuccess) {
|
||||
notify('Deleted item successfully.')
|
||||
navigate(fallbackTarget)
|
||||
} else if (deleteError) {
|
||||
notify('Could not delete item. Error happened.', 'error')
|
||||
}
|
||||
}, [deleteSuccess, deleteError, notify, navigate, fallbackTarget])
|
||||
}, [deleteSuccess, notify, navigate, fallbackTarget])
|
||||
|
||||
if (isError) return <div>Error loading data</div>
|
||||
if (isFetching || (!data && !isNew) || mutationLoading) return <CircularProgress />
|
||||
@@ -57,7 +78,12 @@ export const ReferenceDetails = () => {
|
||||
}
|
||||
|
||||
const deleteFunction = async () => {
|
||||
await deleteMutation(parseInt(id!)).unwrap()
|
||||
try {
|
||||
await deleteMutation(parseInt(id!)).unwrap()
|
||||
} catch (error) {
|
||||
const message = resolveDeleteErrorMessage(error ?? deleteError)
|
||||
notify(message, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const tabs: TabType[] = [
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { beforeEach, describe, expect, it, jest } from '@jest/globals'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom'
|
||||
|
||||
import { Notification, NotificationContextProvider } from '@/components/Notification'
|
||||
import { ReferenceDetails } from '@/components/Reference/ReferenceDetails'
|
||||
import { useDeleteReferenceMutation, useEditReferenceMutation, useGetReferenceDetailsQuery } from '@/redux/referenceReducer'
|
||||
import type { ReferenceDetailsType } from '@/shared/types'
|
||||
|
||||
jest.mock('@/components/DetailView/DetailView', () => ({
|
||||
DetailView: ({ deleteFunction }: { deleteFunction?: () => Promise<void> }) => (
|
||||
<button onClick={() => deleteFunction && deleteFunction()}>Delete</button>
|
||||
),
|
||||
}))
|
||||
|
||||
jest.mock('lodash-es', () => ({
|
||||
cloneDeep: (value: unknown) => value,
|
||||
}))
|
||||
|
||||
jest.mock('@/util/config', () => ({
|
||||
BACKEND_URL: '',
|
||||
ENABLE_WRITE: true,
|
||||
ENV: 'test',
|
||||
}))
|
||||
|
||||
jest.mock('@/redux/referenceReducer', () => ({
|
||||
useGetReferenceDetailsQuery: jest.fn(),
|
||||
useEditReferenceMutation: jest.fn(),
|
||||
useDeleteReferenceMutation: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('@/components/Reference/Tabs/ReferenceTab', () => ({ ReferenceTab: () => <div>Reference Tab</div> }))
|
||||
jest.mock('@/components/Reference/Tabs/LocalityTab', () => ({ LocalityTab: () => <div>Locality Tab</div> }))
|
||||
jest.mock('@/components/Reference/Tabs/SpeciesTab', () => ({ SpeciesTab: () => <div>Species Tab</div> }))
|
||||
jest.mock('@/components/Reference/referenceFormatting', () => ({ createReferenceTitle: () => 'Reference title' }))
|
||||
jest.mock('@/hooks/useReturnNavigation', () => ({ useReturnNavigation: () => ({ fallbackTarget: '/reference' }) }))
|
||||
jest.mock('@/components/TimeBound/TimeBoundTable', () => ({ TimeBoundTable: () => <div>TimeBound Table</div> }))
|
||||
jest.mock('@/components/Page', () => ({
|
||||
usePageContext: () => ({
|
||||
editRights: { delete: true, edit: true },
|
||||
previousTableUrls: [],
|
||||
setPreviousTableUrls: jest.fn(),
|
||||
viewName: 'reference',
|
||||
idList: [],
|
||||
setIdList: jest.fn(),
|
||||
idFieldName: 'rid',
|
||||
createTitle: jest.fn(),
|
||||
createSubtitle: jest.fn(),
|
||||
sqlLimit: 25,
|
||||
sqlOffset: 0,
|
||||
sqlColumnFilters: [],
|
||||
sqlOrderBy: [],
|
||||
setSqlLimit: jest.fn(),
|
||||
setSqlOffset: jest.fn(),
|
||||
setSqlColumnFilters: jest.fn(),
|
||||
setSqlOrderBy: jest.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
const conflictError = {
|
||||
status: 409,
|
||||
data: { message: 'The Reference with associated updates cannot be deleted.' },
|
||||
}
|
||||
|
||||
describe('ReferenceDetails deletion', () => {
|
||||
const originalConfirm = window.confirm
|
||||
const mockDeleteTrigger = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
;(useGetReferenceDetailsQuery as jest.Mock).mockReturnValue({
|
||||
isFetching: false,
|
||||
isError: false,
|
||||
data: {
|
||||
rid: 1,
|
||||
ref_authors: [],
|
||||
ref_journal: { journal_id: 1, journal_title: '', short_title: '', alt_title: '', ISSN: '' },
|
||||
ref_ref_type: { ref_type: '' },
|
||||
exact_date: null,
|
||||
} as unknown as ReferenceDetailsType,
|
||||
})
|
||||
;(useEditReferenceMutation as jest.Mock).mockReturnValue([
|
||||
jest.fn(() => ({ unwrap: jest.fn() })),
|
||||
{ isLoading: false },
|
||||
])
|
||||
mockDeleteTrigger.mockReturnValue({ unwrap: () => Promise.reject(conflictError) })
|
||||
;(useDeleteReferenceMutation as jest.Mock).mockReturnValue([
|
||||
mockDeleteTrigger,
|
||||
{ isSuccess: false, error: conflictError },
|
||||
])
|
||||
window.confirm = jest.fn(() => true)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
window.confirm = originalConfirm
|
||||
})
|
||||
|
||||
it('shows a conflict message when deletion fails due to linked updates', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<NotificationContextProvider>
|
||||
<Notification />
|
||||
<MemoryRouter initialEntries={[{ pathname: '/reference/1' }]}>
|
||||
<Routes>
|
||||
<Route path="/reference/:id" element={<ReferenceDetails />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</NotificationContextProvider>
|
||||
)
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: /delete/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('The Reference with associated updates cannot be deleted.')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user