feat: implement error handling for task preview generation and add error message utility
All checks were successful
Build And Publish Production Image / Build And Publish Production Image (push) Successful in 14s
All checks were successful
Build And Publish Production Image / Build And Publish Production Image (push) Successful in 14s
This commit is contained in:
@@ -328,4 +328,18 @@ INSTRUCTIONS
|
||||
'backend unavailable'
|
||||
)
|
||||
})
|
||||
|
||||
it('should_returnReadable401Message_when_generateTaskPreviewUnauthorized', async () => {
|
||||
mockedApiClient.post.mockRejectedValue({
|
||||
isAxiosError: true,
|
||||
response: {
|
||||
status: 401,
|
||||
data: {},
|
||||
},
|
||||
})
|
||||
|
||||
await expect(generateTaskPreview('task-1', { entity, task: previewTask })).rejects.toThrow(
|
||||
'Your session has expired. Please sign in again and retry.'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -205,7 +205,7 @@ describe('EditTaskPage', () => {
|
||||
|
||||
it('should_showReadablePreviewError_when_generationFails', async () => {
|
||||
vi.mocked(tasksApi.generateTaskPreview).mockRejectedValue(
|
||||
new Error('Unable to generate a test message from the local model. Connection refused')
|
||||
new Error('Your session has expired. Please sign in again and retry.')
|
||||
)
|
||||
|
||||
renderPage()
|
||||
@@ -215,7 +215,7 @@ describe('EditTaskPage', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(/Unable to generate a test message from the local model. Connection refused/i)
|
||||
screen.getByText(/Your session has expired. Please sign in again and retry./i)
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
53
frontend/src/api/errorMessage.ts
Normal file
53
frontend/src/api/errorMessage.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { isAxiosError } from 'axios'
|
||||
|
||||
function getMessageFromResponseData(data: unknown): string | null {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const payload = data as Record<string, unknown>
|
||||
const candidate = payload.message ?? payload.error ?? payload.detail
|
||||
|
||||
if (typeof candidate === 'string' && candidate.trim().length > 0) {
|
||||
return candidate.trim()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function getApiErrorMessage(error: unknown, fallback: string): string {
|
||||
if (isAxiosError(error)) {
|
||||
const status = error.response?.status
|
||||
const responseMessage = getMessageFromResponseData(error.response?.data)
|
||||
|
||||
if (responseMessage) {
|
||||
return responseMessage
|
||||
}
|
||||
|
||||
if (status === 401) {
|
||||
return 'Your session has expired. Please sign in again and retry.'
|
||||
}
|
||||
|
||||
if (status === 403) {
|
||||
return 'You do not have permission to perform this action.'
|
||||
}
|
||||
|
||||
if (status === 404) {
|
||||
return 'The requested resource was not found.'
|
||||
}
|
||||
|
||||
if (status && status >= 500) {
|
||||
return 'The server failed to process the request. Please try again in a moment.'
|
||||
}
|
||||
|
||||
if (!error.response) {
|
||||
return 'Unable to reach the server. Check your network connection and try again.'
|
||||
}
|
||||
}
|
||||
|
||||
if (error instanceof Error && error.message.trim().length > 0) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { VirtualEntityResponse } from './entitiesApi'
|
||||
import apiClient from './apiClient'
|
||||
import { getApiErrorMessage } from './errorMessage'
|
||||
|
||||
export type EmailLookback = 'last_day' | 'last_week' | 'last_month'
|
||||
export type GenerationSource = 'openai' | 'llama'
|
||||
@@ -88,11 +89,17 @@ export async function generateTaskPreview(
|
||||
taskId: string,
|
||||
payload: TaskPreviewRequest
|
||||
): Promise<string> {
|
||||
try {
|
||||
const response = await apiClient.post<GeneratedMessageHistoryItem>(
|
||||
`/v1/tasks/${taskId}/generated-messages/generate`,
|
||||
payload
|
||||
)
|
||||
return response.data.content
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getApiErrorMessage(error, 'Unable to generate a test message right now. Please try again.')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns generated message history for one task. */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||
import { getApiErrorMessage } from '../api/errorMessage'
|
||||
import { getEntity } from '../api/entitiesApi'
|
||||
import {
|
||||
activateTask,
|
||||
@@ -122,6 +123,7 @@ export default function EditTaskPage() {
|
||||
const [taskForm, setTaskForm] = useState<TaskFormState>(DEFAULT_TASK_FORM)
|
||||
const [selectedMessageId, setSelectedMessageId] = useState('')
|
||||
const [previewError, setPreviewError] = useState('')
|
||||
const [actionError, setActionError] = useState('')
|
||||
|
||||
const { data: entity, isLoading: isLoadingEntity } = useQuery({
|
||||
queryKey: ['entity', entityId],
|
||||
@@ -181,34 +183,58 @@ export default function EditTaskPage() {
|
||||
emailLookback: data.emailLookback,
|
||||
generationSource: data.generationSource,
|
||||
}),
|
||||
onMutate: () => {
|
||||
setActionError('')
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await invalidateTaskQueries(queryClient, entityId, taskId)
|
||||
navigate(`/entities/${entityId}`)
|
||||
},
|
||||
onError: (error) => {
|
||||
setActionError(getApiErrorMessage(error, 'Unable to save the task right now. Please try again.'))
|
||||
},
|
||||
})
|
||||
|
||||
const inactivateTaskMutation = useMutation({
|
||||
mutationFn: () => inactivateTask(taskId),
|
||||
onMutate: () => {
|
||||
setActionError('')
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await invalidateTaskQueries(queryClient, entityId, taskId)
|
||||
navigate(`/entities/${entityId}`)
|
||||
},
|
||||
onError: (error) => {
|
||||
setActionError(getApiErrorMessage(error, 'Unable to inactivate the task right now. Please try again.'))
|
||||
},
|
||||
})
|
||||
|
||||
const activateTaskMutation = useMutation({
|
||||
mutationFn: () => activateTask(taskId),
|
||||
onMutate: () => {
|
||||
setActionError('')
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await invalidateTaskQueries(queryClient, entityId, taskId)
|
||||
navigate(`/entities/${entityId}`)
|
||||
},
|
||||
onError: (error) => {
|
||||
setActionError(getApiErrorMessage(error, 'Unable to activate the task right now. Please try again.'))
|
||||
},
|
||||
})
|
||||
|
||||
const deleteTaskMutation = useMutation({
|
||||
mutationFn: () => deleteTask(taskId),
|
||||
onMutate: () => {
|
||||
setActionError('')
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await invalidateTaskQueries(queryClient, entityId, taskId)
|
||||
navigate(`/entities/${entityId}`)
|
||||
},
|
||||
onError: (error) => {
|
||||
setActionError(getApiErrorMessage(error, 'Unable to delete the task right now. Please try again.'))
|
||||
},
|
||||
})
|
||||
|
||||
const previewMutation = useMutation({
|
||||
@@ -222,9 +248,10 @@ export default function EditTaskPage() {
|
||||
},
|
||||
onError: (error) => {
|
||||
setPreviewError(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Unable to generate a test message from the local model.'
|
||||
getApiErrorMessage(
|
||||
error,
|
||||
'Unable to generate a test message right now. Please try again.'
|
||||
)
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -268,6 +295,9 @@ export default function EditTaskPage() {
|
||||
|
||||
const deleteGeneratedMessageMutation = useMutation({
|
||||
mutationFn: (messageId: string) => deleteTaskGeneratedMessage(taskId, messageId),
|
||||
onMutate: () => {
|
||||
setActionError('')
|
||||
},
|
||||
onSuccess: ( _data, messageId) => {
|
||||
queryClient.setQueryData(
|
||||
['task-generated-messages', taskId],
|
||||
@@ -278,6 +308,14 @@ export default function EditTaskPage() {
|
||||
) => currentMessages?.filter((message) => message.id !== messageId) ?? []
|
||||
)
|
||||
},
|
||||
onError: (error) => {
|
||||
setActionError(
|
||||
getApiErrorMessage(
|
||||
error,
|
||||
'Unable to delete the generated message right now. Please try again.'
|
||||
)
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const applyCronParts = (nextCronParts: CronParts) => {
|
||||
@@ -589,6 +627,12 @@ export default function EditTaskPage() {
|
||||
|
||||
</div>
|
||||
|
||||
{actionError && (
|
||||
<p role="alert" className="text-sm text-red-300">
|
||||
{actionError}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-3 pb-8">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user