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 @@
-
-
-
-
-
-
-
-
- {{ $globals.icons.primary }}
-
-
-
-
- {{ name }}
- {{ description }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+ {{ $globals.icons.primary }}
+
+
+
+
+ {{ name }}
+ {{ description }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 }}
+
@@ -44,19 +49,47 @@
\ 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 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
- Advanced
-
-
-
-
-
-
-
- {{ $t("category.category-filter") }}
-
-
-
-
-
-
- {{ $t("search.tag-filter") }}
-
-
-
-
-
-
-
-
-
-
+
+
+
+ Advanced
+
+
+
+
+
+
+
+ {{ $t("category.category-filter") }}
+
+
+
+
+
+
+ {{ $t("search.tag-filter") }}
+
+
+
+
+
+ Food Filter
+
+
+
+
+ {{ data.item.name || data.item }}
+
+
+
+
+
+
+
+
+
+
+
+
-
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())