Improve error handling for duplicate recipe names

- Enhanced backend error message to be more descriptive and actionable
- Added prominent error dialog in frontend to alert user about save failures
- Keep user's edits in the editor so they can fix the issue and retry
- Updated test to verify improved error messaging

Co-authored-by: hay-kot <64056131+hay-kot@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-29 14:21:59 +00:00
parent 874dc94d81
commit cee2c351a3
4 changed files with 34 additions and 7 deletions

View File

@@ -13,6 +13,22 @@
{{ $t("general.discard-changes-description") }}
</v-card-text>
</BaseDialog>
<BaseDialog
v-model="saveErrorDialog"
:title="$t('recipe.save-error')"
color="error"
:icon="$globals.icons.alertCircle"
@cancel="saveErrorDialog = false"
>
<v-card-text>
<p class="mb-2">
<strong>{{ saveErrorMessage }}</strong>
</p>
<p>
{{ $t('recipe.save-error-description') }}
</p>
</v-card-text>
</BaseDialog>
<RecipePageParseDialog
:model-value="isParsing"
:ingredients="recipe.recipeIngredient"
@@ -246,6 +262,8 @@ const notLinkedIngredients = computed(() => {
*/
const originalRecipe = ref<Recipe | null>(null);
const discardDialog = ref(false);
const saveErrorDialog = ref(false);
const saveErrorMessage = ref("");
const pendingRoute = ref<RouteLocationNormalized | null>(null);
invoke(async () => {
@@ -353,8 +371,9 @@ async function saveRecipe() {
if (!error) {
setMode(PageMode.VIEW);
} else {
// Restore original recipe data on error to prevent data loss
restoreOriginalRecipe();
// Show prominent error dialog for save failures
saveErrorMessage.value = error?.response?.data?.detail?.message || "An error occurred while saving the recipe.";
saveErrorDialog.value = true;
return;
}
if (data?.slug) {

View File

@@ -530,6 +530,8 @@
"recipe-settings": "Recipe Settings",
"recipe-update-failed": "Recipe update failed",
"recipe-updated": "Recipe updated",
"save-error": "Unable to Save Recipe",
"save-error-description": "Your changes have been preserved in the editor. Please fix the issue and try saving again.",
"remove-from-favorites": "Remove from Favorites",
"remove-section": "Remove Section",
"saturated-fat-content": "Saturated fat",

View File

@@ -92,7 +92,10 @@ class RecipeController(BaseRecipeController):
elif thrownType == sqlalchemy.exc.IntegrityError:
self.logger.error("SQL Integrity Error on recipe controller action")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ErrorResponse.respond(message="Recipe already exists")
status_code=status.HTTP_400_BAD_REQUEST,
detail=ErrorResponse.respond(
message="A recipe with this name already exists. Please choose a different name and try saving again."
)
)
elif thrownType == exceptions.RecursiveRecipe:
self.logger.error("Recursive Recipe Link Error on recipe controller action")

View File

@@ -1327,7 +1327,7 @@ def test_create_recipe_slug_length_validation(api_client: TestClient, unique_use
def test_recipe_update_duplicate_name_error(api_client: TestClient, unique_user: TestUser):
"""Test that updating a recipe with a duplicate name returns a 400 error."""
"""Test that updating a recipe with a duplicate name returns a 400 error with a helpful message."""
# Create two recipes with different names
recipe1_name = random_string(10)
recipe2_name = random_string(10)
@@ -1353,12 +1353,15 @@ def test_recipe_update_duplicate_name_error(api_client: TestClient, unique_user:
headers=unique_user.token
)
# Should return 400 error with appropriate message
# Should return 400 error with helpful message
assert update_response.status_code == 400
response_data = json.loads(update_response.text)
assert "already exists" in response_data["detail"]["message"].lower()
error_message = response_data["detail"]["message"]
assert "already exists" in error_message.lower()
assert "choose a different name" in error_message.lower()
assert "try saving again" in error_message.lower()
# Verify original recipe2 is unchanged
# Verify original recipe2 is unchanged in the database
get_response = api_client.get(api_routes.recipes_slug(recipe2_slug), headers=unique_user.token)
assert get_response.status_code == 200
unchanged_recipe = json.loads(get_response.text)