mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 02:03:35 -04:00 
			
		
		
		
	feat: unify recipe card sections (#1560)
* removed unused import * moved categories/tags to new recipe card section * nuked old frontend sort code minor refactoring * bug fixes * added backend recipes filter for tools * removed debug log * removed unusued props * fixed sort for recipes by tool * added tests for getting recipes by tool
This commit is contained in:
		| @@ -15,49 +15,7 @@ | ||||
|         {{ $vuetify.breakpoint.xsOnly ? null : $t("general.random") }} | ||||
|       </v-btn> | ||||
|  | ||||
|       <v-menu v-if="$listeners.sort" offset-y left> | ||||
|         <template #activator="{ on, attrs }"> | ||||
|           <v-btn text :icon="$vuetify.breakpoint.xsOnly" v-bind="attrs" :loading="sortLoading" v-on="on"> | ||||
|             <v-icon :left="!$vuetify.breakpoint.xsOnly"> | ||||
|               {{ $globals.icons.sort }} | ||||
|             </v-icon> | ||||
|             {{ $vuetify.breakpoint.xsOnly ? null : $t("general.sort") }} | ||||
|           </v-btn> | ||||
|         </template> | ||||
|         <v-list> | ||||
|           <v-list-item @click="sortRecipesFrontend(EVENTS.az)"> | ||||
|             <v-icon left> | ||||
|               {{ $globals.icons.orderAlphabeticalAscending }} | ||||
|             </v-icon> | ||||
|             <v-list-item-title>{{ $t("general.sort-alphabetically") }}</v-list-item-title> | ||||
|           </v-list-item> | ||||
|           <v-list-item @click="sortRecipesFrontend(EVENTS.rating)"> | ||||
|             <v-icon left> | ||||
|               {{ $globals.icons.star }} | ||||
|             </v-icon> | ||||
|             <v-list-item-title>{{ $t("general.rating") }}</v-list-item-title> | ||||
|           </v-list-item> | ||||
|           <v-list-item @click="sortRecipesFrontend(EVENTS.created)"> | ||||
|             <v-icon left> | ||||
|               {{ $globals.icons.newBox }} | ||||
|             </v-icon> | ||||
|             <v-list-item-title>{{ $t("general.created") }}</v-list-item-title> | ||||
|           </v-list-item> | ||||
|           <v-list-item @click="sortRecipesFrontend(EVENTS.updated)"> | ||||
|             <v-icon left> | ||||
|               {{ $globals.icons.update }} | ||||
|             </v-icon> | ||||
|             <v-list-item-title>{{ $t("general.updated") }}</v-list-item-title> | ||||
|           </v-list-item> | ||||
|           <v-list-item @click="sortRecipesFrontend(EVENTS.shuffle)"> | ||||
|             <v-icon left> | ||||
|               {{ $globals.icons.shuffleVariant }} | ||||
|             </v-icon> | ||||
|             <v-list-item-title>{{ $t("general.shuffle") }}</v-list-item-title> | ||||
|           </v-list-item> | ||||
|         </v-list> | ||||
|       </v-menu> | ||||
|       <v-menu v-if="$listeners.sortRecipes" offset-y left> | ||||
|       <v-menu offset-y left> | ||||
|         <template #activator="{ on, attrs }"> | ||||
|           <v-btn text :icon="$vuetify.breakpoint.xsOnly" v-bind="attrs" :loading="sortLoading" v-on="on"> | ||||
|             <v-icon :left="!$vuetify.breakpoint.xsOnly"> | ||||
| @@ -147,13 +105,11 @@ | ||||
|         </v-col> | ||||
|       </v-row> | ||||
|     </div> | ||||
|     <div v-if="usePagination"> | ||||
|     <v-card v-intersect="infiniteScroll"></v-card> | ||||
|     <v-fade-transition> | ||||
|       <AppLoader v-if="loading" :loading="loading" /> | ||||
|     </v-fade-transition> | ||||
|   </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| @@ -172,11 +128,10 @@ import { useThrottleFn } from "@vueuse/core"; | ||||
| import RecipeCard from "./RecipeCard.vue"; | ||||
| import RecipeCardMobile from "./RecipeCardMobile.vue"; | ||||
| import { useAsyncKey } from "~/composables/use-utils"; | ||||
| import { useLazyRecipes, useSorter } from "~/composables/recipes"; | ||||
| import { useLazyRecipes } from "~/composables/recipes"; | ||||
| import { Recipe } from "~/types/api-types/recipe"; | ||||
| import { useUserSortPreferences } from "~/composables/use-users/preferences"; | ||||
|  | ||||
| const SORT_EVENT = "sort"; | ||||
| const REPLACE_RECIPES_EVENT = "replaceRecipes"; | ||||
| const APPEND_RECIPES_EVENT = "appendRecipes"; | ||||
|  | ||||
| @@ -206,16 +161,22 @@ export default defineComponent({ | ||||
|       type: Array as () => Recipe[], | ||||
|       default: () => [], | ||||
|     }, | ||||
|     usePagination: { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     categorySlug: { | ||||
|       type: String, | ||||
|       default: null, | ||||
|     }, | ||||
|     tagSlug: { | ||||
|       type: String, | ||||
|       default: null, | ||||
|     }, | ||||
|     toolSlug: { | ||||
|       type: String, | ||||
|       default: null, | ||||
|     }, | ||||
|   }, | ||||
|   setup(props, context) { | ||||
|     const preferences = useUserSortPreferences(); | ||||
|  | ||||
|     const utils = useSorter(); | ||||
|  | ||||
|     const EVENTS = { | ||||
|       az: "az", | ||||
|       rating: "rating", | ||||
| @@ -252,18 +213,23 @@ export default defineComponent({ | ||||
|     const hasMore = ref(true); | ||||
|     const ready = ref(false); | ||||
|     const loading = ref(false); | ||||
|     const category = ref<string>(props.categorySlug); | ||||
|     const tag = ref<string>(props.tagSlug); | ||||
|     const tool = ref<string>(props.toolSlug); | ||||
|  | ||||
|     const { fetchMore } = useLazyRecipes(); | ||||
|  | ||||
|     onMounted(async () => { | ||||
|       if (props.usePagination) { | ||||
|       const newRecipes = await fetchMore( | ||||
|         page.value, | ||||
|  | ||||
|         // we double-up the first call to avoid a bug with large screens that render the entire first page without scrolling, preventing additional loading | ||||
|         perPage.value*2, | ||||
|         preferences.value.orderBy, | ||||
|           preferences.value.orderDirection | ||||
|         preferences.value.orderDirection, | ||||
|         category.value, | ||||
|         tag.value, | ||||
|         tool.value, | ||||
|       ); | ||||
|  | ||||
|       // since we doubled the first call, we also need to advance the page | ||||
| @@ -271,7 +237,6 @@ export default defineComponent({ | ||||
|  | ||||
|       context.emit(REPLACE_RECIPES_EVENT, newRecipes); | ||||
|       ready.value = true; | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     const infiniteScroll = useThrottleFn(() => { | ||||
| @@ -287,7 +252,10 @@ export default defineComponent({ | ||||
|           page.value, | ||||
|           perPage.value, | ||||
|           preferences.value.orderBy, | ||||
|           preferences.value.orderDirection | ||||
|           preferences.value.orderDirection, | ||||
|           category.value, | ||||
|           tag.value, | ||||
|           tool.value, | ||||
|         ); | ||||
|         if (!newRecipes.length) { | ||||
|           hasMore.value = false; | ||||
| @@ -299,12 +267,6 @@ export default defineComponent({ | ||||
|       }, useAsyncKey()); | ||||
|     }, 500); | ||||
|  | ||||
|     /** | ||||
|      * sortRecipes helps filter using the API. This will eventually replace the sortRecipesFrontend function which pulls all recipes | ||||
|      * (without pagination) and does the sorting in the frontend. | ||||
|      * TODO: remove sortRecipesFrontend and remove duplicate "sortRecipes" section in the template (above) | ||||
|      * @param sortType | ||||
|      */ | ||||
|     function sortRecipes(sortType: string) { | ||||
|       if (state.sortLoading || loading.value) { | ||||
|         return; | ||||
| @@ -351,7 +313,10 @@ export default defineComponent({ | ||||
|           page.value, | ||||
|           perPage.value, | ||||
|           preferences.value.orderBy, | ||||
|           preferences.value.orderDirection | ||||
|           preferences.value.orderDirection, | ||||
|           category.value, | ||||
|           tag.value, | ||||
|           tool.value, | ||||
|         ); | ||||
|         context.emit(REPLACE_RECIPES_EVENT, newRecipes); | ||||
|  | ||||
| @@ -360,33 +325,6 @@ export default defineComponent({ | ||||
|       }, useAsyncKey()); | ||||
|     } | ||||
|  | ||||
|     function sortRecipesFrontend(sortType: string) { | ||||
|       state.sortLoading = true; | ||||
|       const sortTarget = [...props.recipes]; | ||||
|       switch (sortType) { | ||||
|         case EVENTS.az: | ||||
|           utils.sortAToZ(sortTarget); | ||||
|           break; | ||||
|         case EVENTS.rating: | ||||
|           utils.sortByRating(sortTarget); | ||||
|           break; | ||||
|         case EVENTS.created: | ||||
|           utils.sortByCreated(sortTarget); | ||||
|           break; | ||||
|         case EVENTS.updated: | ||||
|           utils.sortByUpdated(sortTarget); | ||||
|           break; | ||||
|         case EVENTS.shuffle: | ||||
|           utils.shuffle(sortTarget); | ||||
|           break; | ||||
|         default: | ||||
|           console.log("Unknown Event", sortType); | ||||
|           return; | ||||
|       } | ||||
|       context.emit(SORT_EVENT, sortTarget); | ||||
|       state.sortLoading = false; | ||||
|     } | ||||
|  | ||||
|     function toggleMobileCards() { | ||||
|       preferences.value.useMobileCards = !preferences.value.useMobileCards; | ||||
|     } | ||||
| @@ -400,7 +338,6 @@ export default defineComponent({ | ||||
|       navigateRandom, | ||||
|       preferences, | ||||
|       sortRecipes, | ||||
|       sortRecipesFrontend, | ||||
|       toggleMobileCards, | ||||
|       useMobileCards, | ||||
|     }; | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| export { useFraction } from "./use-fraction"; | ||||
| export { useRecipe } from "./use-recipe"; | ||||
| export { useRecipes, recentRecipes, allRecipes, useLazyRecipes, useSorter } from "./use-recipes"; | ||||
| export { useRecipes, recentRecipes, allRecipes, useLazyRecipes } from "./use-recipes"; | ||||
| export { parseIngredientText } from "./use-recipe-ingredients"; | ||||
| export { useRecipeSearch } from "./use-recipe-search"; | ||||
| export { useTools } from "./use-recipe-tools"; | ||||
|   | ||||
| @@ -6,69 +6,46 @@ import { Recipe } from "~/types/api-types/recipe"; | ||||
| export const allRecipes = ref<Recipe[]>([]); | ||||
| export const recentRecipes = ref<Recipe[]>([]); | ||||
|  | ||||
| const rand = (n: number) => Math.floor(Math.random() * n); | ||||
|  | ||||
| function swap(t: Array<unknown>, i: number, j: number) { | ||||
|   const q = t[i]; | ||||
|   t[i] = t[j]; | ||||
|   t[j] = q; | ||||
|   return t; | ||||
| } | ||||
|  | ||||
| export const useSorter = () => { | ||||
|   function sortAToZ(list: Array<Recipe>) { | ||||
|     list.sort((a, b) => { | ||||
|       const textA: string = a.name?.toUpperCase() ?? ""; | ||||
|       const textB: string = b.name?.toUpperCase() ?? ""; | ||||
|       return textA < textB ? -1 : textA > textB ? 1 : 0; | ||||
|     }); | ||||
|   } | ||||
|   function sortByCreated(list: Array<Recipe>) { | ||||
|     list.sort((a, b) => ((a.dateAdded ?? "") > (b.dateAdded ?? "") ? -1 : 1)); | ||||
|   } | ||||
|   function sortByUpdated(list: Array<Recipe>) { | ||||
|     list.sort((a, b) => ((a.dateUpdated ?? "") > (b.dateUpdated ?? "") ? -1 : 1)); | ||||
|   } | ||||
|   function sortByRating(list: Array<Recipe>) { | ||||
|     list.sort((a, b) => ((a.rating ?? 0) > (b.rating ?? 0) ? -1 : 1)); | ||||
|   } | ||||
|  | ||||
|   function randomRecipe(list: Array<Recipe>): Recipe { | ||||
|     return list[Math.floor(Math.random() * list.length)]; | ||||
|   } | ||||
|  | ||||
|   function shuffle(list: Array<Recipe>) { | ||||
|     let last = list.length; | ||||
|     let n; | ||||
|     while (last > 0) { | ||||
|       n = rand(last); | ||||
|       swap(list, n, --last); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|     sortAToZ, | ||||
|     sortByCreated, | ||||
|     sortByUpdated, | ||||
|     sortByRating, | ||||
|     randomRecipe, | ||||
|     shuffle, | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export const useLazyRecipes = function () { | ||||
|   const api = useUserApi(); | ||||
|  | ||||
|   const recipes = ref<Recipe[]>([]); | ||||
|  | ||||
|   async function fetchMore(page: number, perPage: number, orderBy: string | null = null, orderDirection = "desc") { | ||||
|     const { data } = await api.recipes.getAll(page, perPage, { orderBy, orderDirection }); | ||||
|   async function fetchMore(page: number, perPage: number, orderBy: string | null = null, orderDirection = "desc", category: string | null = null, tag: string | null = null, tool: string | null = null) { | ||||
|     const { data } = await api.recipes.getAll(page, perPage, { orderBy, orderDirection, "categories": category, "tags": tag, "tools": tool }); | ||||
|     return data ? data.items : []; | ||||
|   } | ||||
|  | ||||
|   function appendRecipes(val: Array<Recipe>) { | ||||
|     val.forEach((recipe) => { | ||||
|       recipes.value.push(recipe); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   function assignSorted(val: Array<Recipe>) { | ||||
|     recipes.value = val; | ||||
|   } | ||||
|  | ||||
|   function removeRecipe(slug: string) { | ||||
|     for (let i = 0; i < recipes?.value?.length; i++) { | ||||
|       if (recipes?.value[i].slug === slug) { | ||||
|         recipes?.value.splice(i, 1); | ||||
|         break; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   function replaceRecipes(val: Array<Recipe>) { | ||||
|     recipes.value = val; | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|     recipes, | ||||
|     fetchMore, | ||||
|     appendRecipes, | ||||
|     assignSorted, | ||||
|     removeRecipe, | ||||
|     replaceRecipes | ||||
|   }; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -4,7 +4,6 @@ | ||||
|       :icon="$globals.icons.primary" | ||||
|       :title="$t('page.all-recipes')" | ||||
|       :recipes="recipes" | ||||
|       :use-pagination="true" | ||||
|       @sortRecipes="assignSorted" | ||||
|       @replaceRecipes="replaceRecipes" | ||||
|       @appendRecipes="appendRecipes" | ||||
| @@ -17,36 +16,11 @@ | ||||
| import { defineComponent } from "@nuxtjs/composition-api"; | ||||
| import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue"; | ||||
| import { useLazyRecipes } from "~/composables/recipes"; | ||||
| import { Recipe } from "~/types/api-types/recipe"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   components: { RecipeCardSection }, | ||||
|   setup() { | ||||
|     const { recipes, fetchMore } = useLazyRecipes(); | ||||
|  | ||||
|     function appendRecipes(val: Array<Recipe>) { | ||||
|       val.forEach((recipe) => { | ||||
|         recipes.value.push(recipe); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     function assignSorted(val: Array<Recipe>) { | ||||
|       recipes.value = val; | ||||
|     } | ||||
|  | ||||
|     function removeRecipe(slug: string) { | ||||
|       for (let i = 0; i < recipes?.value?.length; i++) { | ||||
|         if (recipes?.value[i].slug === slug) { | ||||
|           recipes?.value.splice(i, 1); | ||||
|           break; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     function replaceRecipes(val: Array<Recipe>) { | ||||
|       recipes.value = val; | ||||
|     } | ||||
|  | ||||
|     const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(); | ||||
|     return { appendRecipes, assignSorted, recipes, removeRecipe, replaceRecipes }; | ||||
|   }, | ||||
|   head() { | ||||
|   | ||||
| @@ -4,8 +4,12 @@ | ||||
|       v-if="category" | ||||
|       :icon="$globals.icons.tags" | ||||
|       :title="category.name" | ||||
|       :recipes="category.recipes" | ||||
|       @sort="assignSorted" | ||||
|       :recipes="recipes" | ||||
|       :category-slug="category.slug" | ||||
|       @sortRecipes="assignSorted" | ||||
|       @replaceRecipes="replaceRecipes" | ||||
|       @appendRecipes="appendRecipes" | ||||
|       @delete="removeRecipe" | ||||
|     > | ||||
|       <template #title> | ||||
|         <v-btn icon class="mr-1"> | ||||
| @@ -54,13 +58,15 @@ | ||||
|  | ||||
| <script lang="ts"> | ||||
| import { defineComponent, useAsync, useRoute, reactive, toRefs, useRouter } from "@nuxtjs/composition-api"; | ||||
| import { useLazyRecipes } from "~/composables/recipes"; | ||||
| import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue"; | ||||
| import { useUserApi } from "~/composables/api"; | ||||
| import { Recipe } from "~/types/api-types/recipe"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   components: { RecipeCardSection }, | ||||
|   setup() { | ||||
|     const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(); | ||||
|  | ||||
|     const api = useUserApi(); | ||||
|     const route = useRoute(); | ||||
|     const router = useRouter(); | ||||
| @@ -105,6 +111,11 @@ export default defineComponent({ | ||||
|       reset, | ||||
|       ...toRefs(state), | ||||
|       updateCategory, | ||||
|       appendRecipes, | ||||
|       assignSorted, | ||||
|       recipes, | ||||
|       removeRecipe, | ||||
|       replaceRecipes, | ||||
|     }; | ||||
|   }, | ||||
|   head() { | ||||
| @@ -112,12 +123,5 @@ export default defineComponent({ | ||||
|       title: this.$t("category.categories") as string, | ||||
|     }; | ||||
|   }, | ||||
|   methods: { | ||||
|     assignSorted(val: Array<Recipe>) { | ||||
|       if (this.category) { | ||||
|         this.category.recipes = val; | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|   | ||||
| @@ -1,11 +1,15 @@ | ||||
| <template> | ||||
|   <v-container> | ||||
|     <RecipeCardSection | ||||
|       v-if="tags" | ||||
|       v-if="tag" | ||||
|       :icon="$globals.icons.tags" | ||||
|       :title="tags.name" | ||||
|       :recipes="tags.recipes" | ||||
|       @sort="assignSorted" | ||||
|       :title="tag.name" | ||||
|       :recipes="recipes" | ||||
|       :tag-slug="tag.slug" | ||||
|       @sortRecipes="assignSorted" | ||||
|       @replaceRecipes="replaceRecipes" | ||||
|       @appendRecipes="appendRecipes" | ||||
|       @delete="removeRecipe" | ||||
|     > | ||||
|       <template #title> | ||||
|         <v-btn icon class="mr-1"> | ||||
| @@ -16,7 +20,7 @@ | ||||
|  | ||||
|         <template v-if="edit"> | ||||
|           <v-text-field | ||||
|             v-model="tags.name" | ||||
|             v-model="tag.name" | ||||
|             autofocus | ||||
|             single-line | ||||
|             dense | ||||
| @@ -41,7 +45,7 @@ | ||||
|           <v-tooltip top> | ||||
|             <template #activator="{ on, attrs }"> | ||||
|               <v-toolbar-title v-bind="attrs" style="cursor: pointer" class="headline" v-on="on" @click="edit = true"> | ||||
|                 {{ tags.name }} | ||||
|                 {{ tag.name }} | ||||
|               </v-toolbar-title> | ||||
|             </template> | ||||
|             <span> Click to Edit </span> | ||||
| @@ -54,13 +58,15 @@ | ||||
|  | ||||
| <script lang="ts"> | ||||
| import { defineComponent, useAsync, useRoute, reactive, toRefs, useRouter } from "@nuxtjs/composition-api"; | ||||
| import { useLazyRecipes } from "~/composables/recipes"; | ||||
| import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue"; | ||||
| import { useUserApi } from "~/composables/api"; | ||||
| import { Recipe } from "~/types/api-types/recipe"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   components: { RecipeCardSection }, | ||||
|   setup() { | ||||
|     const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(); | ||||
|  | ||||
|     const api = useUserApi(); | ||||
|     const route = useRoute(); | ||||
|     const router = useRouter(); | ||||
| @@ -71,7 +77,7 @@ export default defineComponent({ | ||||
|       edit: false, | ||||
|     }); | ||||
|  | ||||
|     const tags = useAsync(async () => { | ||||
|     const tag = useAsync(async () => { | ||||
|       const { data } = await api.tags.bySlug(slug); | ||||
|       if (data) { | ||||
|         state.initialValue = data.name; | ||||
| @@ -82,18 +88,18 @@ export default defineComponent({ | ||||
|     function reset() { | ||||
|       state.edit = false; | ||||
|  | ||||
|       if (tags.value) { | ||||
|         tags.value.name = state.initialValue; | ||||
|       if (tag.value) { | ||||
|         tag.value.name = state.initialValue; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     async function updateTags() { | ||||
|       state.edit = false; | ||||
|  | ||||
|       if (!tags.value) { | ||||
|       if (!tag.value) { | ||||
|         return; | ||||
|       } | ||||
|       const { data } = await api.tags.updateOne(tags.value.id, tags.value); | ||||
|       const { data } = await api.tags.updateOne(tag.value.id, tag.value); | ||||
|  | ||||
|       if (data) { | ||||
|         router.push("/recipes/tags/" + data.slug); | ||||
| @@ -101,10 +107,15 @@ export default defineComponent({ | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|       tags, | ||||
|       tag, | ||||
|       reset, | ||||
|       ...toRefs(state), | ||||
|       updateTags, | ||||
|       appendRecipes, | ||||
|       assignSorted, | ||||
|       recipes, | ||||
|       removeRecipe, | ||||
|       replaceRecipes, | ||||
|     }; | ||||
|   }, | ||||
|   head() { | ||||
| @@ -112,12 +123,5 @@ export default defineComponent({ | ||||
|       title: this.$t("tag.tags") as string, | ||||
|     }; | ||||
|   }, | ||||
|   methods: { | ||||
|     assignSorted(val: Array<Recipe>) { | ||||
|       if (this.tags) { | ||||
|         this.tags.recipes = val; | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|   | ||||
| @@ -1,6 +1,16 @@ | ||||
| <template> | ||||
|   <v-container> | ||||
|     <RecipeCardSection v-if="tools" :title="tools.name" :recipes="tools.recipes" @sort="assignSorted"> | ||||
|     <RecipeCardSection | ||||
|       v-if="tool" | ||||
|       :icon="$globals.icons.potSteam" | ||||
|       :title="tool.name" | ||||
|       :recipes="recipes" | ||||
|       :tool-slug="tool.slug" | ||||
|       @sortRecipes="assignSorted" | ||||
|       @replaceRecipes="replaceRecipes" | ||||
|       @appendRecipes="appendRecipes" | ||||
|       @delete="removeRecipe" | ||||
|     > | ||||
|       <template #title> | ||||
|         <v-btn icon class="mr-1"> | ||||
|           <v-icon dark large @click="reset"> | ||||
| @@ -10,7 +20,7 @@ | ||||
|  | ||||
|         <template v-if="edit"> | ||||
|           <v-text-field | ||||
|             v-model="tools.name" | ||||
|             v-model="tool.name" | ||||
|             autofocus | ||||
|             single-line | ||||
|             dense | ||||
| @@ -35,7 +45,7 @@ | ||||
|           <v-tooltip top> | ||||
|             <template #activator="{ on, attrs }"> | ||||
|               <v-toolbar-title v-bind="attrs" style="cursor: pointer" class="headline" v-on="on" @click="edit = true"> | ||||
|                 {{ tools.name }} | ||||
|                 {{ tool.name }} | ||||
|               </v-toolbar-title> | ||||
|             </template> | ||||
|             <span> Click to Edit </span> | ||||
| @@ -48,13 +58,15 @@ | ||||
|  | ||||
| <script lang="ts"> | ||||
| import { defineComponent, useAsync, useRoute, reactive, toRefs, useRouter } from "@nuxtjs/composition-api"; | ||||
| import { useLazyRecipes } from "~/composables/recipes"; | ||||
| import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue"; | ||||
| import { useUserApi } from "~/composables/api"; | ||||
| import { Recipe } from "~/types/api-types/recipe"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   components: { RecipeCardSection }, | ||||
|   setup() { | ||||
|     const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(); | ||||
|  | ||||
|     const api = useUserApi(); | ||||
|     const route = useRoute(); | ||||
|     const router = useRouter(); | ||||
| @@ -65,7 +77,7 @@ export default defineComponent({ | ||||
|       edit: false, | ||||
|     }); | ||||
|  | ||||
|     const tools = useAsync(async () => { | ||||
|     const tool = useAsync(async () => { | ||||
|       const { data } = await api.tools.bySlug(slug); | ||||
|       if (data) { | ||||
|         state.initialValue = data.name; | ||||
| @@ -76,18 +88,18 @@ export default defineComponent({ | ||||
|     function reset() { | ||||
|       state.edit = false; | ||||
|  | ||||
|       if (tools.value) { | ||||
|         tools.value.name = state.initialValue; | ||||
|       if (tool.value) { | ||||
|         tool.value.name = state.initialValue; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     async function updateTools() { | ||||
|       state.edit = false; | ||||
|  | ||||
|       if (!tools.value) { | ||||
|       if (!tool.value) { | ||||
|         return; | ||||
|       } | ||||
|       const { data } = await api.tools.updateOne(tools.value.id, tools.value); | ||||
|       const { data } = await api.tools.updateOne(tool.value.id, tool.value); | ||||
|  | ||||
|       if (data) { | ||||
|         router.push("/recipes/tools/" + data.slug); | ||||
| @@ -95,10 +107,15 @@ export default defineComponent({ | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|       tools, | ||||
|       tool, | ||||
|       reset, | ||||
|       ...toRefs(state), | ||||
|       updateTools, | ||||
|       appendRecipes, | ||||
|       assignSorted, | ||||
|       recipes, | ||||
|       removeRecipe, | ||||
|       replaceRecipes, | ||||
|     }; | ||||
|   }, | ||||
|   head() { | ||||
| @@ -106,12 +123,5 @@ export default defineComponent({ | ||||
|       title: this.$t("tool.tools") as string, | ||||
|     }; | ||||
|   }, | ||||
|   methods: { | ||||
|     assignSorted(val: Array<Recipe>) { | ||||
|       if (this.tools) { | ||||
|         this.tools.recipes = val; | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|   | ||||
| @@ -136,6 +136,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]): | ||||
|         load_food=False, | ||||
|         categories: Optional[list[UUID4 | str]] = None, | ||||
|         tags: Optional[list[UUID4 | str]] = None, | ||||
|         tools: Optional[list[UUID4 | str]] = None, | ||||
|     ) -> RecipePagination: | ||||
|         q = self.session.query(self.model) | ||||
|  | ||||
| @@ -169,6 +170,14 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]): | ||||
|                 else: | ||||
|                     q = q.filter(RecipeModel.tags.any(Tag.slug == tag)) | ||||
|  | ||||
|         if tools: | ||||
|             for tool in tools: | ||||
|                 if isinstance(tool, UUID): | ||||
|                     q = q.filter(RecipeModel.tools.any(Tool.id == tool)) | ||||
|  | ||||
|                 else: | ||||
|                     q = q.filter(RecipeModel.tools.any(Tool.slug == tool)) | ||||
|  | ||||
|         q, count, total_pages = self.add_pagination_to_query(q, pagination) | ||||
|  | ||||
|         try: | ||||
|   | ||||
| @@ -213,12 +213,14 @@ class RecipeController(BaseRecipeController): | ||||
|         q: RecipePaginationQuery = Depends(RecipePaginationQuery), | ||||
|         categories: Optional[list[UUID4 | str]] = Query(None), | ||||
|         tags: Optional[list[UUID4 | str]] = Query(None), | ||||
|         tools: Optional[list[UUID4 | str]] = Query(None), | ||||
|     ): | ||||
|         response = self.repo.page_all( | ||||
|             pagination=q, | ||||
|             load_food=q.load_food, | ||||
|             categories=categories, | ||||
|             tags=tags, | ||||
|             tools=tools, | ||||
|         ) | ||||
|  | ||||
|         response.set_pagination_guides(router.url_path_for("get_all"), q.dict()) | ||||
|   | ||||
| @@ -4,6 +4,7 @@ from mealie.repos.repository_factory import AllRepositories | ||||
| from mealie.repos.repository_recipes import RepositoryRecipes | ||||
| from mealie.schema.recipe.recipe import Recipe, RecipeCategory, RecipePaginationQuery, RecipeSummary | ||||
| from mealie.schema.recipe.recipe_category import CategoryOut, CategorySave, TagSave | ||||
| from mealie.schema.recipe.recipe_tool import RecipeToolSave | ||||
| from tests.utils.factories import random_string | ||||
| from tests.utils.fixture_schemas import TestUser | ||||
|  | ||||
| @@ -276,3 +277,84 @@ def test_recipe_repo_pagination_by_tags(database: AllRepositories, unique_user: | ||||
|         tag_ids = [tag.id for tag in recipe_summary.tags] | ||||
|         for tag in created_tags: | ||||
|             assert tag.id in tag_ids | ||||
|  | ||||
|  | ||||
| def test_recipe_repo_pagination_by_tools(database: AllRepositories, unique_user: TestUser): | ||||
|     slug1, slug2 = [random_string(10) for _ in range(2)] | ||||
|  | ||||
|     tools = [ | ||||
|         RecipeToolSave(group_id=unique_user.group_id, name=slug1, slug=slug1), | ||||
|         RecipeToolSave(group_id=unique_user.group_id, name=slug2, slug=slug2), | ||||
|     ] | ||||
|  | ||||
|     created_tools = [database.tools.create(tool) for tool in tools] | ||||
|  | ||||
|     # Bootstrap the database with recipes | ||||
|     recipes = [] | ||||
|  | ||||
|     for i in range(10): | ||||
|         # None of the tools | ||||
|         recipes.append( | ||||
|             Recipe( | ||||
|                 user_id=unique_user.user_id, | ||||
|                 group_id=unique_user.group_id, | ||||
|                 name=random_string(), | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|         # Only one of the tools | ||||
|         recipes.append( | ||||
|             Recipe( | ||||
|                 user_id=unique_user.user_id, | ||||
|                 group_id=unique_user.group_id, | ||||
|                 name=random_string(), | ||||
|                 tools=[created_tools[i % 2]], | ||||
|             ), | ||||
|         ) | ||||
|  | ||||
|         # Both of the tools | ||||
|         recipes.append( | ||||
|             Recipe( | ||||
|                 user_id=unique_user.user_id, | ||||
|                 group_id=unique_user.group_id, | ||||
|                 name=random_string(), | ||||
|                 tools=created_tools, | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|     for recipe in recipes: | ||||
|         database.recipes.create(recipe) | ||||
|  | ||||
|     pagination_query = RecipePaginationQuery( | ||||
|         page=1, | ||||
|         per_page=-1, | ||||
|     ) | ||||
|  | ||||
|     # Get all recipes with only one tool by UUID | ||||
|     tool_id = created_tools[0].id | ||||
|     recipes_with_one_tool = database.recipes.page_all(pagination_query, tools=[tool_id]).items | ||||
|     assert len(recipes_with_one_tool) == 15 | ||||
|  | ||||
|     for recipe_summary in recipes_with_one_tool: | ||||
|         tool_ids = [tool.id for tool in recipe_summary.tools] | ||||
|         assert tool_id in tool_ids | ||||
|  | ||||
|     # Get all recipes with only one tool by slug | ||||
|     tool_slug = created_tools[1].slug | ||||
|     recipes_with_one_tool = database.recipes.page_all(pagination_query, tools=[tool_slug]).items | ||||
|     assert len(recipes_with_one_tool) == 15 | ||||
|  | ||||
|     for recipe_summary in recipes_with_one_tool: | ||||
|         tool_slugs = [tool.slug for tool in recipe_summary.tools] | ||||
|         assert tool_slug in tool_slugs | ||||
|  | ||||
|     # Get all recipes with both tools | ||||
|     recipes_with_both_tools = database.recipes.page_all( | ||||
|         pagination_query, tools=[tool.id for tool in created_tools] | ||||
|     ).items | ||||
|     assert len(recipes_with_both_tools) == 10 | ||||
|  | ||||
|     for recipe_summary in recipes_with_both_tools: | ||||
|         tool_ids = [tool.id for tool in recipe_summary.tools] | ||||
|         for tool in created_tools: | ||||
|             assert tool.id in tool_ids | ||||
|   | ||||
		Reference in New Issue
	
	Block a user