From 13d27a5929fd688a3a38abc587f3ce15683c7f86 Mon Sep 17 00:00:00 2001 From: Fringg Date: Mon, 23 Mar 2026 15:36:22 +0300 Subject: [PATCH] fix: tag color bug, FormData interceptor, falsy id check - Fix tag using category_color instead of its own color when editing - Add FormData auto-detection interceptor in client.ts (root cause of 422) - Remove redundant Content-Type overrides from all 5 upload endpoints - Fix falsy id check (id: 0 treated as null) with strict comparison - Await invalidateQueries to prevent stale dropdown data --- src/api/adminBroadcasts.ts | 6 +----- src/api/adminPinnedMessages.ts | 6 +----- src/api/branding.ts | 6 +----- src/api/client.ts | 5 +++++ src/api/news.ts | 5 +---- src/api/tickets.ts | 6 +----- src/pages/AdminNewsCreate.tsx | 15 ++++++++------- 7 files changed, 18 insertions(+), 31 deletions(-) diff --git a/src/api/adminBroadcasts.ts b/src/api/adminBroadcasts.ts index 2f0d0dd..4e6fb0a 100644 --- a/src/api/adminBroadcasts.ts +++ b/src/api/adminBroadcasts.ts @@ -242,11 +242,7 @@ export const adminBroadcastsApi = { formData.append('file', file); formData.append('media_type', mediaType); - const response = await apiClient.post('/cabinet/media/upload', formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }); + const response = await apiClient.post('/cabinet/media/upload', formData); return response.data; }, }; diff --git a/src/api/adminPinnedMessages.ts b/src/api/adminPinnedMessages.ts index edbfcec..3eff552 100644 --- a/src/api/adminPinnedMessages.ts +++ b/src/api/adminPinnedMessages.ts @@ -163,11 +163,7 @@ export const adminPinnedMessagesApi = { formData.append('file', file); formData.append('media_type', mediaType); - const response = await apiClient.post('/cabinet/media/upload', formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }); + const response = await apiClient.post('/cabinet/media/upload', formData); return response.data; }, }; diff --git a/src/api/branding.ts b/src/api/branding.ts index 094a0d2..ec3b9c8 100644 --- a/src/api/branding.ts +++ b/src/api/branding.ts @@ -153,11 +153,7 @@ export const brandingApi = { uploadLogo: async (file: File): Promise => { const formData = new FormData(); formData.append('file', file); - const response = await apiClient.post('/cabinet/branding/logo', formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }); + const response = await apiClient.post('/cabinet/branding/logo', formData); // Invalidate cached blob so it gets re-fetched if (_logoBlobUrl) { URL.revokeObjectURL(_logoBlobUrl); diff --git a/src/api/client.ts b/src/api/client.ts index 57f9116..5b0fb78 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -82,6 +82,11 @@ function isAuthEndpoint(url: string | undefined): boolean { } apiClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) => { + // Let axios set the correct multipart/form-data header with boundary for FormData + if (config.data instanceof FormData && config.headers) { + delete config.headers['Content-Type']; + } + if (!isAuthEndpoint(config.url)) { let token = tokenStorage.getAccessToken(); diff --git a/src/api/news.ts b/src/api/news.ts index b97ea31..70ff5c0 100644 --- a/src/api/news.ts +++ b/src/api/news.ts @@ -66,10 +66,7 @@ export const newsApi = { const response = await apiClient.post( '/cabinet/admin/news/media/upload', formData, - { - headers: { 'Content-Type': 'multipart/form-data' }, - signal, - }, + { signal }, ); return response.data; }, diff --git a/src/api/tickets.ts b/src/api/tickets.ts index df3b3b2..1cb4405 100644 --- a/src/api/tickets.ts +++ b/src/api/tickets.ts @@ -68,11 +68,7 @@ export const ticketsApi = { formData.append('file', file); formData.append('media_type', mediaType); - const response = await apiClient.post('/cabinet/media/upload', formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }); + const response = await apiClient.post('/cabinet/media/upload', formData); return response.data; }, diff --git a/src/pages/AdminNewsCreate.tsx b/src/pages/AdminNewsCreate.tsx index c63e990..3dd31b0 100644 --- a/src/pages/AdminNewsCreate.tsx +++ b/src/pages/AdminNewsCreate.tsx @@ -430,7 +430,7 @@ export default function AdminNewsCreate() { const handleCreateCategory = useCallback( async (name: string, color: string) => { const cat = await newsApi.createCategory({ name, color }); - queryClient.invalidateQueries({ queryKey: ['admin', 'news', 'categories'] }); + await queryClient.invalidateQueries({ queryKey: ['admin', 'news', 'categories'] }); return cat; }, [queryClient], @@ -439,7 +439,7 @@ export default function AdminNewsCreate() { const handleCreateTag = useCallback( async (name: string, color: string) => { const tag = await newsApi.createTag({ name, color }); - queryClient.invalidateQueries({ queryKey: ['admin', 'news', 'tags'] }); + await queryClient.invalidateQueries({ queryKey: ['admin', 'news', 'tags'] }); return tag; }, [queryClient], @@ -475,10 +475,11 @@ export default function AdminNewsCreate() { }); } if (articleData.tag) { + const matchedTag = tagsData?.find((t) => t.id === articleData.tag_id); setSelectedTag({ id: articleData.tag_id ?? 0, name: articleData.tag, - color: articleData.category_color, + color: matchedTag?.color ?? '#94a3b8', }); } setExcerpt(articleData.excerpt ?? ''); @@ -490,7 +491,7 @@ export default function AdminNewsCreate() { editor.commands.setContent(articleData.content); editorPopulated.current = true; } - }, [articleData, editor]); + }, [articleData, editor, tagsData]); // Auto-generate slug from title useEffect(() => { @@ -531,9 +532,9 @@ export default function AdminNewsCreate() { excerpt: excerpt.trim() || null, category: selectedCategory.name, category_color: selectedCategory.color, - category_id: selectedCategory.id || null, - tag: selectedTag?.name || null, - tag_id: selectedTag?.id || null, + category_id: selectedCategory.id !== 0 ? selectedCategory.id : null, + tag: selectedTag?.name ?? null, + tag_id: selectedTag?.id != null && selectedTag.id !== 0 ? selectedTag.id : null, featured_image_url: isSafeUrl(featuredImageUrl.trim()) ? featuredImageUrl.trim() : null, is_published: isPublished, is_featured: isFeatured,