mirror of
https://github.com/readur/readur.git
synced 2025-12-16 20:04:32 -06:00
fix(ui): handle duplicate label error
This commit is contained in:
@@ -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<LabelCreateDialogProps> = ({
|
||||
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<LabelCreateDialogProps> = ({
|
||||
{editingLabel ? t('labels.create.editTitle') : t('labels.create.title')}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent sx={{ pt: 2 }}>
|
||||
<DialogContent sx={{ pt: 4 }}>
|
||||
<Grid container spacing={3}>
|
||||
{/* Name Field */}
|
||||
<Grid item xs={12}>
|
||||
|
||||
@@ -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<Arc<AppState>>,
|
||||
auth_user: AuthUser,
|
||||
Json(payload): Json<CreateLabel>,
|
||||
) -> Result<Json<Label>, StatusCode> {
|
||||
) -> Result<Json<Label>, 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<Arc<AppState>>,
|
||||
auth_user: AuthUser,
|
||||
Json(payload): Json<UpdateLabel>,
|
||||
) -> Result<Json<Label>, StatusCode> {
|
||||
) -> Result<Json<Label>, 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())
|
||||
}
|
||||
})?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user