mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 06:23:34 -07:00
Recipe Context Menu
This commit is contained in:
parent
275b89a9eb
commit
1cb1a0552b
1 changed files with 310 additions and 361 deletions
|
@ -100,7 +100,7 @@
|
||||||
:open-on-hover="$vuetify.display.mdAndUp"
|
:open-on-hover="$vuetify.display.mdAndUp"
|
||||||
content-class="d-print-none"
|
content-class="d-print-none"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: activatorProps }">
|
||||||
<v-btn
|
<v-btn
|
||||||
icon
|
icon
|
||||||
:variant="fab ? 'flat' : undefined"
|
:variant="fab ? 'flat' : undefined"
|
||||||
|
@ -108,7 +108,7 @@
|
||||||
:size="fab ? 'small' : undefined"
|
:size="fab ? 'small' : undefined"
|
||||||
:color="fab ? 'info' : 'secondary'"
|
:color="fab ? 'info' : 'secondary'"
|
||||||
:fab="fab"
|
:fab="fab"
|
||||||
v-bind="props"
|
v-bind="activatorProps"
|
||||||
@click.prevent
|
@click.prevent
|
||||||
>
|
>
|
||||||
<v-icon
|
<v-icon
|
||||||
|
@ -150,7 +150,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import RecipeDialogAddToShoppingList from "./RecipeDialogAddToShoppingList.vue";
|
import RecipeDialogAddToShoppingList from "./RecipeDialogAddToShoppingList.vue";
|
||||||
import RecipeDialogPrintPreferences from "./RecipeDialogPrintPreferences.vue";
|
import RecipeDialogPrintPreferences from "./RecipeDialogPrintPreferences.vue";
|
||||||
import RecipeDialogShare from "./RecipeDialogShare.vue";
|
import RecipeDialogShare from "./RecipeDialogShare.vue";
|
||||||
|
@ -186,16 +186,22 @@ export interface ContextMenuItem {
|
||||||
isPublic: boolean;
|
isPublic: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
components: {
|
useItems?: ContextMenuIncludes;
|
||||||
RecipeDialogAddToShoppingList,
|
appendItems?: ContextMenuItem[];
|
||||||
RecipeDialogPrintPreferences,
|
leadingItems?: ContextMenuItem[];
|
||||||
RecipeDialogShare,
|
menuTop?: boolean;
|
||||||
},
|
fab?: boolean;
|
||||||
props: {
|
color?: string;
|
||||||
useItems: {
|
slug: string;
|
||||||
type: Object as () => ContextMenuIncludes,
|
menuIcon?: string | null;
|
||||||
default: () => ({
|
name: string;
|
||||||
|
recipe?: Recipe;
|
||||||
|
recipeId: string;
|
||||||
|
recipeScale?: number;
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
useItems: () => ({
|
||||||
delete: true,
|
delete: true,
|
||||||
edit: true,
|
edit: true,
|
||||||
download: true,
|
download: true,
|
||||||
|
@ -207,98 +213,61 @@ export default defineNuxtComponent({
|
||||||
share: true,
|
share: true,
|
||||||
recipeActions: true,
|
recipeActions: true,
|
||||||
}),
|
}),
|
||||||
},
|
appendItems: () => [],
|
||||||
// Append items are added at the end of the useItems list
|
leadingItems: () => [],
|
||||||
appendItems: {
|
menuTop: true,
|
||||||
type: Array as () => ContextMenuItem[],
|
fab: false,
|
||||||
default: () => [],
|
color: "primary",
|
||||||
},
|
menuIcon: null,
|
||||||
// Append items are added at the beginning of the useItems list
|
recipe: undefined,
|
||||||
leadingItems: {
|
recipeScale: 1,
|
||||||
type: Array as () => ContextMenuItem[],
|
});
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
menuTop: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
fab: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
type: String,
|
|
||||||
default: "primary",
|
|
||||||
},
|
|
||||||
slug: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
menuIcon: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
required: true,
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
recipe: {
|
|
||||||
type: Object as () => Recipe,
|
|
||||||
default: undefined,
|
|
||||||
},
|
|
||||||
recipeId: {
|
|
||||||
required: true,
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
recipeScale: {
|
|
||||||
type: Number,
|
|
||||||
default: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["delete"],
|
|
||||||
setup(props, context) {
|
|
||||||
const api = useUserApi();
|
|
||||||
|
|
||||||
const state = reactive({
|
const emit = defineEmits<{
|
||||||
printPreferencesDialog: false,
|
[key: string]: any;
|
||||||
shareDialog: false,
|
delete: [slug: string];
|
||||||
recipeDeleteDialog: false,
|
}>();
|
||||||
mealplannerDialog: false,
|
|
||||||
shoppingListDialog: false,
|
|
||||||
recipeDuplicateDialog: false,
|
|
||||||
recipeName: props.name,
|
|
||||||
loading: false,
|
|
||||||
menuItems: [] as ContextMenuItem[],
|
|
||||||
newMealdate: new Date(),
|
|
||||||
newMealType: "dinner" as PlanEntryType,
|
|
||||||
pickerMenu: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const newMealdateString = computed(() => {
|
const api = useUserApi();
|
||||||
|
|
||||||
|
const printPreferencesDialog = ref(false);
|
||||||
|
const shareDialog = ref(false);
|
||||||
|
const recipeDeleteDialog = ref(false);
|
||||||
|
const mealplannerDialog = ref(false);
|
||||||
|
const shoppingListDialog = ref(false);
|
||||||
|
const recipeDuplicateDialog = ref(false);
|
||||||
|
const recipeName = ref(props.name);
|
||||||
|
const loading = ref(false);
|
||||||
|
const menuItems = ref<ContextMenuItem[]>([]);
|
||||||
|
const newMealdate = ref(new Date());
|
||||||
|
const newMealType = ref<PlanEntryType>("dinner");
|
||||||
|
const pickerMenu = ref(false);
|
||||||
|
|
||||||
|
const newMealdateString = computed(() => {
|
||||||
// Format the date to YYYY-MM-DD in the same timezone as newMealdate
|
// Format the date to YYYY-MM-DD in the same timezone as newMealdate
|
||||||
const year = state.newMealdate.getFullYear();
|
const year = newMealdate.value.getFullYear();
|
||||||
const month = String(state.newMealdate.getMonth() + 1).padStart(2, "0");
|
const month = String(newMealdate.value.getMonth() + 1).padStart(2, "0");
|
||||||
const day = String(state.newMealdate.getDate()).padStart(2, "0");
|
const day = String(newMealdate.value.getDate()).padStart(2, "0");
|
||||||
return `${year}-${month}-${day}`;
|
return `${year}-${month}-${day}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const $auth = useMealieAuth();
|
const $auth = useMealieAuth();
|
||||||
const { $globals } = useNuxtApp();
|
const { $globals } = useNuxtApp();
|
||||||
const { household } = useHouseholdSelf();
|
const { household } = useHouseholdSelf();
|
||||||
const { isOwnGroup } = useLoggedInState();
|
const { isOwnGroup } = useLoggedInState();
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
||||||
|
|
||||||
const firstDayOfWeek = computed(() => {
|
const firstDayOfWeek = computed(() => {
|
||||||
return household.value?.preferences?.firstDayOfWeek || 0;
|
return household.value?.preferences?.firstDayOfWeek || 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Context Menu Setup
|
// Context Menu Setup
|
||||||
|
|
||||||
const defaultItems: { [key: string]: ContextMenuItem } = {
|
const defaultItems: { [key: string]: ContextMenuItem } = {
|
||||||
edit: {
|
edit: {
|
||||||
title: i18n.t("general.edit"),
|
title: i18n.t("general.edit"),
|
||||||
icon: $globals.icons.edit,
|
icon: $globals.icons.edit,
|
||||||
|
@ -362,35 +331,35 @@ export default defineNuxtComponent({
|
||||||
event: "share",
|
event: "share",
|
||||||
isPublic: false,
|
isPublic: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add leading and Appending Items
|
// Add leading and Appending Items
|
||||||
state.menuItems = [...state.menuItems, ...props.leadingItems, ...props.appendItems];
|
menuItems.value = [...menuItems.value, ...props.leadingItems, ...props.appendItems];
|
||||||
|
|
||||||
const icon = props.menuIcon || $globals.icons.dotsVertical;
|
const icon = props.menuIcon || $globals.icons.dotsVertical;
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Context Menu Event Handler
|
// Context Menu Event Handler
|
||||||
|
|
||||||
const shoppingLists = ref<ShoppingListSummary[]>();
|
const shoppingLists = ref<ShoppingListSummary[]>();
|
||||||
const recipeRef = ref<Recipe | undefined>(props.recipe);
|
const recipeRef = ref<Recipe | undefined>(props.recipe);
|
||||||
const recipeRefWithScale = computed(() =>
|
const recipeRefWithScale = computed(() =>
|
||||||
recipeRef.value ? { scale: props.recipeScale, ...recipeRef.value } : undefined,
|
recipeRef.value ? { scale: props.recipeScale, ...recipeRef.value } : undefined,
|
||||||
);
|
);
|
||||||
const isAdminAndNotOwner = computed(() => {
|
const isAdminAndNotOwner = computed(() => {
|
||||||
return (
|
return (
|
||||||
$auth.user.value?.admin
|
$auth.user.value?.admin
|
||||||
&& $auth.user.value?.id !== recipeRef.value?.userId
|
&& $auth.user.value?.id !== recipeRef.value?.userId
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const canDelete = computed(() => {
|
const canDelete = computed(() => {
|
||||||
const user = $auth.user.value;
|
const user = $auth.user.value;
|
||||||
const recipe = recipeRef.value;
|
const recipe = recipeRef.value;
|
||||||
return user && recipe && (user.admin || user.id === recipe.userId);
|
return user && recipe && (user.admin || user.id === recipe.userId);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get Default Menu Items Specified in Props
|
// Get Default Menu Items Specified in Props
|
||||||
for (const [key, value] of Object.entries(props.useItems)) {
|
for (const [key, value] of Object.entries(props.useItems)) {
|
||||||
if (!value) continue;
|
if (!value) continue;
|
||||||
|
|
||||||
// Skip delete if not allowed
|
// Skip delete if not allowed
|
||||||
|
@ -398,28 +367,28 @@ export default defineNuxtComponent({
|
||||||
|
|
||||||
const item = defaultItems[key];
|
const item = defaultItems[key];
|
||||||
if (item && (item.isPublic || isOwnGroup.value)) {
|
if (item && (item.isPublic || isOwnGroup.value)) {
|
||||||
state.menuItems.push(item);
|
menuItems.value.push(item);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function getShoppingLists() {
|
async function getShoppingLists() {
|
||||||
const { data } = await api.shopping.lists.getAll(1, -1, { orderBy: "name", orderDirection: "asc" });
|
const { data } = await api.shopping.lists.getAll(1, -1, { orderBy: "name", orderDirection: "asc" });
|
||||||
if (data) {
|
if (data) {
|
||||||
shoppingLists.value = data.items ?? [];
|
shoppingLists.value = data.items ?? [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshRecipe() {
|
async function refreshRecipe() {
|
||||||
const { data } = await api.recipes.getOne(props.slug);
|
const { data } = await api.recipes.getOne(props.slug);
|
||||||
if (data) {
|
if (data) {
|
||||||
recipeRef.value = data;
|
recipeRef.value = data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const groupRecipeActionsStore = useGroupRecipeActions();
|
const groupRecipeActionsStore = useGroupRecipeActions();
|
||||||
|
|
||||||
async function executeRecipeAction(action: GroupRecipeActionOut) {
|
async function executeRecipeAction(action: GroupRecipeActionOut) {
|
||||||
if (!props.recipe) return;
|
if (!props.recipe) return;
|
||||||
const response = await groupRecipeActionsStore.execute(action, props.recipe, props.recipeScale);
|
const response = await groupRecipeActionsStore.execute(action, props.recipe, props.recipeScale);
|
||||||
|
|
||||||
|
@ -431,30 +400,30 @@ export default defineNuxtComponent({
|
||||||
alert.error(i18n.t("events.something-went-wrong"));
|
alert.error(i18n.t("events.something-went-wrong"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteRecipe() {
|
async function deleteRecipe() {
|
||||||
const { data } = await api.recipes.deleteOne(props.slug);
|
const { data } = await api.recipes.deleteOne(props.slug);
|
||||||
if (data?.slug) {
|
if (data?.slug) {
|
||||||
router.push(`/g/${groupSlug.value}`);
|
router.push(`/g/${groupSlug.value}`);
|
||||||
}
|
}
|
||||||
context.emit("delete", props.slug);
|
emit("delete", props.slug);
|
||||||
}
|
}
|
||||||
|
|
||||||
const download = useDownloader();
|
const download = useDownloader();
|
||||||
|
|
||||||
async function handleDownloadEvent() {
|
async function handleDownloadEvent() {
|
||||||
const { data } = await api.recipes.getZipToken(props.slug);
|
const { data } = await api.recipes.getZipToken(props.slug);
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
download(api.recipes.getZipRedirectUrl(props.slug, data.token), `${props.slug}.zip`);
|
download(api.recipes.getZipRedirectUrl(props.slug, data.token), `${props.slug}.zip`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addRecipeToPlan() {
|
async function addRecipeToPlan() {
|
||||||
const { response } = await api.mealplans.createOne({
|
const { response } = await api.mealplans.createOne({
|
||||||
date: newMealdateString.value,
|
date: newMealdateString.value,
|
||||||
entryType: state.newMealType,
|
entryType: newMealType.value,
|
||||||
title: "",
|
title: "",
|
||||||
text: "",
|
text: "",
|
||||||
recipeId: props.recipeId,
|
recipeId: props.recipeId,
|
||||||
|
@ -466,34 +435,34 @@ export default defineNuxtComponent({
|
||||||
else {
|
else {
|
||||||
alert.error(i18n.t("recipe.failed-to-add-recipe-to-mealplan") as string);
|
alert.error(i18n.t("recipe.failed-to-add-recipe-to-mealplan") as string);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function duplicateRecipe() {
|
async function duplicateRecipe() {
|
||||||
const { data } = await api.recipes.duplicateOne(props.slug, state.recipeName);
|
const { data } = await api.recipes.duplicateOne(props.slug, recipeName.value);
|
||||||
if (data && data.slug) {
|
if (data && data.slug) {
|
||||||
router.push(`/g/${groupSlug.value}/r/${data.slug}`);
|
router.push(`/g/${groupSlug.value}/r/${data.slug}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Print is handled as an event in the parent component
|
// Note: Print is handled as an event in the parent component
|
||||||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||||
const eventHandlers: { [key: string]: () => void | Promise<any> } = {
|
const eventHandlers: { [key: string]: () => void | Promise<any> } = {
|
||||||
delete: () => {
|
delete: () => {
|
||||||
state.recipeDeleteDialog = true;
|
recipeDeleteDialog.value = true;
|
||||||
},
|
},
|
||||||
edit: () => router.push(`/g/${groupSlug.value}/r/${props.slug}` + "?edit=true"),
|
edit: () => router.push(`/g/${groupSlug.value}/r/${props.slug}` + "?edit=true"),
|
||||||
download: handleDownloadEvent,
|
download: handleDownloadEvent,
|
||||||
duplicate: () => {
|
duplicate: () => {
|
||||||
state.recipeDuplicateDialog = true;
|
recipeDuplicateDialog.value = true;
|
||||||
},
|
},
|
||||||
mealplanner: () => {
|
mealplanner: () => {
|
||||||
state.mealplannerDialog = true;
|
mealplannerDialog.value = true;
|
||||||
},
|
},
|
||||||
printPreferences: async () => {
|
printPreferences: async () => {
|
||||||
if (!recipeRef.value) {
|
if (!recipeRef.value) {
|
||||||
await refreshRecipe();
|
await refreshRecipe();
|
||||||
}
|
}
|
||||||
state.printPreferencesDialog = true;
|
printPreferencesDialog.value = true;
|
||||||
},
|
},
|
||||||
shoppingList: () => {
|
shoppingList: () => {
|
||||||
const promises: Promise<void>[] = [getShoppingLists()];
|
const promises: Promise<void>[] = [getShoppingLists()];
|
||||||
|
@ -502,47 +471,27 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.allSettled(promises).then(() => {
|
Promise.allSettled(promises).then(() => {
|
||||||
state.shoppingListDialog = true;
|
shoppingListDialog.value = true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
share: () => {
|
share: () => {
|
||||||
state.shareDialog = true;
|
shareDialog.value = true;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function contextMenuEventHandler(eventKey: string) {
|
function contextMenuEventHandler(eventKey: string) {
|
||||||
const handler = eventHandlers[eventKey];
|
const handler = eventHandlers[eventKey];
|
||||||
|
|
||||||
if (handler && typeof handler === "function") {
|
if (handler && typeof handler === "function") {
|
||||||
handler();
|
handler();
|
||||||
state.loading = false;
|
loading.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
context.emit(eventKey);
|
emit(eventKey);
|
||||||
state.loading = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const planTypeOptions = usePlanTypeOptions();
|
const planTypeOptions = usePlanTypeOptions();
|
||||||
|
const recipeActions = groupRecipeActionsStore.recipeActions;
|
||||||
return {
|
|
||||||
...toRefs(state),
|
|
||||||
newMealdateString,
|
|
||||||
recipeRef,
|
|
||||||
recipeRefWithScale,
|
|
||||||
executeRecipeAction,
|
|
||||||
recipeActions: groupRecipeActionsStore.recipeActions,
|
|
||||||
shoppingLists,
|
|
||||||
duplicateRecipe,
|
|
||||||
contextMenuEventHandler,
|
|
||||||
deleteRecipe,
|
|
||||||
addRecipeToPlan,
|
|
||||||
icon,
|
|
||||||
planTypeOptions,
|
|
||||||
firstDayOfWeek,
|
|
||||||
isAdminAndNotOwner,
|
|
||||||
canDelete,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue