mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-01-30 12:33:11 -05:00
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:
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user