mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 10:13:32 -04:00 
			
		
		
		
	feat: restore frontend sorting for all recipes (#1497)
* fixed typo * merged "all recipes" pagination into recipe card created custom sort card for all recipes refactored backend calls for all recipes to sort/paginate * frontend lint fixes * restored recipes reference * replaced "this" with reference * fix linting errors * re-order context menu * add todo Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
This commit is contained in:
		| @@ -14,18 +14,50 @@ | |||||||
|         </v-icon> |         </v-icon> | ||||||
|         {{ $vuetify.breakpoint.xsOnly ? null : $t("general.random") }} |         {{ $vuetify.breakpoint.xsOnly ? null : $t("general.random") }} | ||||||
|       </v-btn> |       </v-btn> | ||||||
|       <ContextMenu |  | ||||||
|         v-if="!$vuetify.breakpoint.xsOnly" |  | ||||||
|         :items="[ |  | ||||||
|           { |  | ||||||
|             title: 'Toggle View', |  | ||||||
|             icon: $globals.icons.eye, |  | ||||||
|             event: 'toggle-dense-view', |  | ||||||
|           }, |  | ||||||
|         ]" |  | ||||||
|         @toggle-dense-view="mobileCards = !mobileCards" |  | ||||||
|       /> |  | ||||||
|       <v-menu v-if="$listeners.sort" offset-y left> |       <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> | ||||||
|         <template #activator="{ on, attrs }"> |         <template #activator="{ on, attrs }"> | ||||||
|           <v-btn text :icon="$vuetify.breakpoint.xsOnly" v-bind="attrs" :loading="sortLoading" v-on="on"> |           <v-btn text :icon="$vuetify.breakpoint.xsOnly" v-bind="attrs" :loading="sortLoading" v-on="on"> | ||||||
|             <v-icon :left="!$vuetify.breakpoint.xsOnly"> |             <v-icon :left="!$vuetify.breakpoint.xsOnly"> | ||||||
| @@ -59,14 +91,19 @@ | |||||||
|             </v-icon> |             </v-icon> | ||||||
|             <v-list-item-title>{{ $t("general.updated") }}</v-list-item-title> |             <v-list-item-title>{{ $t("general.updated") }}</v-list-item-title> | ||||||
|           </v-list-item> |           </v-list-item> | ||||||
|           <v-list-item @click="sortRecipes(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-list> | ||||||
|       </v-menu> |       </v-menu> | ||||||
|  |       <ContextMenu | ||||||
|  |         v-if="!$vuetify.breakpoint.xsOnly" | ||||||
|  |         :items="[ | ||||||
|  |           { | ||||||
|  |             title: 'Toggle View', | ||||||
|  |             icon: $globals.icons.eye, | ||||||
|  |             event: 'toggle-dense-view', | ||||||
|  |           }, | ||||||
|  |         ]" | ||||||
|  |         @toggle-dense-view="mobileCards = !mobileCards" | ||||||
|  |       /> | ||||||
|     </v-app-bar> |     </v-app-bar> | ||||||
|     <div v-if="recipes" class="mt-2"> |     <div v-if="recipes" class="mt-2"> | ||||||
|       <v-row v-if="!viewScale"> |       <v-row v-if="!viewScale"> | ||||||
| @@ -110,17 +147,37 @@ | |||||||
|         </v-col> |         </v-col> | ||||||
|       </v-row> |       </v-row> | ||||||
|     </div> |     </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> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { computed, defineComponent, reactive, toRefs, useContext, useRouter, ref } from "@nuxtjs/composition-api"; | import { | ||||||
|  |   computed, | ||||||
|  |   defineComponent, | ||||||
|  |   onMounted, | ||||||
|  |   reactive, | ||||||
|  |   ref, | ||||||
|  |   toRefs, | ||||||
|  |   useAsync, | ||||||
|  |   useContext, | ||||||
|  |   useRouter, | ||||||
|  | } from "@nuxtjs/composition-api"; | ||||||
|  | import { useThrottleFn } from "@vueuse/core"; | ||||||
| import RecipeCard from "./RecipeCard.vue"; | import RecipeCard from "./RecipeCard.vue"; | ||||||
| import RecipeCardMobile from "./RecipeCardMobile.vue"; | import RecipeCardMobile from "./RecipeCardMobile.vue"; | ||||||
| import { useSorter } from "~/composables/recipes"; | import { useAsyncKey } from "~/composables/use-utils"; | ||||||
|  | import { useLazyRecipes, useSorter } from "~/composables/recipes"; | ||||||
| import { Recipe } from "~/types/api-types/recipe"; | import { Recipe } from "~/types/api-types/recipe"; | ||||||
|  |  | ||||||
| const SORT_EVENT = "sort"; | const SORT_EVENT = "sort"; | ||||||
|  | const REPLACE_RECIPES_EVENT = "replaceRecipes"; | ||||||
|  | const APPEND_RECIPES_EVENT = "appendRecipes"; | ||||||
|  |  | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
|   components: { |   components: { | ||||||
| @@ -148,6 +205,10 @@ export default defineComponent({ | |||||||
|       type: Array as () => Recipe[], |       type: Array as () => Recipe[], | ||||||
|       default: () => [], |       default: () => [], | ||||||
|     }, |     }, | ||||||
|  |     usePagination: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false, | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   setup(props, context) { |   setup(props, context) { | ||||||
|     const mobileCards = ref(false); |     const mobileCards = ref(false); | ||||||
| @@ -184,7 +245,114 @@ export default defineComponent({ | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     const page = ref(1); | ||||||
|  |     const perPage = ref(30); | ||||||
|  |     const orderBy = ref("name"); | ||||||
|  |     const orderDirection = ref("asc"); | ||||||
|  |     const hasMore = ref(true); | ||||||
|  |  | ||||||
|  |     const ready = ref(false); | ||||||
|  |     const loading = ref(false); | ||||||
|  |  | ||||||
|  |     const { recipes, fetchMore } = useLazyRecipes(); | ||||||
|  |  | ||||||
|  |     onMounted(async () => { | ||||||
|  |       if (props.usePagination) { | ||||||
|  |         const newRecipes = await fetchMore(page.value, perPage.value, orderBy.value, orderDirection.value); | ||||||
|  |         context.emit(REPLACE_RECIPES_EVENT, newRecipes); | ||||||
|  |         ready.value = true; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const infiniteScroll = useThrottleFn(() => { | ||||||
|  |       useAsync(async () => { | ||||||
|  |         if (!ready.value || !hasMore.value || loading.value) { | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         loading.value = true; | ||||||
|  |         page.value = page.value + 1; | ||||||
|  |  | ||||||
|  |         const newRecipes = await fetchMore(page.value, perPage.value, orderBy.value, orderDirection.value); | ||||||
|  |         if (!newRecipes.length) { | ||||||
|  |           hasMore.value = false; | ||||||
|  |         } else { | ||||||
|  |           context.emit(APPEND_RECIPES_EVENT, newRecipes); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         loading.value = false; | ||||||
|  |       }, 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) | ||||||
|  |     TODO: use indicator to show asc / desc order | ||||||
|  |     */ | ||||||
|  |  | ||||||
|     function sortRecipes(sortType: string) { |     function sortRecipes(sortType: string) { | ||||||
|  |       if (state.sortLoading || loading.value) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       switch (sortType) { | ||||||
|  |         case EVENTS.az: | ||||||
|  |           if (orderBy.value !== "name") { | ||||||
|  |             orderBy.value = "name"; | ||||||
|  |             orderDirection.value = "asc"; | ||||||
|  |           } else { | ||||||
|  |             orderDirection.value = orderDirection.value === "asc" ? "desc" : "asc"; | ||||||
|  |           } | ||||||
|  |           break; | ||||||
|  |         case EVENTS.rating: | ||||||
|  |           if (orderBy.value !== "rating") { | ||||||
|  |             orderBy.value = "rating"; | ||||||
|  |             orderDirection.value = "desc"; | ||||||
|  |           } else { | ||||||
|  |             orderDirection.value = orderDirection.value === "asc" ? "desc" : "asc"; | ||||||
|  |           } | ||||||
|  |           break; | ||||||
|  |         case EVENTS.created: | ||||||
|  |           if (orderBy.value !== "created_at") { | ||||||
|  |             orderBy.value = "created_at"; | ||||||
|  |             orderDirection.value = "desc"; | ||||||
|  |           } else { | ||||||
|  |             orderDirection.value = orderDirection.value === "asc" ? "desc" : "asc"; | ||||||
|  |           } | ||||||
|  |           break; | ||||||
|  |         case EVENTS.updated: | ||||||
|  |           if (orderBy.value !== "update_at") { | ||||||
|  |             orderBy.value = "update_at"; | ||||||
|  |             orderDirection.value = "desc"; | ||||||
|  |           } else { | ||||||
|  |             orderDirection.value = orderDirection.value === "asc" ? "desc" : "asc"; | ||||||
|  |           } | ||||||
|  |           break; | ||||||
|  |         default: | ||||||
|  |           console.log("Unknown Event", sortType); | ||||||
|  |           return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       useAsync(async () => { | ||||||
|  |         // reset pagination | ||||||
|  |         page.value = 1; | ||||||
|  |         hasMore.value = true; | ||||||
|  |  | ||||||
|  |         state.sortLoading = true; | ||||||
|  |         loading.value = true; | ||||||
|  |  | ||||||
|  |         // fetch new recipes | ||||||
|  |         const newRecipes = await fetchMore(page.value, perPage.value, orderBy.value, orderDirection.value); | ||||||
|  |         context.emit(REPLACE_RECIPES_EVENT, newRecipes); | ||||||
|  |  | ||||||
|  |         state.sortLoading = false; | ||||||
|  |         loading.value = false; | ||||||
|  |       }, useAsyncKey()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function sortRecipesFrontend(sortType: string) { | ||||||
|       state.sortLoading = true; |       state.sortLoading = true; | ||||||
|       const sortTarget = [...props.recipes]; |       const sortTarget = [...props.recipes]; | ||||||
|       switch (sortType) { |       switch (sortType) { | ||||||
| @@ -217,8 +385,11 @@ export default defineComponent({ | |||||||
|       EVENTS, |       EVENTS, | ||||||
|       viewScale, |       viewScale, | ||||||
|       displayTitleIcon, |       displayTitleIcon, | ||||||
|  |       infiniteScroll, | ||||||
|  |       loading, | ||||||
|       navigateRandom, |       navigateRandom, | ||||||
|       sortRecipes, |       sortRecipes, | ||||||
|  |       sortRecipesFrontend, | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -63,11 +63,7 @@ export const useLazyRecipes = function () { | |||||||
|  |  | ||||||
|   async function fetchMore(page: number, perPage: number, orderBy: string | null = null, orderDirection = "desc") { |   async function fetchMore(page: number, perPage: number, orderBy: string | null = null, orderDirection = "desc") { | ||||||
|     const { data } = await api.recipes.getAll(page, perPage, { orderBy, orderDirection }); |     const { data } = await api.recipes.getAll(page, perPage, { orderBy, orderDirection }); | ||||||
|     if (data) { |     return data ? data.items : []; | ||||||
|       data.items.forEach((recipe) => { |  | ||||||
|         recipes.value?.push(recipe); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return { |   return { | ||||||
|   | |||||||
| @@ -4,48 +4,35 @@ | |||||||
|       :icon="$globals.icons.primary" |       :icon="$globals.icons.primary" | ||||||
|       :title="$t('page.all-recipes')" |       :title="$t('page.all-recipes')" | ||||||
|       :recipes="recipes" |       :recipes="recipes" | ||||||
|  |       :use-pagination="true" | ||||||
|  |       @sortRecipes="assignSorted" | ||||||
|  |       @replaceRecipes="replaceRecipes" | ||||||
|  |       @appendRecipes="appendRecipes" | ||||||
|       @delete="removeRecipe" |       @delete="removeRecipe" | ||||||
|     ></RecipeCardSection> |     ></RecipeCardSection> | ||||||
|     <v-card v-intersect="infiniteScroll"></v-card> |  | ||||||
|     <v-fade-transition> |  | ||||||
|       <AppLoader v-if="loading" :loading="loading" /> |  | ||||||
|     </v-fade-transition> |  | ||||||
|   </v-container> |   </v-container> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent, onMounted, ref } from "@nuxtjs/composition-api"; | import { defineComponent } from "@nuxtjs/composition-api"; | ||||||
| import { useThrottleFn } from "@vueuse/core"; |  | ||||||
| import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue"; | import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue"; | ||||||
| import { useLazyRecipes } from "~/composables/recipes"; | import { useLazyRecipes } from "~/composables/recipes"; | ||||||
|  | import { Recipe } from "~/types/api-types/recipe"; | ||||||
|  |  | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
|   components: { RecipeCardSection }, |   components: { RecipeCardSection }, | ||||||
|   setup() { |   setup() { | ||||||
|     const page = ref(1); |  | ||||||
|     const perPage = ref(30); |  | ||||||
|     const orderBy = "name"; |  | ||||||
|     const orderDirection = "asc"; |  | ||||||
|  |  | ||||||
|     const ready = ref(false); |  | ||||||
|     const loading = ref(false); |  | ||||||
|  |  | ||||||
|     const { recipes, fetchMore } = useLazyRecipes(); |     const { recipes, fetchMore } = useLazyRecipes(); | ||||||
|  |  | ||||||
|     onMounted(async () => { |     function appendRecipes(val: Array<Recipe>) { | ||||||
|       await fetchMore(page.value, perPage.value, orderBy, orderDirection); |       val.forEach((recipe) => { | ||||||
|       ready.value = true; |         recipes.value.push(recipe); | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|     const infiniteScroll = useThrottleFn(() => { |  | ||||||
|       if (!ready.value) { |  | ||||||
|         return; |  | ||||||
|     } |     } | ||||||
|       loading.value = true; |  | ||||||
|       page.value = page.value + 1; |     function assignSorted(val: Array<Recipe>) { | ||||||
|       fetchMore(page.value, perPage.value, orderBy, orderDirection); |       recipes.value = val; | ||||||
|       loading.value = false; |     } | ||||||
|     }, 500); |  | ||||||
|  |  | ||||||
|     function removeRecipe(slug: string) { |     function removeRecipe(slug: string) { | ||||||
|       for (let i = 0; i < recipes?.value?.length; i++) { |       for (let i = 0; i < recipes?.value?.length; i++) { | ||||||
| @@ -56,7 +43,11 @@ export default defineComponent({ | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return { recipes, infiniteScroll, loading, removeRecipe }; |     function replaceRecipes(val: Array<Recipe>) { | ||||||
|  |       recipes.value = val; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return { appendRecipes, assignSorted, recipes, removeRecipe, replaceRecipes }; | ||||||
|   }, |   }, | ||||||
|   head() { |   head() { | ||||||
|     return { |     return { | ||||||
|   | |||||||
| @@ -30,7 +30,7 @@ export default defineComponent({ | |||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   head: { |   head: { | ||||||
|     title: "Tags", |     title: "Categories", | ||||||
|   }, |   }, | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user