diff --git a/frontend/src/components/Labels/LabelCreateDialog.tsx b/frontend/src/components/Labels/LabelCreateDialog.tsx index 0d4f6fb..476ec5f 100644 --- a/frontend/src/components/Labels/LabelCreateDialog.tsx +++ b/frontend/src/components/Labels/LabelCreateDialog.tsx @@ -12,6 +12,7 @@ import { Paper, Tooltip, } from '@mui/material'; +import axios from 'axios'; import Grid from '@mui/material/GridLegacy'; import { Star as StarIcon, @@ -142,7 +143,14 @@ const LabelCreateDialog: React.FC = ({ onClose(); } catch (error) { console.error('Failed to save label:', error); - // Could add error handling UI here + // Extract error message from backend JSON response + if (axios.isAxiosError(error) && error.response?.data?.error) { + setNameError(error.response.data.error); + } else if (error instanceof Error) { + setNameError(error.message); + } else { + setNameError(t('labels.errors.serverError')); + } } finally { setLoading(false); } @@ -183,7 +191,7 @@ const LabelCreateDialog: React.FC = ({ {editingLabel ? t('labels.create.editTitle') : t('labels.create.title')} - + {/* Name Field */} diff --git a/src/routes/labels.rs b/src/routes/labels.rs index 9c5a089..56def5e 100644 --- a/src/routes/labels.rs +++ b/src/routes/labels.rs @@ -12,7 +12,7 @@ use uuid::Uuid; use chrono::{DateTime, Utc}; use sqlx::{FromRow, Row}; -use crate::{auth::AuthUser, AppState}; +use crate::{auth::AuthUser, errors::label::LabelError, AppState}; #[derive(Debug, Clone, Serialize, Deserialize, FromRow, ToSchema)] pub struct Label { @@ -166,28 +166,28 @@ pub async fn create_label( State(state): State>, auth_user: AuthUser, Json(payload): Json, -) -> Result, StatusCode> { +) -> Result, LabelError> { let user_id = auth_user.user.id; // Validate name is not empty if payload.name.trim().is_empty() { - return Err(StatusCode::BAD_REQUEST); + return Err(LabelError::invalid_name(payload.name.clone(), "Name cannot be empty".to_string())); } // Disallow commas in label names (breaks comma-separated search filters) // Note: URL-encoded values (%2c) are decoded by serde before reaching here if payload.name.contains(',') { - return Err(StatusCode::BAD_REQUEST); + return Err(LabelError::invalid_name(payload.name.clone(), "Name cannot contain commas".to_string())); } // Validate color format if !payload.color.starts_with('#') || payload.color.len() != 7 { - return Err(StatusCode::BAD_REQUEST); + return Err(LabelError::invalid_color(&payload.color)); } if let Some(ref bg_color) = payload.background_color { if !bg_color.starts_with('#') || bg_color.len() != 7 { - return Err(StatusCode::BAD_REQUEST); + return Err(LabelError::invalid_color(bg_color)); } } @@ -195,14 +195,14 @@ pub async fn create_label( r#" INSERT INTO labels (user_id, name, description, color, background_color, icon) VALUES ($1, $2, $3, $4, $5, $6) - RETURNING - id, user_id, name, description, color, background_color, icon, + RETURNING + id, user_id, name, description, color, background_color, icon, is_system, created_at, updated_at, 0::bigint as document_count, 0::bigint as source_count "# ) .bind(user_id) - .bind(payload.name) + .bind(&payload.name) .bind(payload.description) .bind(payload.color) .bind(payload.background_color) @@ -212,9 +212,9 @@ pub async fn create_label( .map_err(|e| { tracing::error!("Failed to create label: {}", e); if e.to_string().contains("duplicate key") { - StatusCode::CONFLICT + LabelError::duplicate_name(payload.name.clone()) } else { - StatusCode::INTERNAL_SERVER_ERROR + LabelError::invalid_name(payload.name.clone(), e.to_string()) } })?; @@ -291,27 +291,31 @@ pub async fn update_label( State(state): State>, auth_user: AuthUser, Json(payload): Json, -) -> Result, StatusCode> { +) -> Result, LabelError> { let user_id = auth_user.user.id; - // Disallow commas in label names (breaks comma-separated search filters) - // Note: URL-encoded values (%2c) are decoded by serde before reaching here + // Validate name if provided if let Some(ref name) = payload.name { + if name.trim().is_empty() { + return Err(LabelError::invalid_name(name.clone(), "Name cannot be empty".to_string())); + } + // Disallow commas in label names (breaks comma-separated search filters) + // Note: URL-encoded values (%2c) are decoded by serde before reaching here if name.contains(',') { - return Err(StatusCode::BAD_REQUEST); + return Err(LabelError::invalid_name(name.clone(), "Name cannot contain commas".to_string())); } } // Validate color formats if provided if let Some(ref color) = payload.color { if !color.starts_with('#') || color.len() != 7 { - return Err(StatusCode::BAD_REQUEST); + return Err(LabelError::invalid_color(color)); } } - if let Some(ref bg_color) = payload.background_color.as_ref() { + if let Some(ref bg_color) = payload.background_color { if !bg_color.starts_with('#') || bg_color.len() != 7 { - return Err(StatusCode::BAD_REQUEST); + return Err(LabelError::invalid_color(bg_color)); } } @@ -325,18 +329,18 @@ pub async fn update_label( .await .map_err(|e| { tracing::error!("Failed to check label existence: {}", e); - StatusCode::INTERNAL_SERVER_ERROR + LabelError::NotFound })?; if existing.is_none() { - return Err(StatusCode::NOT_FOUND); + return Err(LabelError::NotFound); } // Use COALESCE to update only provided fields let label = sqlx::query_as::<_, Label>( r#" - UPDATE labels - SET + UPDATE labels + SET name = COALESCE($2, name), description = COALESCE($3, description), color = COALESCE($4, color), @@ -344,14 +348,14 @@ pub async fn update_label( icon = COALESCE($6, icon), updated_at = CURRENT_TIMESTAMP WHERE id = $1 - RETURNING - id, user_id, name, description, color, background_color, icon, + RETURNING + id, user_id, name, description, color, background_color, icon, is_system, created_at, updated_at, 0::bigint as document_count, 0::bigint as source_count "# ) .bind(label_id) - .bind(payload.name) + .bind(&payload.name) .bind(payload.description) .bind(payload.color) .bind(payload.background_color) @@ -361,9 +365,9 @@ pub async fn update_label( .map_err(|e| { tracing::error!("Failed to update label: {}", e); if e.to_string().contains("duplicate key") { - StatusCode::CONFLICT + LabelError::duplicate_name(payload.name.clone().unwrap_or_default()) } else { - StatusCode::INTERNAL_SERVER_ERROR + LabelError::invalid_name(payload.name.clone().unwrap_or_default(), e.to_string()) } })?;