mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 02:03:35 -04:00 
			
		
		
		
	feat(frontend): ✨ Rewrite context menu in TS and add 'add to mealplan' context menu action (#786)
* make entry for NLP model `setup-model` * add comments * feat(frontend): ✨ Rewrite context menu in TS and add 'add to mealplan' options * add note to changelog Co-authored-by: Hayden K <hay-kot@pm.me>
This commit is contained in:
		| @@ -37,6 +37,7 @@ | |||||||
| - Meal plans have been completely redesigned to use a calendar approach so you'll be able to see what meals you have planned in a more traditional view | - Meal plans have been completely redesigned to use a calendar approach so you'll be able to see what meals you have planned in a more traditional view | ||||||
| - Drag and Drop meals between days | - Drag and Drop meals between days | ||||||
| - Add Recipes or Notes to a specific day | - Add Recipes or Notes to a specific day | ||||||
|  | - New context menu action for recipes to add a recipe to a specific day on the meal-plan | ||||||
|  |  | ||||||
| ### 🥙 Recipes | ### 🥙 Recipes | ||||||
|  |  | ||||||
|   | |||||||
| @@ -48,6 +48,15 @@ | |||||||
|         fab |         fab | ||||||
|         color="info" |         color="info" | ||||||
|         :card-menu="false" |         :card-menu="false" | ||||||
|  |         :recipe-id="recipeId" | ||||||
|  |         :use-items="{ | ||||||
|  |           delete: false, | ||||||
|  |           edit: false, | ||||||
|  |           download: true, | ||||||
|  |           mealplanner: true, | ||||||
|  |           print: true, | ||||||
|  |           share: true, | ||||||
|  |         }" | ||||||
|         @print="$emit('print')" |         @print="$emit('print')" | ||||||
|       /> |       /> | ||||||
|     </div> |     </div> | ||||||
| @@ -96,6 +105,10 @@ export default { | |||||||
|       type: Boolean, |       type: Boolean, | ||||||
|       default: false, |       default: false, | ||||||
|     }, |     }, | ||||||
|  |     recipeId: { | ||||||
|  |       required: true, | ||||||
|  |       type: Number, | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|   | |||||||
| @@ -28,7 +28,20 @@ | |||||||
|           <RecipeRating :value="rating" :name="name" :slug="slug" :small="true" /> |           <RecipeRating :value="rating" :name="name" :slug="slug" :small="true" /> | ||||||
|           <v-spacer></v-spacer> |           <v-spacer></v-spacer> | ||||||
|           <RecipeChips :truncate="true" :items="tags" :title="false" :limit="2" :small="true" :is-category="false" /> |           <RecipeChips :truncate="true" :items="tags" :title="false" :limit="2" :small="true" :is-category="false" /> | ||||||
|           <RecipeContextMenu :slug="slug" :name="name" @deleted="$emit('deleted', slug)" /> |           <RecipeContextMenu | ||||||
|  |             :slug="slug" | ||||||
|  |             :name="name" | ||||||
|  |             :recipe-id="recipeId" | ||||||
|  |             :use-items="{ | ||||||
|  |               delete: true, | ||||||
|  |               edit: true, | ||||||
|  |               download: true, | ||||||
|  |               mealplanner: true, | ||||||
|  |               print: false, | ||||||
|  |               share: true, | ||||||
|  |             }" | ||||||
|  |             @delete="$emit('delete', slug)" | ||||||
|  |           /> | ||||||
|         </v-card-actions> |         </v-card-actions> | ||||||
|         <slot></slot> |         <slot></slot> | ||||||
|       </v-card> |       </v-card> | ||||||
| @@ -75,6 +88,10 @@ export default { | |||||||
|       type: Array, |       type: Array, | ||||||
|       default: () => [], |       default: () => [], | ||||||
|     }, |     }, | ||||||
|  |     recipeId: { | ||||||
|  |       required: true, | ||||||
|  |       type: Number, | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|   | |||||||
| @@ -38,7 +38,21 @@ | |||||||
|                   :value="rating" |                   :value="rating" | ||||||
|                 ></v-rating> |                 ></v-rating> | ||||||
|                 <v-spacer></v-spacer> |                 <v-spacer></v-spacer> | ||||||
|                 <RecipeContextMenu :slug="slug" :menu-icon="$globals.icons.dotsHorizontal" :name="name" /> |                 <RecipeContextMenu | ||||||
|  |                   :slug="slug" | ||||||
|  |                   :menu-icon="$globals.icons.dotsHorizontal" | ||||||
|  |                   :name="name" | ||||||
|  |                   :recipe-id="recipeId" | ||||||
|  |                   :use-items="{ | ||||||
|  |                     delete: true, | ||||||
|  |                     edit: true, | ||||||
|  |                     download: true, | ||||||
|  |                     mealplanner: true, | ||||||
|  |                     print: false, | ||||||
|  |                     share: true, | ||||||
|  |                   }" | ||||||
|  |                   @deleted="$emit('delete', slug)" | ||||||
|  |                 /> | ||||||
|               </slot> |               </slot> | ||||||
|             </div> |             </div> | ||||||
|           </v-list-item-content> |           </v-list-item-content> | ||||||
| @@ -74,19 +88,19 @@ export default defineComponent({ | |||||||
|     }, |     }, | ||||||
|     rating: { |     rating: { | ||||||
|       type: Number, |       type: Number, | ||||||
|       required: true, |       default: 0, | ||||||
|     }, |     }, | ||||||
|     image: { |     image: { | ||||||
|       type: String, |       type: [String, null], | ||||||
|       required: true, |       default: "", | ||||||
|     }, |     }, | ||||||
|     route: { |     route: { | ||||||
|       type: Boolean, |       type: Boolean, | ||||||
|       default: true, |       default: true, | ||||||
|     }, |     }, | ||||||
|     tags: { |     recipeId: { | ||||||
|       type: Boolean, |       type: Number, | ||||||
|       default: true, |       required: true, | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   setup() { |   setup() { | ||||||
|   | |||||||
| @@ -66,7 +66,8 @@ | |||||||
|               :rating="recipe.rating" |               :rating="recipe.rating" | ||||||
|               :image="recipe.image" |               :image="recipe.image" | ||||||
|               :tags="recipe.tags" |               :tags="recipe.tags" | ||||||
|               @deleted="$emit('deleted', $event)" |               :recipe-id="recipe.id" | ||||||
|  |               @delete="$emit('delete', recipe.slug)" | ||||||
|             /> |             /> | ||||||
|           </v-lazy> |           </v-lazy> | ||||||
|         </v-col> |         </v-col> | ||||||
| @@ -89,6 +90,8 @@ | |||||||
|               :rating="recipe.rating" |               :rating="recipe.rating" | ||||||
|               :image="recipe.image" |               :image="recipe.image" | ||||||
|               :tags="recipe.tags" |               :tags="recipe.tags" | ||||||
|  |               :recipe-id="recipe.id" | ||||||
|  |               @delete="$emit('delete', recipe.slug)" | ||||||
|             /> |             /> | ||||||
|           </v-lazy> |           </v-lazy> | ||||||
|         </v-col> |         </v-col> | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="text-center"> |   <div class="text-center"> | ||||||
|     <BaseDialog |     <BaseDialog | ||||||
|       ref="confirmDelete" |       ref="domConfirmDelete" | ||||||
|       :title="$t('recipe.delete-recipe')" |       :title="$t('recipe.delete-recipe')" | ||||||
|       color="error" |       color="error" | ||||||
|       :icon="$globals.icons.alertCircle" |       :icon="$globals.icons.alertCircle" | ||||||
| @@ -11,6 +11,38 @@ | |||||||
|         {{ $t("recipe.delete-confirmation") }} |         {{ $t("recipe.delete-confirmation") }} | ||||||
|       </v-card-text> |       </v-card-text> | ||||||
|     </BaseDialog> |     </BaseDialog> | ||||||
|  |     <BaseDialog | ||||||
|  |       ref="domMealplanDialog" | ||||||
|  |       title="Add Recipe to Mealplan" | ||||||
|  |       color="primary" | ||||||
|  |       :icon="$globals.icons.calendar" | ||||||
|  |       @confirm="addRecipeToPlan()" | ||||||
|  |     > | ||||||
|  |       <v-card-text> | ||||||
|  |         <v-menu | ||||||
|  |           v-model="pickerMenu" | ||||||
|  |           :close-on-content-click="false" | ||||||
|  |           transition="scale-transition" | ||||||
|  |           offset-y | ||||||
|  |           max-width="290px" | ||||||
|  |           min-width="auto" | ||||||
|  |         > | ||||||
|  |           <template #activator="{ on, attrs }"> | ||||||
|  |             <v-text-field | ||||||
|  |               v-model="newMealdate" | ||||||
|  |               label="Date" | ||||||
|  |               hint="MM/DD/YYYY format" | ||||||
|  |               persistent-hint | ||||||
|  |               :prepend-icon="$globals.icons.calendar" | ||||||
|  |               v-bind="attrs" | ||||||
|  |               readonly | ||||||
|  |               v-on="on" | ||||||
|  |             ></v-text-field> | ||||||
|  |           </template> | ||||||
|  |           <v-date-picker v-model="newMealdate" no-title @input="pickerMenu = false"></v-date-picker> | ||||||
|  |         </v-menu> | ||||||
|  |       </v-card-text> | ||||||
|  |     </BaseDialog> | ||||||
|     <v-menu |     <v-menu | ||||||
|       offset-y |       offset-y | ||||||
|       left |       left | ||||||
| @@ -25,11 +57,11 @@ | |||||||
|     > |     > | ||||||
|       <template #activator="{ on, attrs }"> |       <template #activator="{ on, attrs }"> | ||||||
|         <v-btn :fab="fab" :small="fab" :color="color" :icon="!fab" dark v-bind="attrs" v-on="on" @click.prevent> |         <v-btn :fab="fab" :small="fab" :color="color" :icon="!fab" dark v-bind="attrs" v-on="on" @click.prevent> | ||||||
|           <v-icon>{{ effMenuIcon }}</v-icon> |           <v-icon>{{ icon }}</v-icon> | ||||||
|         </v-btn> |         </v-btn> | ||||||
|       </template> |       </template> | ||||||
|       <v-list dense> |       <v-list dense> | ||||||
|         <v-list-item v-for="(item, index) in displayedMenu" :key="index" @click="menuAction(item.action)"> |         <v-list-item v-for="(item, index) in menuItems" :key="index" @click="contextMenuEventHandler(item.event)"> | ||||||
|           <v-list-item-icon> |           <v-list-item-icon> | ||||||
|             <v-icon :color="item.color" v-text="item.icon"></v-icon> |             <v-icon :color="item.color" v-text="item.icon"></v-icon> | ||||||
|           </v-list-item-icon> |           </v-list-item-icon> | ||||||
| @@ -40,20 +72,55 @@ | |||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script lang="ts"> | ||||||
| import { defineComponent, ref } from "@nuxtjs/composition-api"; | import { defineComponent, reactive, ref, toRefs, useContext, useRouter } from "@nuxtjs/composition-api"; | ||||||
|  | import { useClipboard, useShare } from "@vueuse/core"; | ||||||
| import { useApiSingleton } from "~/composables/use-api"; | import { useApiSingleton } from "~/composables/use-api"; | ||||||
| import { alert } from "~/composables/use-toast"; | import { alert } from "~/composables/use-toast"; | ||||||
|  |  | ||||||
|  | export interface ContextMenuIncludes { | ||||||
|  |   delete: boolean; | ||||||
|  |   edit: boolean; | ||||||
|  |   download: boolean; | ||||||
|  |   mealplanner: boolean; | ||||||
|  |   print: boolean; | ||||||
|  |   share: boolean; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface ContextMenuItem { | ||||||
|  |   title: string; | ||||||
|  |   icon: string; | ||||||
|  |   color: string; | ||||||
|  |   event: string; | ||||||
|  | } | ||||||
|  |  | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
|   props: { |   props: { | ||||||
|  |     useItems: { | ||||||
|  |       type: Object as () => ContextMenuIncludes, | ||||||
|  |       default: () => ({ | ||||||
|  |         delete: true, | ||||||
|  |         edit: true, | ||||||
|  |         download: true, | ||||||
|  |         mealplanner: true, | ||||||
|  |         print: true, | ||||||
|  |         share: true, | ||||||
|  |       }), | ||||||
|  |     }, | ||||||
|  |     // Append items are added at the end of the useItems list | ||||||
|  |     appendItems: { | ||||||
|  |       type: Array as () => ContextMenuItem[], | ||||||
|  |       default: () => [], | ||||||
|  |     }, | ||||||
|  |     // Append items are added at the beginning of the useItems list | ||||||
|  |     leadingItems: { | ||||||
|  |       type: Array as () => ContextMenuItem[], | ||||||
|  |       default: () => [], | ||||||
|  |     }, | ||||||
|     menuTop: { |     menuTop: { | ||||||
|       type: Boolean, |       type: Boolean, | ||||||
|       default: true, |       default: true, | ||||||
|     }, |     }, | ||||||
|     showPrint: { |  | ||||||
|       type: Boolean, |  | ||||||
|       default: false, |  | ||||||
|     }, |  | ||||||
|     fab: { |     fab: { | ||||||
|       type: Boolean, |       type: Boolean, | ||||||
|       default: false, |       default: false, | ||||||
| @@ -74,146 +141,177 @@ export default defineComponent({ | |||||||
|       required: true, |       required: true, | ||||||
|       type: String, |       type: String, | ||||||
|     }, |     }, | ||||||
|     cardMenu: { |     recipeId: { | ||||||
|       type: Boolean, |       required: true, | ||||||
|       default: true, |       type: Number, | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   setup() { |   setup(props, context) { | ||||||
|     const api = useApiSingleton(); |     const api = useApiSingleton(); | ||||||
|     const confirmDelete = ref(null); |  | ||||||
|     return { api, confirmDelete }; |  | ||||||
|   }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       loading: true, |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
|   computed: { |  | ||||||
|     effMenuIcon() { |  | ||||||
|       return this.menuIcon ? this.menuIcon : this.$globals.icons.dotsVertical; |  | ||||||
|     }, |  | ||||||
|     loggedIn() { |  | ||||||
|       return this.$auth.loggedIn; |  | ||||||
|     }, |  | ||||||
|     baseURL() { |  | ||||||
|       return window.location.origin; |  | ||||||
|     }, |  | ||||||
|     recipeURL() { |  | ||||||
|       return `${this.baseURL}/recipe/${this.slug}`; |  | ||||||
|     }, |  | ||||||
|     printerMenu() { |  | ||||||
|       return { |  | ||||||
|         title: this.$t("general.print"), |  | ||||||
|         icon: this.$globals.icons.printer, |  | ||||||
|         color: "accent", |  | ||||||
|         action: "print", |  | ||||||
|       }; |  | ||||||
|     }, |  | ||||||
|     defaultMenu() { |  | ||||||
|       return [ |  | ||||||
|         { |  | ||||||
|           title: this.$t("general.share"), |  | ||||||
|           icon: this.$globals.icons.shareVariant, |  | ||||||
|           color: "accent", |  | ||||||
|           action: "share", |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           title: this.$t("general.download"), |  | ||||||
|           icon: this.$globals.icons.download, |  | ||||||
|           color: "accent", |  | ||||||
|           action: "download", |  | ||||||
|         }, |  | ||||||
|       ]; |  | ||||||
|     }, |  | ||||||
|     userMenu() { |  | ||||||
|       return [ |  | ||||||
|         { |  | ||||||
|           title: this.$t("general.delete"), |  | ||||||
|           icon: this.$globals.icons.delete, |  | ||||||
|           color: "error", |  | ||||||
|           action: "delete", |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           title: this.$t("general.edit"), |  | ||||||
|           icon: this.$globals.icons.edit, |  | ||||||
|           color: "accent", |  | ||||||
|           action: "edit", |  | ||||||
|         }, |  | ||||||
|       ]; |  | ||||||
|     }, |  | ||||||
|     displayedMenu() { |  | ||||||
|       let menu = this.defaultMenu; |  | ||||||
|       if (this.loggedIn && this.cardMenu) { |  | ||||||
|         menu = [...this.userMenu, ...menu]; |  | ||||||
|       } |  | ||||||
|       if (this.showPrint) { |  | ||||||
|         menu = [this.printerMenu, ...menu]; |  | ||||||
|       } |  | ||||||
|       return menu; |  | ||||||
|     }, |  | ||||||
|     recipeText() { |  | ||||||
|       return this.$t("recipe.share-recipe-message", [this.name]); |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     async menuAction(action) { |  | ||||||
|       this.loading = true; |  | ||||||
|  |  | ||||||
|       switch (action) { |     const state = reactive({ | ||||||
|         case "delete": |       loading: false, | ||||||
|           this.confirmDelete.open(); |       menuItems: [] as ContextMenuItem[], | ||||||
|           break; |       newMealdate: "", | ||||||
|         case "share": |       pickerMenu: false, | ||||||
|           if (navigator.share) { |  | ||||||
|             navigator |  | ||||||
|               .share({ |  | ||||||
|                 title: this.name, |  | ||||||
|                 text: this.recipeText, |  | ||||||
|                 url: this.recipeURL, |  | ||||||
|               }) |  | ||||||
|               .then(() => console.log("Successful share")) |  | ||||||
|               .catch((error) => { |  | ||||||
|                 console.log("WebShareAPI not supported", error); |  | ||||||
|                 this.updateClipboard(); |  | ||||||
|     }); |     }); | ||||||
|           } else this.updateClipboard(); |  | ||||||
|           break; |     // @ts-ignore | ||||||
|         case "edit": |     const { i18n, $globals } = useContext(); | ||||||
|           this.$router.push(`/recipe/${this.slug}` + "?edit=true"); |  | ||||||
|           break; |     // =========================================================================== | ||||||
|         case "print": |     // Context Menu Setup | ||||||
|           this.$emit("print"); |  | ||||||
|           break; |     const defaultItems: { [key: string]: ContextMenuItem } = { | ||||||
|         case "download": |       edit: { | ||||||
|           // TODO: Refacor this entire component to not suck so much |         title: i18n.t("general.edit") as string, | ||||||
|           // eslint-disable-next-line |         icon: $globals.icons.edit, | ||||||
|           const { data } = await this.api.recipes.getZipToken(this.slug); |         color: "primary", | ||||||
|           window.open(this.api.recipes.getZipRedirectUrl(this.slug, data.token)); |         event: "edit", | ||||||
|           break; |       }, | ||||||
|         default: |       delete: { | ||||||
|           break; |         title: i18n.t("general.delete") as string, | ||||||
|  |         icon: $globals.icons.delete, | ||||||
|  |         color: "error", | ||||||
|  |         event: "delete", | ||||||
|  |       }, | ||||||
|  |       download: { | ||||||
|  |         title: i18n.t("general.download") as string, | ||||||
|  |         icon: $globals.icons.download, | ||||||
|  |         color: "primary", | ||||||
|  |         event: "download", | ||||||
|  |       }, | ||||||
|  |       mealplanner: { | ||||||
|  |         title: "Add to Plan", | ||||||
|  |         icon: $globals.icons.calendar, | ||||||
|  |         color: "primary", | ||||||
|  |         event: "mealplanner", | ||||||
|  |       }, | ||||||
|  |       print: { | ||||||
|  |         title: i18n.t("general.print") as string, | ||||||
|  |         icon: $globals.icons.printer, | ||||||
|  |         color: "primary", | ||||||
|  |         event: "print", | ||||||
|  |       }, | ||||||
|  |       share: { | ||||||
|  |         title: i18n.t("general.share") as string, | ||||||
|  |         icon: $globals.icons.shareVariant, | ||||||
|  |         color: "primary", | ||||||
|  |         event: "share", | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // Get Default Menu Items Specified in Props | ||||||
|  |     for (const [key, value] of Object.entries(props.useItems)) { | ||||||
|  |       if (value) { | ||||||
|  |         const item = defaultItems[key]; | ||||||
|  |         if (item) { | ||||||
|  |           state.menuItems.push(item); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|       this.loading = false; |     // Add leading and Apppending Items | ||||||
|     }, |     state.menuItems = [...state.menuItems, ...props.leadingItems, ...props.appendItems]; | ||||||
|     async deleteRecipe() { |  | ||||||
|       await this.api.recipes.deleteOne(this.slug); |     const icon = props.menuIcon || $globals.icons.dotsVertical; | ||||||
|       this.$emit("deleted"); |  | ||||||
|     }, |     function getRecipeUrl() { | ||||||
|     updateClipboard() { |       return `${window.location.origin}/recipe/${props.slug}`; | ||||||
|       const copyText = this.recipeURL; |  | ||||||
|       navigator.clipboard.writeText(copyText).then( |  | ||||||
|         () => { |  | ||||||
|           console.log("Copied to Clipboard", copyText); |  | ||||||
|           alert.success("Recipe link copied to clipboard"); |  | ||||||
|         }, |  | ||||||
|         () => { |  | ||||||
|           console.log("Copied Failed", copyText); |  | ||||||
|           alert.error("Copied Failed"); |  | ||||||
|     } |     } | ||||||
|       ); |  | ||||||
|     }, |     function getRecipeText() { | ||||||
|  |       return i18n.t("recipe.share-recipe-message", [props.name]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // =========================================================================== | ||||||
|  |     // Context Menu Event Handler | ||||||
|  |  | ||||||
|  |     const router = useRouter(); | ||||||
|  |  | ||||||
|  |     const domConfirmDelete = ref(null); | ||||||
|  |  | ||||||
|  |     async function deleteRecipe() { | ||||||
|  |       await api.recipes.deleteOne(props.slug); | ||||||
|  |       context.emit("delete", props.slug); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async function handleDownloadEvent() { | ||||||
|  |       const { data } = await api.recipes.getZipToken(props.slug); | ||||||
|  |  | ||||||
|  |       if (data) { | ||||||
|  |         window.open(api.recipes.getZipRedirectUrl(props.slug, data.token)); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const { share, isSupported: shareIsSupported } = useShare(); | ||||||
|  |  | ||||||
|  |     const source = ref(""); | ||||||
|  |     const { copy } = useClipboard({ source }); | ||||||
|  |  | ||||||
|  |     async function handleShareEvent() { | ||||||
|  |       if (shareIsSupported) { | ||||||
|  |         share({ | ||||||
|  |           title: props.name, | ||||||
|  |           url: getRecipeUrl(), | ||||||
|  |           text: getRecipeText() as string, | ||||||
|  |         }); | ||||||
|  |       } else { | ||||||
|  |         await copy(getRecipeUrl()); | ||||||
|  |         alert.success("Recipe link copied to clipboard"); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const domMealplanDialog = ref(null); | ||||||
|  |     async function addRecipeToPlan() { | ||||||
|  |       const { response } = await api.mealplans.createOne({ | ||||||
|  |         date: state.newMealdate, | ||||||
|  |         entryType: "dinner", | ||||||
|  |         title: "", | ||||||
|  |         text: "", | ||||||
|  |         recipeId: props.recipeId, | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       if (response?.status === 201) { | ||||||
|  |         alert.success("Recipe added to mealplan"); | ||||||
|  |       } else { | ||||||
|  |         alert.error("Failed to add recipe to mealplan"); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Note: Print is handled as an event in the parent component | ||||||
|  |     const eventHandlers: { [key: string]: Function } = { | ||||||
|  |       // @ts-ignore - Doens't know about open() | ||||||
|  |       delete: () => domConfirmDelete?.value?.open(), | ||||||
|  |       edit: () => router.push(`/recipe/${props.slug}` + "?edit=true"), | ||||||
|  |       download: handleDownloadEvent, | ||||||
|  |       // @ts-ignore - Doens't know about open() | ||||||
|  |       mealplanner: () => domMealplanDialog?.value?.open(), | ||||||
|  |       share: handleShareEvent, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     function contextMenuEventHandler(eventKey: string) { | ||||||
|  |       const handler = eventHandlers[eventKey]; | ||||||
|  |  | ||||||
|  |       if (handler && typeof handler === "function") { | ||||||
|  |         handler(); | ||||||
|  |         state.loading = false; | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       context.emit(eventKey); | ||||||
|  |       state.loading = false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       ...toRefs(state), | ||||||
|  |       contextMenuEventHandler, | ||||||
|  |       deleteRecipe, | ||||||
|  |       addRecipeToPlan, | ||||||
|  |       domConfirmDelete, | ||||||
|  |       domMealplanDialog, | ||||||
|  |       icon, | ||||||
|  |     }; | ||||||
|   }, |   }, | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -1,5 +1,11 @@ | |||||||
| <template> | <template> | ||||||
|   <VJsoneditor v-model="value" height="1500px" :options="options" :attrs="$attrs"></VJsoneditor> |   <VJsoneditor | ||||||
|  |     :value="value" | ||||||
|  |     height="1500px" | ||||||
|  |     :options="options" | ||||||
|  |     :attrs="$attrs" | ||||||
|  |     @input="$emit('input', $event)" | ||||||
|  |   ></VJsoneditor> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
|   | |||||||
| @@ -54,6 +54,7 @@ | |||||||
|         :name="recipe.name" |         :name="recipe.name" | ||||||
|         :logged-in="$auth.loggedIn" |         :logged-in="$auth.loggedIn" | ||||||
|         :open="form" |         :open="form" | ||||||
|  |         :recipe-id="recipe.id" | ||||||
|         class="ml-auto" |         class="ml-auto" | ||||||
|         @close="closeEditor" |         @close="closeEditor" | ||||||
|         @json="toggleJson" |         @json="toggleJson" | ||||||
| @@ -382,6 +383,7 @@ export default defineComponent({ | |||||||
|     async function updateRecipe(slug: string, recipe: Recipe) { |     async function updateRecipe(slug: string, recipe: Recipe) { | ||||||
|       const { data } = await api.recipes.updateOne(slug, recipe); |       const { data } = await api.recipes.updateOne(slug, recipe); | ||||||
|       state.form = false; |       state.form = false; | ||||||
|  |       state.jsonEditor = false; | ||||||
|       if (data?.slug) { |       if (data?.slug) { | ||||||
|         router.push("/recipe/" + data.slug); |         router.push("/recipe/" + data.slug); | ||||||
|       } |       } | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ | |||||||
|       :icon="$globals.icons.primary" |       :icon="$globals.icons.primary" | ||||||
|       :title="$t('page.all-recipes')" |       :title="$t('page.all-recipes')" | ||||||
|       :recipes="recipes" |       :recipes="recipes" | ||||||
|       @deleted="removeRecipe" |       @delete="removeRecipe" | ||||||
|     ></RecipeCardSection> |     ></RecipeCardSection> | ||||||
|     <v-card v-intersect="infiniteScroll"></v-card> |     <v-card v-intersect="infiniteScroll"></v-card> | ||||||
|     <v-fade-transition> |     <v-fade-transition> | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								makefile
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								makefile
									
									
									
									
									
								
							| @@ -76,14 +76,18 @@ setup: ## 🏗  Setup Development Instance | |||||||
| 	yarn install && \ | 	yarn install && \ | ||||||
| 	cd .. | 	cd .. | ||||||
|  |  | ||||||
| 	echo "Be sure to copy the template.env files" | 	@echo Be sure to copy the template.env files | ||||||
|  | 	@echo Testing the Natural Languuage Processor? Try `make setup-model` to get the most recent model | ||||||
|  |  | ||||||
|  | setup-model: ## 🤖 Get the latest NLP CRF++ Model | ||||||
|  | 	@echo Fetching NLP Model - CRF++ is still Required | ||||||
|  | 	curl -L0 https://github.com/mealie-recipes/nlp-model/releases/download/v1.0.0/model.crfmodel --output ./mealie/services/parser_services/crfpp/model.crfmodel | ||||||
|  |  | ||||||
| backend: ## 🎬 Start Mealie Backend Development Server | backend: ## 🎬 Start Mealie Backend Development Server | ||||||
| 	poetry run python mealie/db/init_db.py && \ | 	poetry run python mealie/db/init_db.py && \ | ||||||
| 	poetry run python mealie/services/image/minify.py && \ | 	poetry run python mealie/services/image/minify.py && \ | ||||||
| 	poetry run python mealie/app.py | 	poetry run python mealie/app.py | ||||||
|  |  | ||||||
|  |  | ||||||
| .PHONY: frontend | .PHONY: frontend | ||||||
| frontend: ## 🎬 Start Mealie Frontend Development Server | frontend: ## 🎬 Start Mealie Frontend Development Server | ||||||
| 	cd frontend && yarn run dev | 	cd frontend && yarn run dev | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user