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",