mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 02:03:35 -04:00 
			
		
		
		
	fix: 2148 infinite scroll on search (#2173)
* refactor recipe card section to use unified query construct * rework search page so it uses lazy-loading of RecipeCardSection * remove RecipeQuery again * prettier reformatting * remove recipes/all page * remove max results setting from search * fix typing issues
This commit is contained in:
		| @@ -53,7 +53,7 @@ | ||||
|             <v-icon left> | ||||
|               {{ $globals.icons.chefHat }} | ||||
|             </v-icon> | ||||
|             <v-list-item-title>{{ $t('general.last-made') }}</v-list-item-title> | ||||
|             <v-list-item-title>{{ $t("general.last-made") }}</v-list-item-title> | ||||
|           </v-list-item> | ||||
|         </v-list> | ||||
|       </v-menu> | ||||
| @@ -129,6 +129,7 @@ import { | ||||
|   useAsync, | ||||
|   useContext, | ||||
|   useRouter, | ||||
|   watch, | ||||
| } from "@nuxtjs/composition-api"; | ||||
| import { useThrottleFn } from "@vueuse/core"; | ||||
| import RecipeCard from "./RecipeCard.vue"; | ||||
| @@ -137,6 +138,7 @@ import { useAsyncKey } from "~/composables/use-utils"; | ||||
| import { useLazyRecipes } from "~/composables/recipes"; | ||||
| import { Recipe } from "~/lib/api/types/recipe"; | ||||
| import { useUserSortPreferences } from "~/composables/use-users/preferences"; | ||||
| import { RecipeSearchQuery } from "~/lib/api/user/recipes/recipe"; | ||||
|  | ||||
| const REPLACE_RECIPES_EVENT = "replaceRecipes"; | ||||
| const APPEND_RECIPES_EVENT = "appendRecipes"; | ||||
| @@ -167,26 +169,10 @@ export default defineComponent({ | ||||
|       type: Array as () => Recipe[], | ||||
|       default: () => [], | ||||
|     }, | ||||
|     cookbookSlug: { | ||||
|       type: String, | ||||
|     query: { | ||||
|       type: Object as () => RecipeSearchQuery, | ||||
|       default: null, | ||||
|     }, | ||||
|     categorySlug: { | ||||
|       type: String, | ||||
|       default: null, | ||||
|     }, | ||||
|     tagSlug: { | ||||
|       type: String, | ||||
|       default: null, | ||||
|     }, | ||||
|     toolSlug: { | ||||
|       type: String, | ||||
|       default: null, | ||||
|     }, | ||||
|     skipLoad: { | ||||
|       type: Boolean, | ||||
|       default: false | ||||
|     } | ||||
|   }, | ||||
|   setup(props, context) { | ||||
|     const preferences = useUserSortPreferences(); | ||||
| @@ -224,45 +210,59 @@ export default defineComponent({ | ||||
|     } | ||||
|  | ||||
|     const page = ref(1); | ||||
|     const perPage = ref(32); | ||||
|     const perPage = 32; | ||||
|     const hasMore = ref(true); | ||||
|     const ready = ref(false); | ||||
|     const loading = ref(false); | ||||
|  | ||||
|     const cookbook = ref<string>(props.cookbookSlug); | ||||
|     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.skipLoad) { | ||||
|         return; | ||||
|       } | ||||
|       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, | ||||
|         cookbook.value, | ||||
|         category.value, | ||||
|         tag.value, | ||||
|         tool.value, | ||||
|  | ||||
|         // filter out recipes that have a null value for the property we're sorting by | ||||
|         preferences.value.filterNull && preferences.value.orderBy ? `${preferences.value.orderBy} <> null` : null | ||||
|       ); | ||||
|  | ||||
|       // since we doubled the first call, we also need to advance the page | ||||
|       page.value = page.value + 1; | ||||
|  | ||||
|       context.emit(REPLACE_RECIPES_EVENT, newRecipes); | ||||
|       ready.value = true; | ||||
|     const queryFilter = computed(() => { | ||||
|       const orderBy = props.query?.orderBy || preferences.value.orderBy; | ||||
|       return preferences.value.filterNull && orderBy ? `${orderBy} <> null` : null; | ||||
|     }); | ||||
|  | ||||
|     async function fetchRecipes(pageCount = 1) { | ||||
|       return 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 * pageCount, | ||||
|         props.query?.orderBy || preferences.value.orderBy, | ||||
|         props.query?.orderDirection || preferences.value.orderDirection, | ||||
|         props.query, | ||||
|         // filter out recipes that have a null value for the property we're sorting by | ||||
|         queryFilter.value | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     onMounted(async () => { | ||||
|       if (props.query) { | ||||
|         const newRecipes = await fetchRecipes(2); | ||||
|  | ||||
|         // since we doubled the first call, we also need to advance the page | ||||
|         page.value = page.value + 1; | ||||
|  | ||||
|         context.emit(REPLACE_RECIPES_EVENT, newRecipes); | ||||
|         ready.value = true; | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     watch( | ||||
|       () => props.query, | ||||
|       async (newValue: RecipeSearchQuery | undefined) => { | ||||
|         if (newValue) { | ||||
|           page.value = 1; | ||||
|           const newRecipes = await fetchRecipes(2); | ||||
|  | ||||
|           // since we doubled the first call, we also need to advance the page | ||||
|           page.value = page.value + 1; | ||||
|  | ||||
|           context.emit(REPLACE_RECIPES_EVENT, newRecipes); | ||||
|           ready.value = true; | ||||
|         } | ||||
|       } | ||||
|     ); | ||||
|  | ||||
|     const infiniteScroll = useThrottleFn(() => { | ||||
|       useAsync(async () => { | ||||
|         if (!ready.value || !hasMore.value || loading.value) { | ||||
| @@ -272,19 +272,7 @@ export default defineComponent({ | ||||
|         loading.value = true; | ||||
|         page.value = page.value + 1; | ||||
|  | ||||
|         const newRecipes = await fetchMore( | ||||
|           page.value, | ||||
|           perPage.value, | ||||
|           preferences.value.orderBy, | ||||
|           preferences.value.orderDirection, | ||||
|           cookbook.value, | ||||
|           category.value, | ||||
|           tag.value, | ||||
|           tool.value, | ||||
|  | ||||
|           // filter out recipes that have a null value for the property we're sorting by | ||||
|           preferences.value.filterNull && preferences.value.orderBy ? `${preferences.value.orderBy} <> null` : null | ||||
|         ); | ||||
|         const newRecipes = await fetchRecipes(); | ||||
|         if (!newRecipes.length) { | ||||
|           hasMore.value = false; | ||||
|         } else { | ||||
| @@ -300,7 +288,13 @@ export default defineComponent({ | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       function setter(orderBy: string, ascIcon: string, descIcon: string, defaultOrderDirection = "asc", filterNull = false) { | ||||
|       function setter( | ||||
|         orderBy: string, | ||||
|         ascIcon: string, | ||||
|         descIcon: string, | ||||
|         defaultOrderDirection = "asc", | ||||
|         filterNull = false | ||||
|       ) { | ||||
|         if (preferences.value.orderBy !== orderBy) { | ||||
|           preferences.value.orderBy = orderBy; | ||||
|           preferences.value.orderDirection = defaultOrderDirection; | ||||
| @@ -313,19 +307,37 @@ export default defineComponent({ | ||||
|  | ||||
|       switch (sortType) { | ||||
|         case EVENTS.az: | ||||
|           setter("name", $globals.icons.sortAlphabeticalAscending, $globals.icons.sortAlphabeticalDescending, "asc", false); | ||||
|           setter( | ||||
|             "name", | ||||
|             $globals.icons.sortAlphabeticalAscending, | ||||
|             $globals.icons.sortAlphabeticalDescending, | ||||
|             "asc", | ||||
|             false | ||||
|           ); | ||||
|           break; | ||||
|         case EVENTS.rating: | ||||
|           setter("rating", $globals.icons.sortAscending, $globals.icons.sortDescending, "desc", true); | ||||
|           break; | ||||
|         case EVENTS.created: | ||||
|           setter("created_at", $globals.icons.sortCalendarAscending, $globals.icons.sortCalendarDescending, "desc", false); | ||||
|           setter( | ||||
|             "created_at", | ||||
|             $globals.icons.sortCalendarAscending, | ||||
|             $globals.icons.sortCalendarDescending, | ||||
|             "desc", | ||||
|             false | ||||
|           ); | ||||
|           break; | ||||
|         case EVENTS.updated: | ||||
|           setter("update_at", $globals.icons.sortClockAscending, $globals.icons.sortClockDescending, "desc", false); | ||||
|           break; | ||||
|         case EVENTS.lastMade: | ||||
|           setter("last_made", $globals.icons.sortCalendarAscending, $globals.icons.sortCalendarDescending, "desc", true); | ||||
|           setter( | ||||
|             "last_made", | ||||
|             $globals.icons.sortCalendarAscending, | ||||
|             $globals.icons.sortCalendarDescending, | ||||
|             "desc", | ||||
|             true | ||||
|           ); | ||||
|           break; | ||||
|         default: | ||||
|           console.log("Unknown Event", sortType); | ||||
| @@ -341,19 +353,7 @@ export default defineComponent({ | ||||
|         loading.value = true; | ||||
|  | ||||
|         // fetch new recipes | ||||
|         const newRecipes = await fetchMore( | ||||
|           page.value, | ||||
|           perPage.value, | ||||
|           preferences.value.orderBy, | ||||
|           preferences.value.orderDirection, | ||||
|           cookbook.value, | ||||
|           category.value, | ||||
|           tag.value, | ||||
|           tool.value, | ||||
|  | ||||
|           // filter out recipes that have a null value for the property we're sorting by | ||||
|           preferences.value.filterNull && preferences.value.orderBy ? `${preferences.value.orderBy} <> null` : null | ||||
|         ); | ||||
|         const newRecipes = await fetchRecipes(); | ||||
|         context.emit(REPLACE_RECIPES_EVENT, newRecipes); | ||||
|  | ||||
|         state.sortLoading = false; | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { Ref, useAsync } from "@nuxtjs/composition-api"; | ||||
| import { useAsyncKey } from "../use-utils"; | ||||
| import { BaseCRUDAPI } from "~/lib/api/base/base-clients"; | ||||
| import { QueryValue } from "~/lib/api/base/route"; | ||||
|  | ||||
| type BoundT = { | ||||
|   id?: string | number; | ||||
| @@ -25,7 +26,7 @@ export function useStoreActions<T extends BoundT>( | ||||
|   allRef: Ref<T[] | null> | null, | ||||
|   loading: Ref<boolean> | ||||
| ): StoreActions<T> { | ||||
|   function getAll(page = 1, perPage = -1, params = {} as any) { | ||||
|   function getAll(page = 1, perPage = -1, params = {} as Record<string, QueryValue>) { | ||||
|     params.orderBy ??= "name"; | ||||
|     params.orderDirection ??= "asc"; | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| import { useAsync, ref } from "@nuxtjs/composition-api"; | ||||
| import { useAsyncKey } from "../use-utils"; | ||||
| import { useUserApi } from "~/composables/api"; | ||||
| import { Recipe } from "~/lib/api/types/recipe"; | ||||
| import {Recipe} from "~/lib/api/types/recipe"; | ||||
| import {RecipeSearchQuery} from "~/lib/api/user/recipes/recipe"; | ||||
|  | ||||
| export const allRecipes = ref<Recipe[]>([]); | ||||
| export const recentRecipes = ref<Recipe[]>([]); | ||||
| @@ -16,19 +17,22 @@ export const useLazyRecipes = function () { | ||||
|     perPage: number, | ||||
|     orderBy: string | null = null, | ||||
|     orderDirection = "desc", | ||||
|     cookbook: string | null = null, | ||||
|     category: string | null = null, | ||||
|     tag: string | null = null, | ||||
|     tool: string | null = null, | ||||
|     query: RecipeSearchQuery | null = null, | ||||
|     queryFilter: string | null = null, | ||||
|   ) { | ||||
|     const { data } = await api.recipes.getAll(page, perPage, { | ||||
|       orderBy, | ||||
|       orderDirection, | ||||
|       cookbook, | ||||
|       categories: category, | ||||
|       tags: tag, | ||||
|       tools: tool, | ||||
|       search: query?.search, | ||||
|       cookbook: query?.cookbook, | ||||
|       categories: query?.categories, | ||||
|       requireAllCategories: query?.requireAllCategories, | ||||
|       tags: query?.tags, | ||||
|       requireAllTags: query?.requireAllTags, | ||||
|       tools: query?.tools, | ||||
|       requireAllTools: query?.requireAllTools, | ||||
|       foods: query?.foods, | ||||
|       requireAllFoods: query?.requireAllFoods, | ||||
|       queryFilter, | ||||
|     }); | ||||
|     return data ? data.items : []; | ||||
|   | ||||
| @@ -169,11 +169,6 @@ export default defineComponent({ | ||||
|         to: "/shopping-lists", | ||||
|         restricted: true, | ||||
|       }, | ||||
|       { | ||||
|         icon: $globals.icons.viewModule, | ||||
|         to: "/recipes/all", | ||||
|         title: i18n.t("sidebar.all-recipes"), | ||||
|       }, | ||||
|       { | ||||
|         icon: $globals.icons.tags, | ||||
|         to: "/recipes/categories", | ||||
|   | ||||
| @@ -50,7 +50,6 @@ export default defineComponent({ | ||||
|  | ||||
|     const buttons = [ | ||||
|       { icon: $globals.icons.home, to: "/", text: i18n.t("general.home") }, | ||||
|       { icon: $globals.icons.primary, to: "/recipes/all", text: i18n.t("page.all-recipes") }, | ||||
|     ]; | ||||
|  | ||||
|     return { | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { Recipe } from "../types/recipe"; | ||||
| import { ApiRequestInstance, PaginationData } from "~/lib/api/types/non-generated"; | ||||
| import { QueryValue, route } from "~/lib/api/base/route"; | ||||
|  | ||||
| export interface CrudAPIInterface { | ||||
|   requests: ApiRequestInstance; | ||||
| @@ -21,14 +22,14 @@ export abstract class BaseAPI { | ||||
|  | ||||
| export abstract class BaseCRUDAPI<CreateType, ReadType, UpdateType = CreateType> | ||||
|   extends BaseAPI | ||||
|   implements CrudAPIInterface { | ||||
|   implements CrudAPIInterface | ||||
| { | ||||
|   abstract baseRoute: string; | ||||
|   abstract itemRoute(itemId: string | number): string; | ||||
|  | ||||
|   async getAll(page = 1, perPage = -1, params = {} as any) { | ||||
|     return await this.requests.get<PaginationData<ReadType>>(this.baseRoute, { | ||||
|       params: { page, perPage, ...params }, | ||||
|     }); | ||||
|   async getAll(page = 1, perPage = -1, params = {} as Record<string, QueryValue>) { | ||||
|     params = Object.fromEntries(Object.entries(params).filter(([_, v]) => v !== null && v !== undefined)); | ||||
|     return await this.requests.get<PaginationData<ReadType>>(route(this.baseRoute, { page, perPage, ...params })); | ||||
|   } | ||||
|  | ||||
|   async createOne(payload: CreateType) { | ||||
|   | ||||
| @@ -21,7 +21,6 @@ export type QueryValue = string | string[] | number | number[] | boolean | null | ||||
|  */ | ||||
| export function route(rest: string, params: Record<string, QueryValue> | null = null): string { | ||||
|   const url = new URL(parts.prefix + rest, parts.host); | ||||
|  | ||||
|   if (params) { | ||||
|     for (const [key, value] of Object.entries(params)) { | ||||
|       if (Array.isArray(value)) { | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| /* tslint:disable */ | ||||
| /* eslint-disable */ | ||||
| /** | ||||
| /* This file was automatically generated from pydantic models by running pydantic2ts. | ||||
| /* Do not modify it by hand - just update the pydantic models and then re-run the script | ||||
| */ | ||||
|  /* This file was automatically generated from pydantic models by running pydantic2ts. | ||||
|  /* Do not modify it by hand - just update the pydantic models and then re-run the script | ||||
|  */ | ||||
|  | ||||
| export type ExportTypes = "json"; | ||||
| export type RegisteredParser = "nlp" | "brute"; | ||||
|   | ||||
| @@ -55,9 +55,9 @@ const routes = { | ||||
|   recipesSlugTimelineEventId: (slug: string, id: string) => `${prefix}/recipes/${slug}/timeline/events/${id}`, | ||||
| }; | ||||
|  | ||||
| export type RecipeSearchQuery ={ | ||||
| export type RecipeSearchQuery = { | ||||
|   search: string; | ||||
|   orderDirection? : "asc" | "desc"; | ||||
|   orderDirection?: "asc" | "desc"; | ||||
|   groupId?: string; | ||||
|  | ||||
|   queryFilter?: string; | ||||
| @@ -76,11 +76,10 @@ export type RecipeSearchQuery ={ | ||||
|   foods?: string[]; | ||||
|   requireAllFoods?: boolean; | ||||
|  | ||||
|   page: number; | ||||
|   perPage: number; | ||||
|   page?: number; | ||||
|   perPage?: number; | ||||
|   orderBy?: string; | ||||
| } | ||||
|  | ||||
| }; | ||||
|  | ||||
| export class RecipeAPI extends BaseCRUDAPI<CreateRecipe, Recipe, Recipe> { | ||||
|   baseRoute: string = routes.recipesBase; | ||||
| @@ -96,7 +95,7 @@ export class RecipeAPI extends BaseCRUDAPI<CreateRecipe, Recipe, Recipe> { | ||||
|     this.share = new RecipeShareApi(requests); | ||||
|   } | ||||
|  | ||||
|   async search(rsq : RecipeSearchQuery) { | ||||
|   async search(rsq: RecipeSearchQuery) { | ||||
|     return await this.requests.get<PaginationData<Recipe>>(route(routes.recipesBase, rsq)); | ||||
|   } | ||||
|  | ||||
| @@ -176,7 +175,10 @@ export class RecipeAPI extends BaseCRUDAPI<CreateRecipe, Recipe, Recipe> { | ||||
|   } | ||||
|  | ||||
|   async updateTimelineEvent(recipeSlug: string, eventId: string, payload: RecipeTimelineEventUpdate) { | ||||
|     return await this.requests.put<RecipeTimelineEventOut, RecipeTimelineEventUpdate>(routes.recipesSlugTimelineEventId(recipeSlug, eventId), payload); | ||||
|     return await this.requests.put<RecipeTimelineEventOut, RecipeTimelineEventUpdate>( | ||||
|       routes.recipesSlugTimelineEventId(recipeSlug, eventId), | ||||
|       payload | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   async deleteTimelineEvent(recipeSlug: string, eventId: string) { | ||||
| @@ -184,8 +186,11 @@ export class RecipeAPI extends BaseCRUDAPI<CreateRecipe, Recipe, Recipe> { | ||||
|   } | ||||
|  | ||||
|   async getAllTimelineEvents(recipeSlug: string, page = 1, perPage = -1, params = {} as any) { | ||||
|     return await this.requests.get<PaginationData<RecipeTimelineEventOut>>(routes.recipesSlugTimelineEvent(recipeSlug), { | ||||
|       params: { page, perPage, ...params }, | ||||
|     }); | ||||
|     return await this.requests.get<PaginationData<RecipeTimelineEventOut>>( | ||||
|       routes.recipesSlugTimelineEvent(recipeSlug), | ||||
|       { | ||||
|         params: { page, perPage, ...params }, | ||||
|       } | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -14,7 +14,7 @@ | ||||
|       <RecipeCardSection | ||||
|         class="mb-5 mx-1" | ||||
|         :recipes="recipes" | ||||
|         :cookbook-slug="slug" | ||||
|         :query="{ cookbook: slug }" | ||||
|         @sortRecipes="assignSorted" | ||||
|         @replaceRecipes="replaceRecipes" | ||||
|         @appendRecipes="appendRecipes" | ||||
|   | ||||
| @@ -98,15 +98,6 @@ | ||||
|             </template> | ||||
|             <v-card> | ||||
|               <v-card-text> | ||||
|                 <v-text-field | ||||
|                   v-model="state.maxResults" | ||||
|                   class="mt-0 pt-0" | ||||
|                   :label="$tc('search.max-results')" | ||||
|                   type="number" | ||||
|                   outlined | ||||
|                   dense | ||||
|                   hide-details | ||||
|                 /> | ||||
|                 <v-switch v-model="state.auto" label="Auto Search" single-line></v-switch> | ||||
|                 <v-btn block color="primary" @click="reset"> | ||||
|                   {{ $tc("general.reset") }} | ||||
| @@ -116,7 +107,7 @@ | ||||
|           </v-menu> | ||||
|         </div> | ||||
|         <div v-if="!state.auto" class="search-button-container"> | ||||
|           <v-btn :loading="state.loading" x-large color="primary" type="submit" block> | ||||
|           <v-btn x-large color="primary" type="submit" block> | ||||
|             <v-icon left> | ||||
|               {{ $globals.icons.search }} | ||||
|             </v-icon> | ||||
| @@ -131,38 +122,41 @@ | ||||
|         class="mt-n5" | ||||
|         :icon="$globals.icons.search" | ||||
|         :title="$tc('search.results')" | ||||
|         :recipes="state.results" | ||||
|       /> | ||||
|         :recipes="recipes" | ||||
|         :query="passedQuery" | ||||
|         @sortRecipes="assignSorted" | ||||
|         @replaceRecipes="replaceRecipes" | ||||
|         @appendRecipes="appendRecipes" | ||||
|         @delete="removeRecipe" | ||||
|       ></RecipeCardSection> | ||||
|     </v-container> | ||||
|   </v-container> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import { ref, defineComponent, useRouter, onMounted, useContext, computed } from "@nuxtjs/composition-api"; | ||||
| // eslint-disable-next-line import/namespace | ||||
| import { watchDebounced } from "@vueuse/shared"; | ||||
| import SearchFilter from "~/components/Domain/SearchFilter.vue"; | ||||
| import { useUserApi } from "~/composables/api"; | ||||
| import { useCategoryStore, useFoodStore, useTagStore, useToolStore } from "~/composables/store"; | ||||
| import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue"; | ||||
| import { IngredientFood, RecipeCategory, RecipeSummary, RecipeTag, RecipeTool } from "~/lib/api/types/recipe"; | ||||
| import { IngredientFood, RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe"; | ||||
| import { NoUndefinedField } from "~/lib/api/types/non-generated"; | ||||
| import { useLazyRecipes } from "~/composables/recipes"; | ||||
| import { RecipeSearchQuery } from "~/lib/api/user/recipes/recipe"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   components: { SearchFilter, RecipeCardSection }, | ||||
|   setup() { | ||||
|     const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(); | ||||
|  | ||||
|     const router = useRouter(); | ||||
|     const api = useUserApi(); | ||||
|     const { $globals, i18n } = useContext(); | ||||
|  | ||||
|     const state = ref({ | ||||
|       auto: true, | ||||
|       loading: false, | ||||
|       search: "", | ||||
|       orderBy: "created_at", | ||||
|       orderDirection: "desc" as "asc" | "desc", | ||||
|       maxResults: 21, | ||||
|       results: [] as RecipeSummary[], | ||||
|  | ||||
|       // and/or | ||||
|       requireAllCategories: false, | ||||
| @@ -183,9 +177,10 @@ export default defineComponent({ | ||||
|     const tools = useToolStore(); | ||||
|     const selectedTools = ref<NoUndefinedField<RecipeTool>[]>([]); | ||||
|  | ||||
|     const passedQuery = ref<RecipeSearchQuery | null>(null); | ||||
|  | ||||
|     function reset() { | ||||
|       state.value.search = ""; | ||||
|       state.value.maxResults = 21; | ||||
|       state.value.orderBy = "created_at"; | ||||
|       state.value.orderDirection = "desc"; | ||||
|       state.value.requireAllCategories = false; | ||||
| @@ -213,7 +208,6 @@ export default defineComponent({ | ||||
|     } | ||||
|  | ||||
|     async function search() { | ||||
|       state.value.loading = true; | ||||
|       await router.push({ | ||||
|         query: { | ||||
|           categories: toIDArray(selectedCategories.value), | ||||
| @@ -225,7 +219,6 @@ export default defineComponent({ | ||||
|           ...{ | ||||
|             auto: state.value.auto ? undefined : "false", | ||||
|             search: state.value.search === "" ? undefined : state.value.search, | ||||
|             maxResults: state.value.maxResults === 21 ? undefined : state.value.maxResults.toString(), | ||||
|             orderBy: state.value.orderBy === "createdAt" ? undefined : state.value.orderBy, | ||||
|             orderDirection: state.value.orderDirection === "desc" ? undefined : state.value.orderDirection, | ||||
|             requireAllCategories: state.value.requireAllCategories ? "true" : undefined, | ||||
| @@ -236,35 +229,19 @@ export default defineComponent({ | ||||
|         }, | ||||
|       }); | ||||
|  | ||||
|       const { data, error } = await api.recipes.search({ | ||||
|       passedQuery.value = { | ||||
|         search: state.value.search, | ||||
|         page: 1, | ||||
|         orderBy: state.value.orderBy, | ||||
|         orderDirection: state.value.orderDirection, | ||||
|         perPage: state.value.maxResults, | ||||
|         categories: toIDArray(selectedCategories.value), | ||||
|         foods: toIDArray(selectedFoods.value), | ||||
|         tags: toIDArray(selectedTags.value), | ||||
|         tools: toIDArray(selectedTools.value), | ||||
|  | ||||
|         requireAllCategories: state.value.requireAllCategories, | ||||
|         requireAllTags: state.value.requireAllTags, | ||||
|         requireAllTools: state.value.requireAllTools, | ||||
|         requireAllFoods: state.value.requireAllFoods, | ||||
|       }); | ||||
|  | ||||
|       if (error) { | ||||
|         console.error(error); | ||||
|         state.value.loading = false; | ||||
|         state.value.results = []; | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       if (data) { | ||||
|         state.value.results = data.items; | ||||
|       } | ||||
|  | ||||
|       state.value.loading = false; | ||||
|         orderBy: state.value.orderBy, | ||||
|         orderDirection: state.value.orderDirection, | ||||
|       }; | ||||
|     } | ||||
|  | ||||
|     function waitUntilAndExecute( | ||||
| @@ -345,10 +322,6 @@ export default defineComponent({ | ||||
|         state.value.search = query.search as string; | ||||
|       } | ||||
|  | ||||
|       if (query.maxResults) { | ||||
|         state.value.maxResults = parseInt(query.maxResults as string); | ||||
|       } | ||||
|  | ||||
|       if (query.orderBy) { | ||||
|         state.value.orderBy = query.orderBy as string; | ||||
|       } | ||||
| @@ -429,7 +402,6 @@ export default defineComponent({ | ||||
|         () => state.value.requireAllFoods, | ||||
|         () => state.value.orderBy, | ||||
|         () => state.value.orderDirection, | ||||
|         () => state.value.maxResults, | ||||
|         selectedCategories, | ||||
|         selectedFoods, | ||||
|         selectedTags, | ||||
| @@ -462,6 +434,12 @@ export default defineComponent({ | ||||
|       selectedFoods, | ||||
|       selectedTags, | ||||
|       selectedTools, | ||||
|       appendRecipes, | ||||
|       assignSorted, | ||||
|       recipes, | ||||
|       removeRecipe, | ||||
|       replaceRecipes, | ||||
|       passedQuery, | ||||
|     }; | ||||
|   }, | ||||
| }); | ||||
|   | ||||
| @@ -1,32 +0,0 @@ | ||||
| <template> | ||||
|   <v-container> | ||||
|     <RecipeCardSection | ||||
|       :icon="$globals.icons.primary" | ||||
|       :title="$t('page.all-recipes')" | ||||
|       :recipes="recipes" | ||||
|       @sortRecipes="assignSorted" | ||||
|       @replaceRecipes="replaceRecipes" | ||||
|       @appendRecipes="appendRecipes" | ||||
|       @delete="removeRecipe" | ||||
|     ></RecipeCardSection> | ||||
|   </v-container> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from "@nuxtjs/composition-api"; | ||||
| import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue"; | ||||
| import { useLazyRecipes } from "~/composables/recipes"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   components: { RecipeCardSection }, | ||||
|   setup() { | ||||
|     const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(); | ||||
|     return { appendRecipes, assignSorted, recipes, removeRecipe, replaceRecipes }; | ||||
|   }, | ||||
|   head() { | ||||
|     return { | ||||
|       title: this.$t("page.all-recipes") as string, | ||||
|     }; | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
| @@ -5,7 +5,7 @@ | ||||
|       :icon="$globals.icons.tags" | ||||
|       :title="category.name" | ||||
|       :recipes="recipes" | ||||
|       :category-slug="category.slug" | ||||
|       :query="{ categories: [category.slug] }" | ||||
|       @sortRecipes="assignSorted" | ||||
|       @replaceRecipes="replaceRecipes" | ||||
|       @appendRecipes="appendRecipes" | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|       :icon="$globals.icons.tags" | ||||
|       :title="tag.name" | ||||
|       :recipes="recipes" | ||||
|       :tag-slug="tag.slug" | ||||
|       :query="{ tags: [tag.slug] }" | ||||
|       @sortRecipes="assignSorted" | ||||
|       @replaceRecipes="replaceRecipes" | ||||
|       @appendRecipes="appendRecipes" | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|       :icon="$globals.icons.potSteam" | ||||
|       :title="tool.name" | ||||
|       :recipes="recipes" | ||||
|       :tool-slug="tool.slug" | ||||
|       :query="{ tools: [tool.slug] }" | ||||
|       @sortRecipes="assignSorted" | ||||
|       @replaceRecipes="replaceRecipes" | ||||
|       @appendRecipes="appendRecipes" | ||||
|   | ||||
| @@ -301,7 +301,7 @@ export default defineComponent({ | ||||
|     } | ||||
|  | ||||
|     const statsTo: { [key: string]: string } = { | ||||
|       totalRecipes: "/recipes/all", | ||||
|       totalRecipes: "/", | ||||
|       totalUsers: "/group/members", | ||||
|       totalCategories: "/recipes/categories", | ||||
|       totalTags: "/recipes/tags", | ||||
|   | ||||
		Reference in New Issue
	
	Block a user