mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-06-02 07:00:26 -04:00
feat: warn when deleting foods used in recipes (#7117)
Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
364af97060
commit
262b531add
@@ -59,9 +59,10 @@
|
|||||||
>
|
>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
{{ $t("general.confirm-delete-generic") }}
|
{{ $t("general.confirm-delete-generic") }}
|
||||||
<p v-if="deleteTarget" class="mt-4 ml-4">
|
<p v-if="deleteTarget" class="mt-4 mb-0 font-weight-bold">
|
||||||
{{ deleteTarget.name || deleteTarget.title || deleteTarget.id }}
|
{{ deleteTarget.name || deleteTarget.title || deleteTarget.id }}
|
||||||
</p>
|
</p>
|
||||||
|
<slot name="delete-dialog-bottom" />
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
||||||
@@ -88,6 +89,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</v-virtual-scroll>
|
</v-virtual-scroll>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
<slot name="delete-dialog-bottom" />
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
||||||
@@ -151,7 +153,7 @@ const createDialog = defineModel("createDialog", { type: Boolean, default: false
|
|||||||
const editForm = defineModel<{ items: AutoFormItems; data: Record<string, any> }>("editForm", { required: true });
|
const editForm = defineModel<{ items: AutoFormItems; data: Record<string, any> }>("editForm", { required: true });
|
||||||
const editDialog = defineModel("editDialog", { type: Boolean, default: false });
|
const editDialog = defineModel("editDialog", { type: Boolean, default: false });
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
icon: {
|
icon: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -185,6 +187,10 @@ defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: "name",
|
default: "name",
|
||||||
},
|
},
|
||||||
|
onDeleteDialogOpen: {
|
||||||
|
type: Function as PropType<(items: any[]) => Promise<void>>,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -212,8 +218,11 @@ const editEventHandler = (item: any) => {
|
|||||||
const deleteTarget = ref<any>(null);
|
const deleteTarget = ref<any>(null);
|
||||||
const deleteDialog = ref(false);
|
const deleteDialog = ref(false);
|
||||||
|
|
||||||
function deleteEventHandler(item: any) {
|
async function deleteEventHandler(item: any) {
|
||||||
deleteTarget.value = item;
|
deleteTarget.value = item;
|
||||||
|
if (props.onDeleteDialogOpen) {
|
||||||
|
await props.onDeleteDialogOpen([item]);
|
||||||
|
}
|
||||||
deleteDialog.value = true;
|
deleteDialog.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,8 +231,11 @@ function deleteEventHandler(item: any) {
|
|||||||
const bulkDeleteTarget = ref<Array<any>>([]);
|
const bulkDeleteTarget = ref<Array<any>>([]);
|
||||||
const bulkDeleteDialog = ref(false);
|
const bulkDeleteDialog = ref(false);
|
||||||
|
|
||||||
function bulkDeleteEventHandler(items: Array<any>) {
|
async function bulkDeleteEventHandler(items: Array<any>) {
|
||||||
bulkDeleteTarget.value = items;
|
bulkDeleteTarget.value = items;
|
||||||
|
if (props.onDeleteDialogOpen) {
|
||||||
|
await props.onDeleteDialogOpen(items);
|
||||||
|
}
|
||||||
bulkDeleteDialog.value = true;
|
bulkDeleteDialog.value = true;
|
||||||
console.log("Bulk Delete Event Handler", items);
|
console.log("Bulk Delete Event Handler", items);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1144,6 +1144,8 @@
|
|||||||
},
|
},
|
||||||
"data-pages": {
|
"data-pages": {
|
||||||
"foods": {
|
"foods": {
|
||||||
|
"delete-affects-recipes": "Warning: this food is used in {count} recipe(s). Deleting it will leave an empty ingredient in the recipe(s).",
|
||||||
|
"delete-affects-recipes-more": "View all {count} recipes",
|
||||||
"merge-dialog-text": "Combining the selected foods will merge the source food and target food into a single food. The source food will be deleted and all of the references to the source food will be updated to point to the target food.",
|
"merge-dialog-text": "Combining the selected foods will merge the source food and target food into a single food. The source food will be deleted and all of the references to the source food will be updated to point to the target food.",
|
||||||
"merge-food-example": "Merging {food1} into {food2}",
|
"merge-food-example": "Merging {food1} into {food2}",
|
||||||
"seed-dialog-text": "Seed the database with foods based on your local language. This will create ~2700 common foods that can be used to organize your database. Foods are translated via a community effort.",
|
"seed-dialog-text": "Seed the database with foods based on your local language. This will create ~2700 common foods that can be used to organize your database. Foods are translated via a community effort.",
|
||||||
|
|||||||
@@ -69,11 +69,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</v-autocomplete>
|
</v-autocomplete>
|
||||||
|
|
||||||
<v-alert
|
<v-alert v-if="foods && foods.length > 0" type="error" class="mb-0 text-body-2">
|
||||||
v-if="foods && foods.length > 0"
|
|
||||||
type="error"
|
|
||||||
class="mb-0 text-body-2"
|
|
||||||
>
|
|
||||||
{{ $t("data-pages.foods.seed-dialog-warning") }}
|
{{ $t("data-pages.foods.seed-dialog-warning") }}
|
||||||
</v-alert>
|
</v-alert>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
@@ -112,11 +108,7 @@
|
|||||||
:label="$t('data-pages.foods.food-label')"
|
:label="$t('data-pages.foods.food-label')"
|
||||||
/>
|
/>
|
||||||
<v-card variant="outlined">
|
<v-card variant="outlined">
|
||||||
<v-virtual-scroll
|
<v-virtual-scroll height="400" item-height="25" :items="bulkAssignTarget">
|
||||||
height="400"
|
|
||||||
item-height="25"
|
|
||||||
:items="bulkAssignTarget"
|
|
||||||
>
|
|
||||||
<template #default="{ item }">
|
<template #default="{ item }">
|
||||||
<v-list-item class="pb-2">
|
<v-list-item class="pb-2">
|
||||||
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||||
@@ -141,6 +133,7 @@
|
|||||||
]"
|
]"
|
||||||
:create-form="createForm"
|
:create-form="createForm"
|
||||||
:edit-form="editForm"
|
:edit-form="editForm"
|
||||||
|
:on-delete-dialog-open="onDeleteDialogOpen"
|
||||||
@create-one="handleCreate"
|
@create-one="handleCreate"
|
||||||
@edit-one="handleEdit"
|
@edit-one="handleEdit"
|
||||||
@delete-one="foodStore.actions.deleteOne"
|
@delete-one="foodStore.actions.deleteOne"
|
||||||
@@ -151,15 +144,12 @@
|
|||||||
<template #icon>
|
<template #icon>
|
||||||
{{ $globals.icons.externalLink }}
|
{{ $globals.icons.externalLink }}
|
||||||
</template>
|
</template>
|
||||||
{{ $t('data-pages.combine') }}
|
{{ $t("data-pages.combine") }}
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #[`item.label`]="{ item }">
|
<template #[`item.label`]="{ item }">
|
||||||
<MultiPurposeLabel
|
<MultiPurposeLabel v-if="item.label" :label="item.label">
|
||||||
v-if="item.label"
|
|
||||||
:label="item.label"
|
|
||||||
>
|
|
||||||
{{ item.label.name }}
|
{{ item.label.name }}
|
||||||
</MultiPurposeLabel>
|
</MultiPurposeLabel>
|
||||||
</template>
|
</template>
|
||||||
@@ -171,7 +161,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #[`item.createdAt`]="{ item }">
|
<template #[`item.createdAt`]="{ item }">
|
||||||
{{ item.createdAt ? $d(new Date(item.createdAt)) : '' }}
|
{{ item.createdAt ? $d(new Date(item.createdAt)) : "" }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #table-button-bottom>
|
<template #table-button-bottom>
|
||||||
@@ -179,18 +169,33 @@
|
|||||||
<template #icon>
|
<template #icon>
|
||||||
{{ $globals.icons.database }}
|
{{ $globals.icons.database }}
|
||||||
</template>
|
</template>
|
||||||
{{ $t('data-pages.seed') }}
|
{{ $t("data-pages.seed") }}
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #edit-dialog-custom-action>
|
<template #edit-dialog-custom-action>
|
||||||
<BaseButton
|
<BaseButton edit @click="aliasManagerDialog = true">
|
||||||
edit
|
{{ $t("data-pages.manage-aliases") }}
|
||||||
@click="aliasManagerDialog = true"
|
|
||||||
>
|
|
||||||
{{ $t('data-pages.manage-aliases') }}
|
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #delete-dialog-bottom>
|
||||||
|
<v-alert v-if="affectedRecipes.length > 0" type="warning" density="compact" class="mt-4 mb-0">
|
||||||
|
{{ $t("data-pages.foods.delete-affects-recipes", { count: affectedRecipesTotal }) }}
|
||||||
|
<ul class="mt-1 pl-5 mb-0">
|
||||||
|
<li v-for="recipe in affectedRecipes.slice(0, 5)" :key="recipe.slug">
|
||||||
|
<NuxtLink :to="recipe.url" class="text-white">{{ recipe.name }}</NuxtLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<NuxtLink
|
||||||
|
v-if="affectedRecipesTotal > 5"
|
||||||
|
:to="affectedRecipesMoreLink"
|
||||||
|
class="text-white d-inline-block mt-1"
|
||||||
|
>
|
||||||
|
{{ $t("data-pages.foods.delete-affects-recipes-more", { count: affectedRecipesTotal }) }}
|
||||||
|
</NuxtLink>
|
||||||
|
</v-alert>
|
||||||
|
</template>
|
||||||
</GroupDataPage>
|
</GroupDataPage>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -218,6 +223,7 @@ interface CreateIngredientFoodWithOnHand extends CreateIngredientFood {
|
|||||||
interface IngredientFoodWithOnHand extends IngredientFood {
|
interface IngredientFoodWithOnHand extends IngredientFood {
|
||||||
onHand: boolean;
|
onHand: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userApi = useUserApi();
|
const userApi = useUserApi();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
@@ -274,11 +280,14 @@ const tableHeaders: TableHeaders[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const userHousehold = computed(() => auth.user.value?.householdSlug || "");
|
const userHousehold = computed(() => auth.user.value?.householdSlug || "");
|
||||||
|
const userGroup = computed(() => auth.user.value?.groupSlug || "");
|
||||||
const foodStore = useFoodStore();
|
const foodStore = useFoodStore();
|
||||||
const foods = computed(() => foodStore.store.value.map((food) => {
|
const foods = computed(() =>
|
||||||
|
foodStore.store.value.map((food) => {
|
||||||
const onHand = food.householdsWithIngredientFood?.includes(userHousehold.value) || false;
|
const onHand = food.householdsWithIngredientFood?.includes(userHousehold.value) || false;
|
||||||
return { ...food, onHand } as IngredientFoodWithOnHand;
|
return { ...food, onHand } as IngredientFoodWithOnHand;
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Labels
|
// Labels
|
||||||
@@ -383,6 +392,9 @@ async function handleBulkAction(event: string, items: IngredientFoodWithOnHand[]
|
|||||||
if (event === "delete-selected") {
|
if (event === "delete-selected") {
|
||||||
const ids = items.map(item => item.id);
|
const ids = items.map(item => item.id);
|
||||||
await foodStore.actions.deleteMany(ids);
|
await foodStore.actions.deleteMany(ids);
|
||||||
|
affectedRecipes.value = [];
|
||||||
|
affectedRecipesTotal.value = 0;
|
||||||
|
affectedRecipesMoreLink.value = "";
|
||||||
}
|
}
|
||||||
else if (event === "assign-selected") {
|
else if (event === "assign-selected") {
|
||||||
bulkAssignEventHandler(items);
|
bulkAssignEventHandler(items);
|
||||||
@@ -401,6 +413,26 @@ function updateFoodAlias(newAliases: IngredientFoodAlias[]) {
|
|||||||
aliasManagerDialog.value = false;
|
aliasManagerDialog.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Delete Foods
|
||||||
|
|
||||||
|
// fetch affected recipes before confirming deletion
|
||||||
|
const affectedRecipes = ref<{ name: string; slug: string; url: string }[]>([]);
|
||||||
|
const affectedRecipesTotal = ref(0);
|
||||||
|
const affectedRecipesMoreLink = ref("");
|
||||||
|
|
||||||
|
async function onDeleteDialogOpen(items: IngredientFoodWithOnHand[]) {
|
||||||
|
const ids = items.map(item => item.id);
|
||||||
|
const { data } = await userApi.recipes.search({ foods: ids, perPage: 5 });
|
||||||
|
affectedRecipes.value = (data?.items ?? []).map(r => ({
|
||||||
|
name: r.name ?? "",
|
||||||
|
slug: r.slug ?? "",
|
||||||
|
url: `/g/${userGroup.value}/r/${r.slug}`,
|
||||||
|
}));
|
||||||
|
affectedRecipesTotal.value = data?.total ?? 0;
|
||||||
|
affectedRecipesMoreLink.value = `/g/${userGroup.value}?${ids.map(id => `foods=${id}`).join("&")}`;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Merge Foods
|
// Merge Foods
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user