fix: prevent duplicate channel action toasts (#5015)
* fix: prevent duplicate channel action toasts * fix: localize api error fallbacks
This commit is contained in:
parent
a64f26d1d2
commit
ad224ecf5b
128
web/default/src/features/channels/api.ts
vendored
128
web/default/src/features/channels/api.ts
vendored
@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
import type { AxiosRequestConfig } from 'axios'
|
import { api, type ApiRequestConfig } from '@/lib/api'
|
||||||
import { api } from '@/lib/api'
|
|
||||||
import { getGroups as getUserGroups } from '@/features/users/api'
|
import { getGroups as getUserGroups } from '@/features/users/api'
|
||||||
import type {
|
import type {
|
||||||
AddChannelRequest,
|
AddChannelRequest,
|
||||||
@ -39,11 +38,13 @@ import type {
|
|||||||
TagOperationParams,
|
TagOperationParams,
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
// Extended API config types
|
const channelActionConfig = (
|
||||||
interface ExtendedApiConfig extends AxiosRequestConfig {
|
config: ApiRequestConfig = {}
|
||||||
skipBusinessError?: boolean
|
): ApiRequestConfig => ({
|
||||||
disableDuplicate?: boolean
|
...config,
|
||||||
}
|
skipBusinessError: true,
|
||||||
|
skipErrorHandler: true,
|
||||||
|
})
|
||||||
|
|
||||||
export type CodexOAuthStartResponse = {
|
export type CodexOAuthStartResponse = {
|
||||||
success: boolean
|
success: boolean
|
||||||
@ -125,7 +126,7 @@ export async function getChannel(id: number): Promise<GetChannelResponse> {
|
|||||||
export async function createChannel(
|
export async function createChannel(
|
||||||
data: AddChannelRequest
|
data: AddChannelRequest
|
||||||
): Promise<{ success: boolean; message?: string }> {
|
): Promise<{ success: boolean; message?: string }> {
|
||||||
const res = await api.post('/api/channel', data)
|
const res = await api.post('/api/channel', data, channelActionConfig())
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,7 +137,11 @@ export async function updateChannel(
|
|||||||
id: number,
|
id: number,
|
||||||
data: Partial<Channel>
|
data: Partial<Channel>
|
||||||
): Promise<{ success: boolean; message?: string; data?: Channel }> {
|
): Promise<{ success: boolean; message?: string; data?: Channel }> {
|
||||||
const res = await api.put('/api/channel/', { id, ...data })
|
const res = await api.put(
|
||||||
|
'/api/channel/',
|
||||||
|
{ id, ...data },
|
||||||
|
channelActionConfig()
|
||||||
|
)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,7 +151,7 @@ export async function updateChannel(
|
|||||||
export async function deleteChannel(
|
export async function deleteChannel(
|
||||||
id: number
|
id: number
|
||||||
): Promise<{ success: boolean; message?: string }> {
|
): Promise<{ success: boolean; message?: string }> {
|
||||||
const res = await api.delete(`/api/channel/${id}`)
|
const res = await api.delete(`/api/channel/${id}`, channelActionConfig())
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,7 +161,7 @@ export async function deleteChannel(
|
|||||||
export async function batchDeleteChannels(
|
export async function batchDeleteChannels(
|
||||||
data: BatchDeleteParams
|
data: BatchDeleteParams
|
||||||
): Promise<{ success: boolean; message?: string; data?: number }> {
|
): Promise<{ success: boolean; message?: string; data?: number }> {
|
||||||
const res = await api.post('/api/channel/batch', data)
|
const res = await api.post('/api/channel/batch', data, channelActionConfig())
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,7 +171,11 @@ export async function batchDeleteChannels(
|
|||||||
export async function batchSetChannelTag(
|
export async function batchSetChannelTag(
|
||||||
data: BatchSetTagParams
|
data: BatchSetTagParams
|
||||||
): Promise<{ success: boolean; message?: string; data?: number }> {
|
): Promise<{ success: boolean; message?: string; data?: number }> {
|
||||||
const res = await api.post('/api/channel/batch/tag', data)
|
const res = await api.post(
|
||||||
|
'/api/channel/batch/tag',
|
||||||
|
data,
|
||||||
|
channelActionConfig()
|
||||||
|
)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,7 +190,10 @@ export async function testChannel(
|
|||||||
id: number,
|
id: number,
|
||||||
params?: { model?: string; endpoint_type?: string; stream?: boolean }
|
params?: { model?: string; endpoint_type?: string; stream?: boolean }
|
||||||
): Promise<ChannelTestResponse> {
|
): Promise<ChannelTestResponse> {
|
||||||
const res = await api.get(`/api/channel/test/${id}`, { params })
|
const res = await api.get(
|
||||||
|
`/api/channel/test/${id}`,
|
||||||
|
channelActionConfig({ params })
|
||||||
|
)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,7 +203,10 @@ export async function testChannel(
|
|||||||
export async function updateChannelBalance(
|
export async function updateChannelBalance(
|
||||||
id: number
|
id: number
|
||||||
): Promise<ChannelBalanceResponse> {
|
): Promise<ChannelBalanceResponse> {
|
||||||
const res = await api.get(`/api/channel/update_balance/${id}`)
|
const res = await api.get(
|
||||||
|
`/api/channel/update_balance/${id}`,
|
||||||
|
channelActionConfig()
|
||||||
|
)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,7 +216,10 @@ export async function updateChannelBalance(
|
|||||||
export async function fetchUpstreamModels(
|
export async function fetchUpstreamModels(
|
||||||
id: number
|
id: number
|
||||||
): Promise<FetchModelsResponse> {
|
): Promise<FetchModelsResponse> {
|
||||||
const res = await api.get(`/api/channel/fetch_models/${id}`)
|
const res = await api.get(
|
||||||
|
`/api/channel/fetch_models/${id}`,
|
||||||
|
channelActionConfig()
|
||||||
|
)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,7 +230,11 @@ export async function copyChannel(
|
|||||||
id: number,
|
id: number,
|
||||||
params: CopyChannelParams = {}
|
params: CopyChannelParams = {}
|
||||||
): Promise<CopyChannelResponse> {
|
): Promise<CopyChannelResponse> {
|
||||||
const res = await api.post(`/api/channel/copy/${id}`, null, { params })
|
const res = await api.post(
|
||||||
|
`/api/channel/copy/${id}`,
|
||||||
|
null,
|
||||||
|
channelActionConfig({ params })
|
||||||
|
)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,7 +246,11 @@ export async function fixChannelAbilities(): Promise<{
|
|||||||
message?: string
|
message?: string
|
||||||
data?: { success: number; fails: number }
|
data?: { success: number; fails: number }
|
||||||
}> {
|
}> {
|
||||||
const res = await api.post('/api/channel/fix')
|
const res = await api.post(
|
||||||
|
'/api/channel/fix',
|
||||||
|
undefined,
|
||||||
|
channelActionConfig()
|
||||||
|
)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,7 +262,7 @@ export async function deleteDisabledChannels(): Promise<{
|
|||||||
message?: string
|
message?: string
|
||||||
data?: number
|
data?: number
|
||||||
}> {
|
}> {
|
||||||
const res = await api.delete('/api/channel/disabled')
|
const res = await api.delete('/api/channel/disabled', channelActionConfig())
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,7 +274,11 @@ export async function getChannelKey(
|
|||||||
code?: string
|
code?: string
|
||||||
): Promise<{ success: boolean; message?: string; data?: { key: string } }> {
|
): Promise<{ success: boolean; message?: string; data?: { key: string } }> {
|
||||||
const payload = code ? { code } : undefined
|
const payload = code ? { code } : undefined
|
||||||
const res = await api.post(`/api/channel/${id}/key`, payload)
|
const res = await api.post(
|
||||||
|
`/api/channel/${id}/key`,
|
||||||
|
payload,
|
||||||
|
channelActionConfig()
|
||||||
|
)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,19 +287,21 @@ export async function getChannelKey(
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export async function startCodexOAuth(): Promise<CodexOAuthStartResponse> {
|
export async function startCodexOAuth(): Promise<CodexOAuthStartResponse> {
|
||||||
const config: ExtendedApiConfig = { skipBusinessError: true }
|
const res = await api.post(
|
||||||
const res = await api.post('/api/channel/codex/oauth/start', {}, config)
|
'/api/channel/codex/oauth/start',
|
||||||
|
{},
|
||||||
|
channelActionConfig()
|
||||||
|
)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function completeCodexOAuth(
|
export async function completeCodexOAuth(
|
||||||
input: string
|
input: string
|
||||||
): Promise<CodexOAuthCompleteResponse> {
|
): Promise<CodexOAuthCompleteResponse> {
|
||||||
const config: ExtendedApiConfig = { skipBusinessError: true }
|
|
||||||
const res = await api.post(
|
const res = await api.post(
|
||||||
'/api/channel/codex/oauth/complete',
|
'/api/channel/codex/oauth/complete',
|
||||||
{ input },
|
{ input },
|
||||||
config
|
channelActionConfig()
|
||||||
)
|
)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
@ -277,11 +309,10 @@ export async function completeCodexOAuth(
|
|||||||
export async function refreshCodexCredential(
|
export async function refreshCodexCredential(
|
||||||
channelId: number
|
channelId: number
|
||||||
): Promise<CodexCredentialRefreshResponse> {
|
): Promise<CodexCredentialRefreshResponse> {
|
||||||
const config: ExtendedApiConfig = { skipBusinessError: true }
|
|
||||||
const res = await api.post(
|
const res = await api.post(
|
||||||
`/api/channel/${channelId}/codex/refresh`,
|
`/api/channel/${channelId}/codex/refresh`,
|
||||||
{},
|
{},
|
||||||
config
|
channelActionConfig()
|
||||||
)
|
)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
@ -289,11 +320,10 @@ export async function refreshCodexCredential(
|
|||||||
export async function getCodexUsage(
|
export async function getCodexUsage(
|
||||||
channelId: number
|
channelId: number
|
||||||
): Promise<CodexUsageResponse> {
|
): Promise<CodexUsageResponse> {
|
||||||
const config: ExtendedApiConfig = {
|
const res = await api.get(
|
||||||
skipBusinessError: true,
|
`/api/channel/${channelId}/codex/usage`,
|
||||||
disableDuplicate: true,
|
channelActionConfig({ disableDuplicate: true })
|
||||||
}
|
)
|
||||||
const res = await api.get(`/api/channel/${channelId}/codex/usage`, config)
|
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,7 +337,11 @@ export async function getCodexUsage(
|
|||||||
export async function manageMultiKeys(
|
export async function manageMultiKeys(
|
||||||
params: MultiKeyManageParams
|
params: MultiKeyManageParams
|
||||||
): Promise<MultiKeyStatusResponse | { success: boolean; message?: string }> {
|
): Promise<MultiKeyStatusResponse | { success: boolean; message?: string }> {
|
||||||
const res = await api.post('/api/channel/multi_key/manage', params)
|
const res = await api.post(
|
||||||
|
'/api/channel/multi_key/manage',
|
||||||
|
params,
|
||||||
|
channelActionConfig()
|
||||||
|
)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -417,7 +451,11 @@ export async function deleteDisabledMultiKeys(
|
|||||||
export async function enableTagChannels(
|
export async function enableTagChannels(
|
||||||
tag: string
|
tag: string
|
||||||
): Promise<{ success: boolean; message?: string }> {
|
): Promise<{ success: boolean; message?: string }> {
|
||||||
const res = await api.post('/api/channel/tag/enabled', { tag })
|
const res = await api.post(
|
||||||
|
'/api/channel/tag/enabled',
|
||||||
|
{ tag },
|
||||||
|
channelActionConfig()
|
||||||
|
)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -427,7 +465,11 @@ export async function enableTagChannels(
|
|||||||
export async function disableTagChannels(
|
export async function disableTagChannels(
|
||||||
tag: string
|
tag: string
|
||||||
): Promise<{ success: boolean; message?: string }> {
|
): Promise<{ success: boolean; message?: string }> {
|
||||||
const res = await api.post('/api/channel/tag/disabled', { tag })
|
const res = await api.post(
|
||||||
|
'/api/channel/tag/disabled',
|
||||||
|
{ tag },
|
||||||
|
channelActionConfig()
|
||||||
|
)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -437,7 +479,7 @@ export async function disableTagChannels(
|
|||||||
export async function editTagChannels(
|
export async function editTagChannels(
|
||||||
params: TagOperationParams
|
params: TagOperationParams
|
||||||
): Promise<{ success: boolean; message?: string }> {
|
): Promise<{ success: boolean; message?: string }> {
|
||||||
const res = await api.put('/api/channel/tag', params)
|
const res = await api.put('/api/channel/tag', params, channelActionConfig())
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -463,7 +505,11 @@ export async function fetchModels(data: {
|
|||||||
type: number
|
type: number
|
||||||
key: string
|
key: string
|
||||||
}): Promise<FetchModelsResponse> {
|
}): Promise<FetchModelsResponse> {
|
||||||
const res = await api.post('/api/channel/fetch_models', data)
|
const res = await api.post(
|
||||||
|
'/api/channel/fetch_models',
|
||||||
|
data,
|
||||||
|
channelActionConfig()
|
||||||
|
)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -474,7 +520,10 @@ export async function deleteOllamaModel(params: {
|
|||||||
channel_id: number
|
channel_id: number
|
||||||
model_name: string
|
model_name: string
|
||||||
}): Promise<{ success: boolean; message?: string }> {
|
}): Promise<{ success: boolean; message?: string }> {
|
||||||
const res = await api.delete('/api/channel/ollama/delete', { data: params })
|
const res = await api.delete(
|
||||||
|
'/api/channel/ollama/delete',
|
||||||
|
channelActionConfig({ data: params })
|
||||||
|
)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -485,7 +534,7 @@ export async function testAllChannels(): Promise<{
|
|||||||
success: boolean
|
success: boolean
|
||||||
message?: string
|
message?: string
|
||||||
}> {
|
}> {
|
||||||
const res = await api.get('/api/channel/test')
|
const res = await api.get('/api/channel/test', channelActionConfig())
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -496,7 +545,10 @@ export async function updateAllChannelsBalance(): Promise<{
|
|||||||
success: boolean
|
success: boolean
|
||||||
message?: string
|
message?: string
|
||||||
}> {
|
}> {
|
||||||
const res = await api.get('/api/channel/update_balance')
|
const res = await api.get(
|
||||||
|
'/api/channel/update_balance',
|
||||||
|
channelActionConfig()
|
||||||
|
)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -135,6 +135,8 @@ export function MultiKeyManageDialog({
|
|||||||
setEnabledCount(response.data.enabled_count || 0)
|
setEnabledCount(response.data.enabled_count || 0)
|
||||||
setManualDisabledCount(response.data.manual_disabled_count || 0)
|
setManualDisabledCount(response.data.manual_disabled_count || 0)
|
||||||
setAutoDisabledCount(response.data.auto_disabled_count || 0)
|
setAutoDisabledCount(response.data.auto_disabled_count || 0)
|
||||||
|
} else {
|
||||||
|
toast.error(response.message || t('Failed to load key status'))
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
toast.error(
|
toast.error(
|
||||||
|
|||||||
@ -211,14 +211,22 @@ export function OllamaModelsDialog({
|
|||||||
? Array.from(new Set(selected))
|
? Array.from(new Set(selected))
|
||||||
: Array.from(new Set([...existingModels, ...selected]))
|
: Array.from(new Set([...existingModels, ...selected]))
|
||||||
|
|
||||||
const res = await updateChannel(currentRow.id, { models: next.join(',') })
|
try {
|
||||||
if (res.success) {
|
const res = await updateChannel(currentRow.id, { models: next.join(',') })
|
||||||
toast.success(
|
if (res.success) {
|
||||||
mode === 'replace'
|
toast.success(
|
||||||
? t('Models updated successfully')
|
mode === 'replace'
|
||||||
: t('Models appended successfully')
|
? t('Models updated successfully')
|
||||||
|
: t('Models appended successfully')
|
||||||
|
)
|
||||||
|
queryClient.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
|
||||||
|
} else {
|
||||||
|
toast.error(res.message || t('Failed to update models'))
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
toast.error(
|
||||||
|
err instanceof Error ? err.message : t('Failed to update models')
|
||||||
)
|
)
|
||||||
queryClient.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,9 +19,14 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
import { useRef, useState, useCallback } from 'react'
|
import { useRef, useState, useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { api } from '@/lib/api'
|
import { api, type ApiRequestConfig } from '@/lib/api'
|
||||||
import { normalizeModelList } from '../lib/upstream-update-utils'
|
import { normalizeModelList } from '../lib/upstream-update-utils'
|
||||||
|
|
||||||
|
const upstreamUpdateRequestConfig = {
|
||||||
|
skipBusinessError: true,
|
||||||
|
skipErrorHandler: true,
|
||||||
|
} satisfies ApiRequestConfig
|
||||||
|
|
||||||
function getManualIgnoredModelCount(settings: unknown): number {
|
function getManualIgnoredModelCount(settings: unknown): number {
|
||||||
let parsed: Record<string, unknown> | null = null
|
let parsed: Record<string, unknown> | null = null
|
||||||
if (settings && typeof settings === 'object')
|
if (settings && typeof settings === 'object')
|
||||||
@ -117,7 +122,7 @@ export function useChannelUpstreamUpdates(refresh: () => Promise<void>) {
|
|||||||
ignore_models: ignoreModels,
|
ignore_models: ignoreModels,
|
||||||
remove_models: normalizeModelList(selectedRemove),
|
remove_models: normalizeModelList(selectedRemove),
|
||||||
},
|
},
|
||||||
{ skipErrorHandler: true } as Record<string, unknown>
|
upstreamUpdateRequestConfig
|
||||||
)
|
)
|
||||||
const { success, message, data } = res.data || {}
|
const { success, message, data } = res.data || {}
|
||||||
if (!success) {
|
if (!success) {
|
||||||
@ -162,7 +167,7 @@ export function useChannelUpstreamUpdates(refresh: () => Promise<void>) {
|
|||||||
const res = await api.post(
|
const res = await api.post(
|
||||||
'/api/channel/upstream_updates/apply_all',
|
'/api/channel/upstream_updates/apply_all',
|
||||||
{},
|
{},
|
||||||
{ skipErrorHandler: true } as Record<string, unknown>
|
upstreamUpdateRequestConfig
|
||||||
)
|
)
|
||||||
const { success, message, data } = res.data || {}
|
const { success, message, data } = res.data || {}
|
||||||
if (!success) {
|
if (!success) {
|
||||||
@ -206,7 +211,7 @@ export function useChannelUpstreamUpdates(refresh: () => Promise<void>) {
|
|||||||
const res = await api.post(
|
const res = await api.post(
|
||||||
'/api/channel/upstream_updates/detect',
|
'/api/channel/upstream_updates/detect',
|
||||||
{ id: ch.id },
|
{ id: ch.id },
|
||||||
{ skipErrorHandler: true } as Record<string, unknown>
|
upstreamUpdateRequestConfig
|
||||||
)
|
)
|
||||||
const { success, message, data } = res.data || {}
|
const { success, message, data } = res.data || {}
|
||||||
if (!success) {
|
if (!success) {
|
||||||
@ -244,7 +249,7 @@ export function useChannelUpstreamUpdates(refresh: () => Promise<void>) {
|
|||||||
const res = await api.post(
|
const res = await api.post(
|
||||||
'/api/channel/upstream_updates/detect_all',
|
'/api/channel/upstream_updates/detect_all',
|
||||||
{},
|
{},
|
||||||
{ skipErrorHandler: true } as Record<string, unknown>
|
upstreamUpdateRequestConfig
|
||||||
)
|
)
|
||||||
const { success, message, data } = res.data || {}
|
const { success, message, data } = res.data || {}
|
||||||
if (!success) {
|
if (!success) {
|
||||||
|
|||||||
@ -70,6 +70,8 @@ export async function handleEnableChannel(
|
|||||||
toast.success(i18next.t(SUCCESS_MESSAGES.ENABLED))
|
toast.success(i18next.t(SUCCESS_MESSAGES.ENABLED))
|
||||||
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
|
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
|
||||||
onSuccess?.()
|
onSuccess?.()
|
||||||
|
} else {
|
||||||
|
toast.error(response.message || i18next.t(ERROR_MESSAGES.UPDATE_FAILED))
|
||||||
}
|
}
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
toast.error(i18next.t(ERROR_MESSAGES.UPDATE_FAILED))
|
toast.error(i18next.t(ERROR_MESSAGES.UPDATE_FAILED))
|
||||||
@ -92,6 +94,8 @@ export async function handleDisableChannel(
|
|||||||
toast.success(i18next.t(SUCCESS_MESSAGES.DISABLED))
|
toast.success(i18next.t(SUCCESS_MESSAGES.DISABLED))
|
||||||
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
|
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
|
||||||
onSuccess?.()
|
onSuccess?.()
|
||||||
|
} else {
|
||||||
|
toast.error(response.message || i18next.t(ERROR_MESSAGES.UPDATE_FAILED))
|
||||||
}
|
}
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
toast.error(i18next.t(ERROR_MESSAGES.UPDATE_FAILED))
|
toast.error(i18next.t(ERROR_MESSAGES.UPDATE_FAILED))
|
||||||
@ -128,6 +132,8 @@ export async function handleDeleteChannel(
|
|||||||
toast.success(i18next.t(SUCCESS_MESSAGES.DELETED))
|
toast.success(i18next.t(SUCCESS_MESSAGES.DELETED))
|
||||||
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
|
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
|
||||||
onSuccess?.()
|
onSuccess?.()
|
||||||
|
} else {
|
||||||
|
toast.error(response.message || i18next.t(ERROR_MESSAGES.DELETE_FAILED))
|
||||||
}
|
}
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
toast.error(i18next.t(ERROR_MESSAGES.DELETE_FAILED))
|
toast.error(i18next.t(ERROR_MESSAGES.DELETE_FAILED))
|
||||||
@ -338,6 +344,8 @@ export async function handleBatchDelete(
|
|||||||
)
|
)
|
||||||
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
|
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
|
||||||
onSuccess?.(response.data || ids.length)
|
onSuccess?.(response.data || ids.length)
|
||||||
|
} else {
|
||||||
|
toast.error(response.message || i18next.t(ERROR_MESSAGES.DELETE_FAILED))
|
||||||
}
|
}
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
toast.error(i18next.t(ERROR_MESSAGES.DELETE_FAILED))
|
toast.error(i18next.t(ERROR_MESSAGES.DELETE_FAILED))
|
||||||
@ -364,8 +372,10 @@ export async function handleBatchEnable(
|
|||||||
)
|
)
|
||||||
const results = await Promise.allSettled(promises)
|
const results = await Promise.allSettled(promises)
|
||||||
|
|
||||||
const successCount = results.filter((r) => r.status === 'fulfilled').length
|
const successCount = results.filter(
|
||||||
const failCount = results.filter((r) => r.status === 'rejected').length
|
(r) => r.status === 'fulfilled' && r.value.success
|
||||||
|
).length
|
||||||
|
const failCount = results.length - successCount
|
||||||
|
|
||||||
if (successCount > 0) {
|
if (successCount > 0) {
|
||||||
toast.success(
|
toast.success(
|
||||||
@ -405,8 +415,10 @@ export async function handleBatchDisable(
|
|||||||
)
|
)
|
||||||
const results = await Promise.allSettled(promises)
|
const results = await Promise.allSettled(promises)
|
||||||
|
|
||||||
const successCount = results.filter((r) => r.status === 'fulfilled').length
|
const successCount = results.filter(
|
||||||
const failCount = results.filter((r) => r.status === 'rejected').length
|
(r) => r.status === 'fulfilled' && r.value.success
|
||||||
|
).length
|
||||||
|
const failCount = results.length - successCount
|
||||||
|
|
||||||
if (successCount > 0) {
|
if (successCount > 0) {
|
||||||
toast.success(
|
toast.success(
|
||||||
@ -448,6 +460,8 @@ export async function handleBatchSetTag(
|
|||||||
toast.success(i18next.t(SUCCESS_MESSAGES.TAG_SET))
|
toast.success(i18next.t(SUCCESS_MESSAGES.TAG_SET))
|
||||||
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
|
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
|
||||||
onSuccess?.()
|
onSuccess?.()
|
||||||
|
} else {
|
||||||
|
toast.error(response.message || i18next.t('Failed to set tag'))
|
||||||
}
|
}
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
toast.error(i18next.t('Failed to set tag'))
|
toast.error(i18next.t('Failed to set tag'))
|
||||||
@ -474,6 +488,10 @@ export async function handleEnableTagChannels(
|
|||||||
)
|
)
|
||||||
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
|
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
|
||||||
onSuccess?.()
|
onSuccess?.()
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
response.message || i18next.t('Failed to enable tag channels')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
toast.error(i18next.t('Failed to enable tag channels'))
|
toast.error(i18next.t('Failed to enable tag channels'))
|
||||||
@ -496,6 +514,10 @@ export async function handleDisableTagChannels(
|
|||||||
)
|
)
|
||||||
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
|
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
|
||||||
onSuccess?.()
|
onSuccess?.()
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
response.message || i18next.t('Failed to disable tag channels')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
toast.error(i18next.t('Failed to disable tag channels'))
|
toast.error(i18next.t('Failed to disable tag channels'))
|
||||||
@ -523,6 +545,10 @@ export async function handleDeleteAllDisabled(
|
|||||||
)
|
)
|
||||||
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
|
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
|
||||||
onSuccess?.(response.data || 0)
|
onSuccess?.(response.data || 0)
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
response.message || i18next.t('Failed to delete disabled channels')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
toast.error(i18next.t('Failed to delete disabled channels'))
|
toast.error(i18next.t('Failed to delete disabled channels'))
|
||||||
@ -547,6 +573,8 @@ export async function handleFixAbilities(
|
|||||||
)
|
)
|
||||||
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
|
queryClient?.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
|
||||||
onSuccess?.(response.data)
|
onSuccess?.(response.data)
|
||||||
|
} else {
|
||||||
|
toast.error(response.message || i18next.t('Failed to fix abilities'))
|
||||||
}
|
}
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
toast.error(i18next.t('Failed to fix abilities'))
|
toast.error(i18next.t('Failed to fix abilities'))
|
||||||
|
|||||||
60
web/default/src/lib/api.ts
vendored
60
web/default/src/lib/api.ts
vendored
@ -16,11 +16,21 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
import axios from 'axios'
|
import axios, { type AxiosRequestConfig } from 'axios'
|
||||||
import i18next from 'i18next'
|
import { t } from 'i18next'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { useAuthStore } from '@/stores/auth-store'
|
import { useAuthStore } from '@/stores/auth-store'
|
||||||
|
|
||||||
|
declare module 'axios' {
|
||||||
|
export interface AxiosRequestConfig {
|
||||||
|
skipBusinessError?: boolean
|
||||||
|
skipErrorHandler?: boolean
|
||||||
|
disableDuplicate?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiRequestConfig = AxiosRequestConfig
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Axios Instance Configuration
|
// Axios Instance Configuration
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -46,14 +56,11 @@ export const api = axios.create({
|
|||||||
const inFlightGet = new Map<string, Promise<unknown>>()
|
const inFlightGet = new Map<string, Promise<unknown>>()
|
||||||
const originalGet = api.get.bind(api)
|
const originalGet = api.get.bind(api)
|
||||||
|
|
||||||
api.get = ((url: string, config = {}) => {
|
api.get = ((url: string, config: ApiRequestConfig = {}) => {
|
||||||
const disableDuplicate = (config as unknown as Record<string, unknown>)
|
const disableDuplicate = config.disableDuplicate
|
||||||
?.disableDuplicate
|
|
||||||
if (disableDuplicate) return originalGet(url, config)
|
if (disableDuplicate) return originalGet(url, config)
|
||||||
|
|
||||||
const params = (config as unknown as Record<string, unknown>)?.params
|
const params = config.params ? JSON.stringify(config.params) : '{}'
|
||||||
? JSON.stringify((config as unknown as Record<string, unknown>).params)
|
|
||||||
: '{}'
|
|
||||||
const key = `${url}?${params}`
|
const key = `${url}?${params}`
|
||||||
|
|
||||||
// Return existing in-flight request if available
|
// Return existing in-flight request if available
|
||||||
@ -72,8 +79,7 @@ api.get = ((url: string, config = {}) => {
|
|||||||
// Handle business logic errors and HTTP errors globally
|
// Handle business logic errors and HTTP errors globally
|
||||||
api.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
const skipBusiness = (response.config as unknown as Record<string, unknown>)
|
const skipBusiness = response.config.skipBusinessError
|
||||||
?.skipBusinessError
|
|
||||||
|
|
||||||
// Unified business response format: { success, message, data }
|
// Unified business response format: { success, message, data }
|
||||||
if (
|
if (
|
||||||
@ -84,7 +90,7 @@ api.interceptors.response.use(
|
|||||||
) {
|
) {
|
||||||
if (!response.data.success) {
|
if (!response.data.success) {
|
||||||
// Show error toast for business failures
|
// Show error toast for business failures
|
||||||
const msg = response.data.message || 'Request failed'
|
const msg = response.data.message || t('Request failed')
|
||||||
toast.error(msg)
|
toast.error(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -92,23 +98,23 @@ api.interceptors.response.use(
|
|||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
const skip = error?.config?.skipErrorHandler
|
const skip = error?.config?.skipErrorHandler
|
||||||
if (!skip) {
|
const status = error?.response?.status
|
||||||
const status = error?.response?.status
|
|
||||||
|
|
||||||
if (status === 401) {
|
if (status === 401) {
|
||||||
// Unauthorized: clear auth state and show toast
|
try {
|
||||||
toast.error(i18next.t('Session expired!'))
|
useAuthStore.getState().auth.reset()
|
||||||
try {
|
} catch {
|
||||||
useAuthStore.getState().auth.reset()
|
/* empty */
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Other errors: show error message from response or default
|
|
||||||
const msg =
|
|
||||||
error?.response?.data?.message || error?.message || 'Request error'
|
|
||||||
toast.error(msg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!skip) {
|
||||||
|
toast.error(t('Session expired!'))
|
||||||
|
}
|
||||||
|
} else if (!skip) {
|
||||||
|
// Other errors: show error message from response or default
|
||||||
|
const msg =
|
||||||
|
error?.response?.data?.message || error?.message || t('Request failed')
|
||||||
|
toast.error(msg)
|
||||||
}
|
}
|
||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
@ -175,7 +181,7 @@ export async function getSelf() {
|
|||||||
const res = await api.get('/api/user/self', {
|
const res = await api.get('/api/user/self', {
|
||||||
// Avoid global 401 toast during guards/preloads
|
// Avoid global 401 toast during guards/preloads
|
||||||
skipErrorHandler: true,
|
skipErrorHandler: true,
|
||||||
} as Record<string, unknown>)
|
})
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user