Skip to content

Commit 49b4ee2

Browse files
authored
Merge branch 'mealie-next' into manage-data-improve-delete-prompt
2 parents 3870819 + 0775072 commit 49b4ee2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1385
-478
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<template>
2+
<div class="text-center">
3+
<RecipeDialogAddToShoppingList
4+
v-if="shoppingLists"
5+
v-model="shoppingListDialog"
6+
:recipes="recipesWithScales"
7+
:shopping-lists="shoppingLists"
8+
/>
9+
<v-menu
10+
offset-y
11+
left
12+
:bottom="!menuTop"
13+
:nudge-bottom="!menuTop ? '5' : '0'"
14+
:top="menuTop"
15+
:nudge-top="menuTop ? '5' : '0'"
16+
allow-overflow
17+
close-delay="125"
18+
:open-on-hover="$vuetify.breakpoint.mdAndUp"
19+
content-class="d-print-none"
20+
>
21+
<template #activator="{ on, attrs }">
22+
<v-btn :fab="fab" :small="fab" :color="color" :icon="!fab" dark v-bind="attrs" v-on="on" @click.prevent>
23+
<v-icon>{{ icon }}</v-icon>
24+
</v-btn>
25+
</template>
26+
<v-list dense>
27+
<v-list-item v-for="(item, index) in menuItems" :key="index" @click="contextMenuEventHandler(item.event)">
28+
<v-list-item-icon>
29+
<v-icon :color="item.color"> {{ item.icon }} </v-icon>
30+
</v-list-item-icon>
31+
<v-list-item-title>{{ item.title }}</v-list-item-title>
32+
</v-list-item>
33+
</v-list>
34+
</v-menu>
35+
</div>
36+
</template>
37+
38+
<script lang="ts">
39+
import { computed, defineComponent, reactive, ref, toRefs, useContext } from "@nuxtjs/composition-api";
40+
import { Recipe } from "~/lib/api/types/recipe";
41+
import RecipeDialogAddToShoppingList from "~/components/Domain/Recipe/RecipeDialogAddToShoppingList.vue";
42+
import { ShoppingListSummary } from "~/lib/api/types/group";
43+
import { useUserApi } from "~/composables/api";
44+
45+
export interface ContextMenuItem {
46+
title: string;
47+
icon: string;
48+
color: string | undefined;
49+
event: string;
50+
isPublic: boolean;
51+
}
52+
53+
export default defineComponent({
54+
components: {
55+
RecipeDialogAddToShoppingList,
56+
},
57+
props: {
58+
recipes: {
59+
type: Array as () => Recipe[],
60+
default: () => [],
61+
},
62+
menuTop: {
63+
type: Boolean,
64+
default: true,
65+
},
66+
fab: {
67+
type: Boolean,
68+
default: false,
69+
},
70+
color: {
71+
type: String,
72+
default: "primary",
73+
},
74+
menuIcon: {
75+
type: String,
76+
default: null,
77+
},
78+
},
79+
setup(props, context) {
80+
const { $globals, i18n } = useContext();
81+
const api = useUserApi();
82+
83+
const state = reactive({
84+
loading: false,
85+
shoppingListDialog: false,
86+
menuItems: [
87+
{
88+
title: i18n.tc("recipe.add-to-list"),
89+
icon: $globals.icons.cartCheck,
90+
color: undefined,
91+
event: "shoppingList",
92+
isPublic: false,
93+
},
94+
],
95+
});
96+
97+
const icon = props.menuIcon || $globals.icons.dotsVertical;
98+
99+
const shoppingLists = ref<ShoppingListSummary[]>();
100+
const recipesWithScales = computed(() => {
101+
return props.recipes.map((recipe) => {
102+
return {
103+
scale: 1,
104+
...recipe,
105+
};
106+
})
107+
})
108+
109+
async function getShoppingLists() {
110+
const { data } = await api.shopping.lists.getAll();
111+
if (data) {
112+
shoppingLists.value = data.items ?? [];
113+
}
114+
}
115+
116+
const eventHandlers: { [key: string]: () => void | Promise<any> } = {
117+
shoppingList: () => {
118+
getShoppingLists();
119+
state.shoppingListDialog = true;
120+
},
121+
};
122+
123+
function contextMenuEventHandler(eventKey: string) {
124+
const handler = eventHandlers[eventKey];
125+
126+
if (handler && typeof handler === "function") {
127+
handler();
128+
state.loading = false;
129+
return;
130+
}
131+
132+
context.emit(eventKey);
133+
state.loading = false;
134+
}
135+
136+
return {
137+
...toRefs(state),
138+
contextMenuEventHandler,
139+
icon,
140+
recipesWithScales,
141+
shoppingLists,
142+
}
143+
},
144+
})
145+
</script>

frontend/components/Domain/Recipe/RecipeContextMenu.vue

Lines changed: 17 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -69,77 +69,12 @@
6969
></v-select>
7070
</v-card-text>
7171
</BaseDialog>
72-
<BaseDialog v-model="shoppingListDialog" :title="$t('recipe.add-to-list')" :icon="$globals.icons.cartCheck">
73-
<v-card-text>
74-
<v-card
75-
v-for="list in shoppingLists"
76-
:key="list.id"
77-
hover
78-
class="my-2 left-border"
79-
@click="openShoppingListIngredientDialog(list)"
80-
>
81-
<v-card-title class="py-2">
82-
{{ list.name }}
83-
</v-card-title>
84-
</v-card>
85-
</v-card-text>
86-
</BaseDialog>
87-
<BaseDialog
88-
v-model="shoppingListIngredientDialog"
89-
:title="selectedShoppingList ? selectedShoppingList.name : $t('recipe.add-to-list')"
90-
:icon="$globals.icons.cartCheck"
91-
width="70%"
92-
:submit-text="$tc('recipe.add-to-list')"
93-
@submit="addRecipeToList()"
94-
>
95-
<v-card
96-
elevation="0"
97-
height="fit-content"
98-
max-height="60vh"
99-
width="100%"
100-
:class="$vuetify.breakpoint.smAndDown ? '' : 'ingredient-grid'"
101-
:style="$vuetify.breakpoint.smAndDown ? '' : { gridTemplateRows: `repeat(${Math.ceil(recipeIngredients.length / 2)}, min-content)` }"
102-
style="overflow-y: auto"
103-
>
104-
<v-list-item
105-
v-for="(ingredientData, i) in recipeIngredients"
106-
:key="'ingredient' + i"
107-
dense
108-
@click="recipeIngredients[i].checked = !recipeIngredients[i].checked"
109-
>
110-
<v-checkbox
111-
hide-details
112-
:input-value="ingredientData.checked"
113-
class="pt-0 my-auto py-auto"
114-
color="secondary"
115-
/>
116-
<v-list-item-content :key="ingredientData.ingredient.quantity">
117-
<RecipeIngredientListItem
118-
:ingredient="ingredientData.ingredient"
119-
:disable-amount="ingredientData.disableAmount"
120-
:scale="recipeScale" />
121-
</v-list-item-content>
122-
</v-list-item>
123-
</v-card>
124-
<div class="d-flex justify-end mb-4 mt-2">
125-
<BaseButtonGroup
126-
:buttons="[
127-
{
128-
icon: $globals.icons.checkboxBlankOutline,
129-
text: $tc('shopping-list.uncheck-all-items'),
130-
event: 'uncheck',
131-
},
132-
{
133-
icon: $globals.icons.checkboxOutline,
134-
text: $tc('shopping-list.check-all-items'),
135-
event: 'check',
136-
},
137-
]"
138-
@uncheck="bulkCheckIngredients(false)"
139-
@check="bulkCheckIngredients(true)"
140-
/>
141-
</div>
142-
</BaseDialog>
72+
<RecipeDialogAddToShoppingList
73+
v-if="shoppingLists && recipeRefWithScale"
74+
v-model="shoppingListDialog"
75+
:recipes="[recipeRefWithScale]"
76+
:shopping-lists="shoppingLists"
77+
/>
14378
<v-menu
14479
offset-y
14580
left
@@ -171,14 +106,14 @@
171106

172107
<script lang="ts">
173108
import { computed, defineComponent, reactive, toRefs, useContext, useRoute, useRouter, ref } from "@nuxtjs/composition-api";
174-
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
109+
import RecipeDialogAddToShoppingList from "./RecipeDialogAddToShoppingList.vue";
175110
import RecipeDialogPrintPreferences from "./RecipeDialogPrintPreferences.vue";
176111
import RecipeDialogShare from "./RecipeDialogShare.vue";
177112
import { useLoggedInState } from "~/composables/use-logged-in-state";
178113
import { useUserApi } from "~/composables/api";
179114
import { alert } from "~/composables/use-toast";
180115
import { usePlanTypeOptions } from "~/composables/use-group-mealplan";
181-
import { Recipe, RecipeIngredient } from "~/lib/api/types/recipe";
116+
import { Recipe } from "~/lib/api/types/recipe";
182117
import { ShoppingListSummary } from "~/lib/api/types/group";
183118
import { PlanEntryType } from "~/lib/api/types/meal-plan";
184119
import { useAxiosDownloader } from "~/composables/api/use-axios-download";
@@ -204,9 +139,9 @@ export interface ContextMenuItem {
204139
205140
export default defineComponent({
206141
components: {
142+
RecipeDialogAddToShoppingList,
207143
RecipeDialogPrintPreferences,
208144
RecipeDialogShare,
209-
RecipeIngredientListItem
210145
},
211146
props: {
212147
useItems: {
@@ -279,7 +214,6 @@ export default defineComponent({
279214
recipeDeleteDialog: false,
280215
mealplannerDialog: false,
281216
shoppingListDialog: false,
282-
shoppingListIngredientDialog: false,
283217
recipeDuplicateDialog: false,
284218
recipeName: props.name,
285219
loading: false,
@@ -374,7 +308,7 @@ export default defineComponent({
374308
}
375309
}
376310
377-
// Add leading and Apppending Items
311+
// Add leading and Appending Items
378312
state.menuItems = [...state.menuItems, ...props.leadingItems, ...props.appendItems];
379313
380314
const icon = props.menuIcon || $globals.icons.dotsVertical;
@@ -383,9 +317,8 @@ export default defineComponent({
383317
// Context Menu Event Handler
384318
385319
const shoppingLists = ref<ShoppingListSummary[]>();
386-
const selectedShoppingList = ref<ShoppingListSummary>();
387320
const recipeRef = ref<Recipe>(props.recipe);
388-
const recipeIngredients = ref<{ checked: boolean; ingredient: RecipeIngredient, disableAmount: boolean }[]>([]);
321+
const recipeRefWithScale = computed(() => recipeRef.value ? { scale: props.recipeScale, ...recipeRef.value } : undefined);
389322
390323
async function getShoppingLists() {
391324
const { data } = await api.shopping.lists.getAll();
@@ -401,61 +334,6 @@ export default defineComponent({
401334
}
402335
}
403336
404-
async function openShoppingListIngredientDialog(list: ShoppingListSummary) {
405-
selectedShoppingList.value = list;
406-
if (!recipeRef.value) {
407-
await refreshRecipe();
408-
}
409-
410-
if (recipeRef.value?.recipeIngredient) {
411-
recipeIngredients.value = recipeRef.value.recipeIngredient.map((ingredient) => {
412-
return {
413-
checked: true,
414-
ingredient,
415-
disableAmount: recipeRef.value.settings?.disableAmount || false
416-
};
417-
});
418-
}
419-
420-
state.shoppingListDialog = false;
421-
state.shoppingListIngredientDialog = true;
422-
}
423-
424-
function bulkCheckIngredients(value = true) {
425-
recipeIngredients.value.forEach((data) => {
426-
data.checked = value;
427-
});
428-
}
429-
430-
async function addRecipeToList() {
431-
if (!selectedShoppingList.value) {
432-
return;
433-
}
434-
435-
const ingredients: RecipeIngredient[] = [];
436-
recipeIngredients.value.forEach((data) => {
437-
if (data.checked) {
438-
ingredients.push(data.ingredient);
439-
}
440-
});
441-
442-
if (!ingredients.length) {
443-
return;
444-
}
445-
446-
const { data } = await api.shopping.lists.addRecipe(
447-
selectedShoppingList.value.id,
448-
props.recipeId,
449-
props.recipeScale,
450-
ingredients
451-
);
452-
if (data) {
453-
alert.success(i18n.t("recipe.recipe-added-to-list") as string);
454-
state.shoppingListDialog = false;
455-
state.shoppingListIngredientDialog = false;
456-
}
457-
}
458-
459337
const router = useRouter();
460338
461339
async function deleteRecipe() {
@@ -516,10 +394,12 @@ export default defineComponent({
516394
state.printPreferencesDialog = true;
517395
},
518396
shoppingList: () => {
519-
getShoppingLists();
397+
const promises: Promise<void>[] = [getShoppingLists()];
398+
if (!recipeRef.value) {
399+
promises.push(refreshRecipe());
400+
}
520401
521-
state.shoppingListDialog = true;
522-
state.shoppingListIngredientDialog = false;
402+
Promise.allSettled(promises).then(() => { state.shoppingListDialog = true });
523403
},
524404
share: () => {
525405
state.shareDialog = true;
@@ -544,28 +424,15 @@ export default defineComponent({
544424
return {
545425
...toRefs(state),
546426
recipeRef,
427+
recipeRefWithScale,
547428
shoppingLists,
548-
selectedShoppingList,
549-
openShoppingListIngredientDialog,
550-
addRecipeToList,
551-
bulkCheckIngredients,
552429
duplicateRecipe,
553430
contextMenuEventHandler,
554431
deleteRecipe,
555432
addRecipeToPlan,
556433
icon,
557434
planTypeOptions,
558-
recipeIngredients,
559435
};
560436
},
561437
});
562438
</script>
563-
564-
<style scoped lang="css">
565-
.ingredient-grid {
566-
display: grid;
567-
grid-auto-flow: column;
568-
grid-template-columns: 1fr 1fr;
569-
grid-gap: 0.5rem;
570-
}
571-
</style>

0 commit comments

Comments
 (0)