diff --git a/frontend/components/Domain/Recipe/RecipeCardSection.vue b/frontend/components/Domain/Recipe/RecipeCardSection.vue index 88e73ef566a0..f1edb8abfcc2 100644 --- a/frontend/components/Domain/Recipe/RecipeCardSection.vue +++ b/frontend/components/Domain/Recipe/RecipeCardSection.vue @@ -53,7 +53,7 @@ {{ $globals.icons.chefHat }} - {{ $t('general.last-made') }} + {{ $t("general.last-made") }} @@ -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(props.cookbookSlug); - const category = ref(props.categorySlug); - const tag = ref(props.tagSlug); - const tool = ref(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; diff --git a/frontend/composables/partials/use-actions-factory.ts b/frontend/composables/partials/use-actions-factory.ts index 7d009687e753..4a21dc8868a2 100644 --- a/frontend/composables/partials/use-actions-factory.ts +++ b/frontend/composables/partials/use-actions-factory.ts @@ -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( allRef: Ref | null, loading: Ref ): StoreActions { - function getAll(page = 1, perPage = -1, params = {} as any) { + function getAll(page = 1, perPage = -1, params = {} as Record) { params.orderBy ??= "name"; params.orderDirection ??= "asc"; diff --git a/frontend/composables/recipes/use-recipes.ts b/frontend/composables/recipes/use-recipes.ts index f56c25f2ab47..b353a55ef8d9 100644 --- a/frontend/composables/recipes/use-recipes.ts +++ b/frontend/composables/recipes/use-recipes.ts @@ -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([]); export const recentRecipes = ref([]); @@ -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 : []; diff --git a/frontend/layouts/default.vue b/frontend/layouts/default.vue index c30485d65b35..77ae6490ace8 100644 --- a/frontend/layouts/default.vue +++ b/frontend/layouts/default.vue @@ -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", diff --git a/frontend/layouts/error.vue b/frontend/layouts/error.vue index c9c92b3bd007..6731482f464a 100644 --- a/frontend/layouts/error.vue +++ b/frontend/layouts/error.vue @@ -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 { diff --git a/frontend/lib/api/base/base-clients.ts b/frontend/lib/api/base/base-clients.ts index 7b0f702329de..265549cc74a4 100644 --- a/frontend/lib/api/base/base-clients.ts +++ b/frontend/lib/api/base/base-clients.ts @@ -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 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>(this.baseRoute, { - params: { page, perPage, ...params }, - }); + async getAll(page = 1, perPage = -1, params = {} as Record) { + params = Object.fromEntries(Object.entries(params).filter(([_, v]) => v !== null && v !== undefined)); + return await this.requests.get>(route(this.baseRoute, { page, perPage, ...params })); } async createOne(payload: CreateType) { diff --git a/frontend/lib/api/base/route.ts b/frontend/lib/api/base/route.ts index 49eea50554e2..302b6d697967 100644 --- a/frontend/lib/api/base/route.ts +++ b/frontend/lib/api/base/route.ts @@ -21,7 +21,6 @@ export type QueryValue = string | string[] | number | number[] | boolean | null */ export function route(rest: string, params: Record | 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)) { diff --git a/frontend/lib/api/types/recipe.ts b/frontend/lib/api/types/recipe.ts index 3b5a8617a448..d5e8e61b1817 100644 --- a/frontend/lib/api/types/recipe.ts +++ b/frontend/lib/api/types/recipe.ts @@ -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"; diff --git a/frontend/lib/api/user/recipes/recipe.ts b/frontend/lib/api/user/recipes/recipe.ts index 65080b143340..e397ec12dd4a 100644 --- a/frontend/lib/api/user/recipes/recipe.ts +++ b/frontend/lib/api/user/recipes/recipe.ts @@ -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 { baseRoute: string = routes.recipesBase; @@ -96,7 +95,7 @@ export class RecipeAPI extends BaseCRUDAPI { this.share = new RecipeShareApi(requests); } - async search(rsq : RecipeSearchQuery) { + async search(rsq: RecipeSearchQuery) { return await this.requests.get>(route(routes.recipesBase, rsq)); } @@ -176,7 +175,10 @@ export class RecipeAPI extends BaseCRUDAPI { } async updateTimelineEvent(recipeSlug: string, eventId: string, payload: RecipeTimelineEventUpdate) { - return await this.requests.put(routes.recipesSlugTimelineEventId(recipeSlug, eventId), payload); + return await this.requests.put( + routes.recipesSlugTimelineEventId(recipeSlug, eventId), + payload + ); } async deleteTimelineEvent(recipeSlug: string, eventId: string) { @@ -184,8 +186,11 @@ export class RecipeAPI extends BaseCRUDAPI { } async getAllTimelineEvents(recipeSlug: string, page = 1, perPage = -1, params = {} as any) { - return await this.requests.get>(routes.recipesSlugTimelineEvent(recipeSlug), { - params: { page, perPage, ...params }, - }); + return await this.requests.get>( + routes.recipesSlugTimelineEvent(recipeSlug), + { + params: { page, perPage, ...params }, + } + ); } } diff --git a/frontend/pages/cookbooks/_slug.vue b/frontend/pages/cookbooks/_slug.vue index 2418dc4e1182..ee32f9f5f512 100644 --- a/frontend/pages/cookbooks/_slug.vue +++ b/frontend/pages/cookbooks/_slug.vue @@ -14,7 +14,7 @@ - {{ $tc("general.reset") }} @@ -116,7 +107,7 @@
- + {{ $globals.icons.search }} @@ -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" + > diff --git a/frontend/pages/recipes/categories/_slug.vue b/frontend/pages/recipes/categories/_slug.vue index a2a8f425c5dd..9f2314353cdc 100644 --- a/frontend/pages/recipes/categories/_slug.vue +++ b/frontend/pages/recipes/categories/_slug.vue @@ -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" diff --git a/frontend/pages/recipes/tags/_slug.vue b/frontend/pages/recipes/tags/_slug.vue index 0262266334b3..a989f961e269 100644 --- a/frontend/pages/recipes/tags/_slug.vue +++ b/frontend/pages/recipes/tags/_slug.vue @@ -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" diff --git a/frontend/pages/recipes/tools/_slug.vue b/frontend/pages/recipes/tools/_slug.vue index 27f46cdd622b..dd6a907d1b30 100644 --- a/frontend/pages/recipes/tools/_slug.vue +++ b/frontend/pages/recipes/tools/_slug.vue @@ -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" diff --git a/frontend/pages/user/profile/index.vue b/frontend/pages/user/profile/index.vue index c1a4986be2b4..cdbeef40e7ac 100644 --- a/frontend/pages/user/profile/index.vue +++ b/frontend/pages/user/profile/index.vue @@ -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",