mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-20 06:43:11 -05:00
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:
@@ -13,6 +13,22 @@
|
|||||||
{{ $t("general.discard-changes-description") }}
|
{{ $t("general.discard-changes-description") }}
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</BaseDialog>
|
</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
|
<RecipePageParseDialog
|
||||||
:model-value="isParsing"
|
:model-value="isParsing"
|
||||||
:ingredients="recipe.recipeIngredient"
|
:ingredients="recipe.recipeIngredient"
|
||||||
@@ -246,6 +262,8 @@ const notLinkedIngredients = computed(() => {
|
|||||||
*/
|
*/
|
||||||
const originalRecipe = ref<Recipe | null>(null);
|
const originalRecipe = ref<Recipe | null>(null);
|
||||||
const discardDialog = ref(false);
|
const discardDialog = ref(false);
|
||||||
|
const saveErrorDialog = ref(false);
|
||||||
|
const saveErrorMessage = ref("");
|
||||||
const pendingRoute = ref<RouteLocationNormalized | null>(null);
|
const pendingRoute = ref<RouteLocationNormalized | null>(null);
|
||||||
|
|
||||||
invoke(async () => {
|
invoke(async () => {
|
||||||
@@ -353,8 +371,9 @@ async function saveRecipe() {
|
|||||||
if (!error) {
|
if (!error) {
|
||||||
setMode(PageMode.VIEW);
|
setMode(PageMode.VIEW);
|
||||||
} else {
|
} else {
|
||||||
// Restore original recipe data on error to prevent data loss
|
// Show prominent error dialog for save failures
|
||||||
restoreOriginalRecipe();
|
saveErrorMessage.value = error?.response?.data?.detail?.message || "An error occurred while saving the recipe.";
|
||||||
|
saveErrorDialog.value = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (data?.slug) {
|
if (data?.slug) {
|
||||||
|
|||||||
@@ -530,6 +530,8 @@
|
|||||||
"recipe-settings": "Recipe Settings",
|
"recipe-settings": "Recipe Settings",
|
||||||
"recipe-update-failed": "Recipe update failed",
|
"recipe-update-failed": "Recipe update failed",
|
||||||
"recipe-updated": "Recipe updated",
|
"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-from-favorites": "Remove from Favorites",
|
||||||
"remove-section": "Remove Section",
|
"remove-section": "Remove Section",
|
||||||
"saturated-fat-content": "Saturated fat",
|
"saturated-fat-content": "Saturated fat",
|
||||||
|
|||||||
@@ -92,7 +92,10 @@ class RecipeController(BaseRecipeController):
|
|||||||
elif thrownType == sqlalchemy.exc.IntegrityError:
|
elif thrownType == sqlalchemy.exc.IntegrityError:
|
||||||
self.logger.error("SQL Integrity Error on recipe controller action")
|
self.logger.error("SQL Integrity Error on recipe controller action")
|
||||||
raise HTTPException(
|
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:
|
elif thrownType == exceptions.RecursiveRecipe:
|
||||||
self.logger.error("Recursive Recipe Link Error on recipe controller action")
|
self.logger.error("Recursive Recipe Link Error on recipe controller action")
|
||||||
|
|||||||
@@ -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):
|
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
|
# Create two recipes with different names
|
||||||
recipe1_name = random_string(10)
|
recipe1_name = random_string(10)
|
||||||
recipe2_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
|
headers=unique_user.token
|
||||||
)
|
)
|
||||||
|
|
||||||
# Should return 400 error with appropriate message
|
# Should return 400 error with helpful message
|
||||||
assert update_response.status_code == 400
|
assert update_response.status_code == 400
|
||||||
response_data = json.loads(update_response.text)
|
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)
|
get_response = api_client.get(api_routes.recipes_slug(recipe2_slug), headers=unique_user.token)
|
||||||
assert get_response.status_code == 200
|
assert get_response.status_code == 200
|
||||||
unchanged_recipe = json.loads(get_response.text)
|
unchanged_recipe = json.loads(get_response.text)
|
||||||
|
|||||||
Reference in New Issue
Block a user