feat: add discard confirmation dialog for recipe editor (#6941)

Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
This commit is contained in:
Hayden
2026-01-28 23:09:32 -06:00
committed by GitHub
parent e48b150f7c
commit 15b5917054
3 changed files with 73 additions and 13 deletions

View File

@@ -1,5 +1,18 @@
<template> <template>
<div> <div>
<BaseDialog
v-model="discardDialog"
:title="$t('general.discard-changes')"
color="warning"
:icon="$globals.icons.alertCircle"
can-confirm
@confirm="confirmDiscard"
@cancel="cancelDiscard"
>
<v-card-text>
{{ $t("general.discard-changes-description") }}
</v-card-text>
</BaseDialog>
<RecipePageParseDialog <RecipePageParseDialog
:model-value="isParsing" :model-value="isParsing"
:ingredients="recipe.recipeIngredient" :ingredients="recipe.recipeIngredient"
@@ -15,6 +28,7 @@
:landscape="landscape" :landscape="landscape"
@save="saveRecipe" @save="saveRecipe"
@delete="deleteRecipe" @delete="deleteRecipe"
@close="closeEditor"
/> />
<RecipeJsonEditor <RecipeJsonEditor
v-if="isEditJSON" v-if="isEditJSON"
@@ -174,6 +188,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { invoke, until } from "@vueuse/core"; import { invoke, until } from "@vueuse/core";
import type { RouteLocationNormalized } from "vue-router";
import RecipeIngredients from "../RecipeIngredients.vue"; import RecipeIngredients from "../RecipeIngredients.vue";
import RecipePageEditorToolbar from "./RecipePageParts/RecipePageEditorToolbar.vue"; import RecipePageEditorToolbar from "./RecipePageParts/RecipePageEditorToolbar.vue";
import RecipePageFooter from "./RecipePageParts/RecipePageFooter.vue"; import RecipePageFooter from "./RecipePageParts/RecipePageFooter.vue";
@@ -205,7 +220,6 @@ import { useNavigationWarning } from "~/composables/use-navigation-warning";
const recipe = defineModel<NoUndefinedField<Recipe>>({ required: true }); const recipe = defineModel<NoUndefinedField<Recipe>>({ required: true });
const display = useDisplay(); const display = useDisplay();
const i18n = useI18n();
const $auth = useMealieAuth(); const $auth = useMealieAuth();
const route = useRoute(); const route = useRoute();
const { isOwnGroup } = useLoggedInState(); const { isOwnGroup } = useLoggedInState();
@@ -231,26 +245,68 @@ const notLinkedIngredients = computed(() => {
* and prompts the user to save if they have unsaved changes. * and prompts the user to save if they have unsaved changes.
*/ */
const originalRecipe = ref<Recipe | null>(null); const originalRecipe = ref<Recipe | null>(null);
const discardDialog = ref(false);
const pendingRoute = ref<RouteLocationNormalized | null>(null);
invoke(async () => { invoke(async () => {
await until(recipe.value).not.toBeNull(); await until(recipe.value).not.toBeNull();
originalRecipe.value = deepCopy(recipe.value); originalRecipe.value = deepCopy(recipe.value);
}); });
onUnmounted(async () => { function hasUnsavedChanges(): boolean {
const isSame = JSON.stringify(recipe.value) === JSON.stringify(originalRecipe.value); if (originalRecipe.value === null) {
if (isEditMode.value && !isSame && recipe.value?.slug !== undefined) { return false;
const save = window.confirm(i18n.t("general.unsaved-changes"));
if (save) {
await api.recipes.updateOne(recipe.value.slug, recipe.value);
}
} }
return JSON.stringify(recipe.value) !== JSON.stringify(originalRecipe.value);
}
function restoreOriginalRecipe() {
if (originalRecipe.value) {
recipe.value = deepCopy(originalRecipe.value) as NoUndefinedField<Recipe>;
}
}
function closeEditor() {
if (hasUnsavedChanges()) {
pendingRoute.value = null;
discardDialog.value = true;
}
else {
setMode(PageMode.VIEW);
}
}
function confirmDiscard() {
restoreOriginalRecipe();
discardDialog.value = false;
if (pendingRoute.value) {
const destination = pendingRoute.value;
pendingRoute.value = null;
router.push(destination);
}
else {
setMode(PageMode.VIEW);
}
}
function cancelDiscard() {
discardDialog.value = false;
pendingRoute.value = null;
}
onBeforeRouteLeave((to) => {
if (isEditMode.value && hasUnsavedChanges()) {
pendingRoute.value = to;
discardDialog.value = true;
return false;
}
});
onUnmounted(() => {
deactivateNavigationWarning(); deactivateNavigationWarning();
toggleCookMode(); toggleCookMode();
clearPageState(recipe.value.slug || ""); clearPageState(recipe.value.slug || "");
console.debug("reset RecipePage state during unmount");
}); });
const hasLinkedIngredients = computed(() => { const hasLinkedIngredients = computed(() => {
return recipe.value.recipeInstructions.some( return recipe.value.recipeInstructions.some(
@@ -300,6 +356,8 @@ async function saveRecipe() {
if (data?.slug) { if (data?.slug) {
router.push(`/g/${groupSlug.value}/r/` + data.slug); router.push(`/g/${groupSlug.value}/r/` + data.slug);
recipe.value = data as NoUndefinedField<Recipe>; recipe.value = data as NoUndefinedField<Recipe>;
// Update the snapshot after successful save
originalRecipe.value = deepCopy(recipe.value);
} }
} }

View File

@@ -16,7 +16,7 @@
:open="isEditMode" :open="isEditMode"
:recipe-id="recipe.id" :recipe-id="recipe.id"
class="ml-auto mt-n7 pb-4" class="ml-auto mt-n7 pb-4"
@close="setMode(PageMode.VIEW)" @close="$emit('close')"
@json="toggleEditMode()" @json="toggleEditMode()"
@edit="setMode(PageMode.EDIT)" @edit="setMode(PageMode.EDIT)"
@save="$emit('save')" @save="$emit('save')"
@@ -47,7 +47,7 @@ const props = withDefaults(defineProps<Props>(), {
landscape: false, landscape: false,
}); });
defineEmits(["save", "delete", "print"]); defineEmits(["save", "delete", "print", "close"]);
const { recipeImage } = useStaticRoutes(); const { recipeImage } = useStaticRoutes();
const { imageKey, setMode, toggleEditMode, isEditMode } = usePageState(props.recipe.slug); const { imageKey, setMode, toggleEditMode, isEditMode } = usePageState(props.recipe.slug);

View File

@@ -212,6 +212,8 @@
"upload-file": "Upload File", "upload-file": "Upload File",
"created-on-date": "Created on: {0}", "created-on-date": "Created on: {0}",
"unsaved-changes": "You have unsaved changes. Do you want to save before leaving? Okay to save, Cancel to discard changes.", "unsaved-changes": "You have unsaved changes. Do you want to save before leaving? Okay to save, Cancel to discard changes.",
"discard-changes": "Discard Changes",
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
"clipboard-copy-failure": "Failed to copy to the clipboard.", "clipboard-copy-failure": "Failed to copy to the clipboard.",
"confirm-delete-generic-items": "Are you sure you want to delete the following items?", "confirm-delete-generic-items": "Are you sure you want to delete the following items?",
"organizers": "Organizers", "organizers": "Organizers",