diff --git a/docs/docs/changelog/v1.0.0.md b/docs/docs/changelog/v1.0.0.md index e7b113b438ab..d527a6768c39 100644 --- a/docs/docs/changelog/v1.0.0.md +++ b/docs/docs/changelog/v1.0.0.md @@ -41,6 +41,10 @@ ### 🥙 Recipes +**Search** +- Search now includes the ability to fuzzy search ingredients +- Search now includes an additional filter for "Foods" which will filter (Include/Exclude) matches based on your selection. + **Recipe General** - Recipes are now only viewable by group members - You can now import multiple URLs at a time pre-tagged using the bulk importer. This task runs in the background so no need to wait for it to finish. diff --git a/frontend/components/Domain/Recipe/RecipeCardMobile.vue b/frontend/components/Domain/Recipe/RecipeCardMobile.vue index 8ab4bd2ad1ba..4d22d548718c 100644 --- a/frontend/components/Domain/Recipe/RecipeCardMobile.vue +++ b/frontend/components/Domain/Recipe/RecipeCardMobile.vue @@ -1,66 +1,64 @@ + + \ No newline at end of file diff --git a/frontend/components/Layout/AppHeader.vue b/frontend/components/Layout/AppHeader.vue index 489a1b1a6874..866033c8dd25 100644 --- a/frontend/components/Layout/AppHeader.vue +++ b/frontend/components/Layout/AppHeader.vue @@ -10,24 +10,29 @@
Mealie
- - {{ value }} + - - + +
+ + +
+ + {{ $globals.icons.search }} + \ No newline at end of file diff --git a/frontend/composables/recipes/index.ts b/frontend/composables/recipes/index.ts index 62984494aa57..7730af4cc662 100644 --- a/frontend/composables/recipes/index.ts +++ b/frontend/composables/recipes/index.ts @@ -5,3 +5,4 @@ export { useUnits } from "./use-recipe-units"; export { useRecipes, recentRecipes, allRecipes, useLazyRecipes, useSorter } from "./use-recipes"; export { useTags, useCategories, allCategories, allTags } from "./use-tags-categories"; export { parseIngredientText } from "./use-recipe-ingredients"; +export { useRecipeSearch } from "./use-recipe-search"; diff --git a/frontend/composables/recipes/use-recipe-search.ts b/frontend/composables/recipes/use-recipe-search.ts new file mode 100644 index 000000000000..1ddde2c539e3 --- /dev/null +++ b/frontend/composables/recipes/use-recipe-search.ts @@ -0,0 +1,46 @@ +import { computed, reactive, ref, Ref } from "@nuxtjs/composition-api"; +import Fuse from "fuse.js"; +import { Recipe } from "~/types/api-types/recipe"; + +export const useRecipeSearch = (recipes: Ref) => { + const localState = reactive({ + options: { + shouldSort: true, + threshold: 0.6, + location: 0, + distance: 100, + findAllMatches: true, + maxPatternLength: 32, + minMatchCharLength: 2, + keys: ["name", "description", "recipeIngredient.note", "recipeIngredient.food.name"], + }, + }); + + const search = ref(""); + + const fuse = computed(() => { + return new Fuse(recipes.value || [], localState.options); + }); + + const fuzzyRecipes = computed(() => { + if (search.value.trim() === "") { + return recipes.value; + } + const result = fuse.value.search(search.value.trim()); + return result.map((x) => x.item); + }); + + const results = computed(() => { + if (!fuzzyRecipes.value) { + return []; + } + + if (fuzzyRecipes.value.length > 0 && search.value.length != null && search.value.length >= 1) { + return fuzzyRecipes.value; + } else { + return recipes.value; + } + }); + + return { results, search }; +}; diff --git a/frontend/composables/use-router.ts b/frontend/composables/use-router.ts index 4216276c951f..701893d1d314 100644 --- a/frontend/composables/use-router.ts +++ b/frontend/composables/use-router.ts @@ -24,16 +24,14 @@ export function useRouteQuery(name: string, default return computed({ get() { - console.log("Getter"); const data = route.value.query[name]; if (data == null) return defaultValue ?? null; return data; }, set(v) { nextTick(() => { - console.log("Setter"); // @ts-ignore - router.value.replace({ query: { ...route.value.query, [name]: v } }); + router.replace({ query: { ...route.value.query, [name]: v } }); }); }, }); diff --git a/frontend/layouts/default.vue b/frontend/layouts/default.vue index f4119c474842..0a62c4b8ae40 100644 --- a/frontend/layouts/default.vue +++ b/frontend/layouts/default.vue @@ -146,23 +146,10 @@ export default defineComponent({ ], topLinks: [ { - icon: this.$globals.icons.calendar, - restricted: true, + icon: this.$globals.icons.calendarMultiselect, title: this.$t("meal-plan.meal-planner"), - children: [ - { - icon: this.$globals.icons.calendarMultiselect, - title: this.$t("meal-plan.planner"), - to: "/meal-plan/planner", - restricted: true, - }, - { - icon: this.$globals.icons.calendarWeek, - title: this.$t("meal-plan.dinner-this-week"), - to: "/meal-plan/this-week", - restricted: true, - }, - ], + to: "/meal-plan/planner", + restricted: true, }, { icon: this.$globals.icons.formatListCheck, diff --git a/frontend/pages/register.vue b/frontend/pages/register.vue index 6c73d0842108..ed7017873aa1 100644 --- a/frontend/pages/register.vue +++ b/frontend/pages/register.vue @@ -120,7 +120,6 @@ export default defineComponent({ watch(token, (newToken) => { if (newToken) { - console.log(token); form.groupToken = newToken; } }); diff --git a/frontend/pages/search.vue b/frontend/pages/search.vue index 9ef613bb122c..00322ff88597 100644 --- a/frontend/pages/search.vue +++ b/frontend/pages/search.vue @@ -1,76 +1,128 @@ - diff --git a/frontend/types/api-types/recipe.ts b/frontend/types/api-types/recipe.ts index d4fd91d6bf2c..6c3176883e5e 100644 --- a/frontend/types/api-types/recipe.ts +++ b/frontend/types/api-types/recipe.ts @@ -131,8 +131,8 @@ export interface RecipeSummary { slug?: string; image?: unknown; description?: string; - recipeCategory?: string[]; - tags?: string[]; + recipeCategory: string[]; + tags: string[]; rating?: number; dateAdded?: string; dateUpdated?: string; diff --git a/mealie/db/data_access_layer/recipe_access_model.py b/mealie/db/data_access_layer/recipe_access_model.py index 26f8e64953d8..48feda0b1e57 100644 --- a/mealie/db/data_access_layer/recipe_access_model.py +++ b/mealie/db/data_access_layer/recipe_access_model.py @@ -3,6 +3,7 @@ from typing import Any from sqlalchemy.orm import joinedload +from mealie.db.models.recipe.ingredient import RecipeIngredient from mealie.db.models.recipe.recipe import RecipeModel from mealie.db.models.recipe.settings import RecipeSettings from mealie.schema.recipe import Recipe @@ -64,7 +65,11 @@ class RecipeDataAccessModel(AccessModel[Recipe, RecipeModel]): def summary(self, group_id, start=0, limit=99999) -> Any: return ( self.session.query(RecipeModel) - .options(joinedload(RecipeModel.recipe_category), joinedload(RecipeModel.tags)) + .options( + joinedload(RecipeModel.recipe_category), + joinedload(RecipeModel.tags), + joinedload(RecipeModel.recipe_ingredient).options(joinedload(RecipeIngredient.food)), + ) .filter(RecipeModel.group_id == group_id) .offset(start) .limit(limit) diff --git a/mealie/schema/recipe/recipe.py b/mealie/schema/recipe/recipe.py index a867819afd52..090208c50008 100644 --- a/mealie/schema/recipe/recipe.py +++ b/mealie/schema/recipe/recipe.py @@ -76,6 +76,8 @@ class RecipeSummary(CamelModel): rating: Optional[int] org_url: Optional[str] = Field(None, alias="orgURL") + recipe_ingredient: Optional[list[RecipeIngredient]] = [] + date_added: Optional[datetime.date] date_updated: Optional[datetime.datetime] diff --git a/mealie/services/recipe/recipe_service.py b/mealie/services/recipe/recipe_service.py index 7bf080c120db..51a38f39c30d 100644 --- a/mealie/services/recipe/recipe_service.py +++ b/mealie/services/recipe/recipe_service.py @@ -60,7 +60,15 @@ class RecipeService(CrudHttpMixins[CreateRecipe, Recipe, Recipe], UserHttpServic def get_all(self, start=0, limit=None): items = self.db.recipes.summary(self.user.group_id, start=start, limit=limit) - return [RecipeSummary.construct(**x.__dict__) for x in items] + + new_items = [] + for item in items: + # Pydantic/FastAPI can't seem to serialize the ingredient field on thier own. + new_item = item.__dict__ + new_item["recipe_ingredient"] = [x.__dict__ for x in item.recipe_ingredient] + new_items.append(new_item) + + return [RecipeSummary.construct(**x) for x in new_items] def create_one(self, create_data: Union[Recipe, CreateRecipe]) -> Recipe: create_data = recipe_creation_factory(self.user, name=create_data.name, additional_attrs=create_data.dict())