From fb4aa2b7139749b8a80b1c1b64bac0e7f76e45c9 Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Fri, 25 Jul 2025 07:18:10 -0500 Subject: [PATCH] fix: Better UX and Error Handling For Adding Timeline Events (#5798) --- .../Domain/Recipe/RecipeLastMade.vue | 87 ++++++++++++++----- frontend/components/global/BaseDialog.vue | 2 +- frontend/lang/messages/en-US.json | 4 + 3 files changed, 69 insertions(+), 24 deletions(-) diff --git a/frontend/components/Domain/Recipe/RecipeLastMade.vue b/frontend/components/Domain/Recipe/RecipeLastMade.vue index 8009bdc13..b9871e8f9 100644 --- a/frontend/components/Domain/Recipe/RecipeLastMade.vue +++ b/frontend/components/Domain/Recipe/RecipeLastMade.vue @@ -3,6 +3,7 @@
import { whenever } from "@vueuse/core"; import { useUserApi } from "~/composables/api"; +import { alert } from "~/composables/use-toast"; import { useHouseholdSelf } from "~/composables/use-households"; -import type { Recipe, RecipeTimelineEventIn } from "~/lib/api/types/recipe"; +import type { Recipe, RecipeTimelineEventIn, RecipeTimelineEventOut } from "~/lib/api/types/recipe"; import type { VForm } from "~/types/auto-forms"; export default defineNuxtComponent({ @@ -196,12 +198,25 @@ export default defineNuxtComponent({ newTimelineEventImagePreviewUrl.value = URL.createObjectURL(fileObject); } - const state = reactive({ datePickerMenu: false }); + const state = reactive({ datePickerMenu: false, madeThisFormLoading: false }); + + function resetMadeThisForm() { + state.madeThisFormLoading = false; + + newTimelineEvent.value.eventMessage = ""; + newTimelineEvent.value.timestamp = undefined; + clearImage(); + madeThisDialog.value = false; + domMadeThisForm.value?.reset(); + } + async function createTimelineEvent() { if (!(newTimelineEventTimestampString.value && props.recipe?.id && props.recipe?.slug)) { return; } + state.madeThisFormLoading = true; + newTimelineEvent.value.recipeId = props.recipe.id; // Note: $auth.user is now a ref newTimelineEvent.value.subject = i18n.t("recipe.user-made-this", { user: $auth.user.value?.fullName }); @@ -210,34 +225,60 @@ export default defineNuxtComponent({ // we choose the end of day so it always comes after "new recipe" events newTimelineEvent.value.timestamp = new Date(newTimelineEventTimestampString.value + "T23:59:59").toISOString(); - const eventResponse = await userApi.recipes.createTimelineEvent(newTimelineEvent.value); - const newEvent = eventResponse.data; + let newEvent: RecipeTimelineEventOut | null = null; + try { + const eventResponse = await userApi.recipes.createTimelineEvent(newTimelineEvent.value); + newEvent = eventResponse.data; + if (!newEvent) { + throw new Error("No event created"); + } + } + catch (error) { + console.error("Failed to create timeline event:", error); + alert.error(i18n.t("recipe.failed-to-add-to-timeline")); + resetMadeThisForm(); + return; + } // we also update the recipe's last made value if (!lastMade.value || newTimelineEvent.value.timestamp > lastMade.value) { - lastMade.value = newTimelineEvent.value.timestamp; - await userApi.recipes.updateLastMade(props.recipe.slug, newTimelineEvent.value.timestamp); - } - - // update the image, if provided - if (newTimelineEventImage.value && newEvent) { - const imageResponse = await userApi.recipes.updateTimelineEventImage( - newEvent.id, - newTimelineEventImage.value, - newTimelineEventImageName.value, - ); - if (imageResponse.data) { - newEvent.image = imageResponse.data.image; + try { + lastMade.value = newTimelineEvent.value.timestamp; + await userApi.recipes.updateLastMade(props.recipe.slug, newTimelineEvent.value.timestamp); + } + catch (error) { + console.error("Failed to update last made date:", error); + alert.error(i18n.t("recipe.failed-to-update-recipe")); } } - // reset form - newTimelineEvent.value.eventMessage = ""; - newTimelineEvent.value.timestamp = undefined; - clearImage(); - madeThisDialog.value = false; - domMadeThisForm.value?.reset(); + // update the image, if provided + let imageError = false; + if (newTimelineEventImage.value) { + try { + const imageResponse = await userApi.recipes.updateTimelineEventImage( + newEvent.id, + newTimelineEventImage.value, + newTimelineEventImageName.value, + ); + if (imageResponse.data) { + newEvent.image = imageResponse.data.image; + } + } + catch (error) { + imageError = true; + console.error("Failed to upload image for timeline event:", error); + } + } + if (imageError) { + alert.error(i18n.t("recipe.added-to-timeline-but-failed-to-add-image")); + } + else { + alert.success(i18n.t("recipe.added-to-timeline")); + } + + resetMadeThisForm(); context.emit("eventCreated", newEvent); } diff --git a/frontend/components/global/BaseDialog.vue b/frontend/components/global/BaseDialog.vue index 2bf4ddf7e..8126f4b1e 100644 --- a/frontend/components/global/BaseDialog.vue +++ b/frontend/components/global/BaseDialog.vue @@ -82,7 +82,7 @@ {{ submitText }} diff --git a/frontend/lang/messages/en-US.json b/frontend/lang/messages/en-US.json index 594498269..59437e6d4 100644 --- a/frontend/lang/messages/en-US.json +++ b/frontend/lang/messages/en-US.json @@ -579,6 +579,10 @@ "made-this": "I Made This", "how-did-it-turn-out": "How did it turn out?", "user-made-this": "{user} made this", + "added-to-timeline": "Added to timeline", + "failed-to-add-to-timeline": "Failed to add to timeline", + "failed-to-update-recipe": "Failed to update recipe", + "added-to-timeline-but-failed-to-add-image": "Added to timeline, but failed to add image", "api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.", "message-key": "Message Key", "parse": "Parse",