From 7c274de778ac1f7d12e37f3fcc0eda04c1e43e1d Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Sun, 22 Sep 2024 09:59:20 -0500 Subject: [PATCH] feat: Filter Recipes By Household (and a ton of bug fixes) (#4207) Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com> --- .../Domain/Recipe/RecipeCardSection.vue | 128 ++++++++++-------- .../Domain/Recipe/RecipeDataTable.vue | 14 ++ .../Domain/Recipe/RecipeExplorerPage.vue | 91 +++++++++---- .../Domain/Recipe/RecipeIngredientEditor.vue | 4 +- .../Domain/Recipe/RecipeOrganizerDialog.vue | 2 +- .../Domain/Recipe/RecipeOrganizerSelector.vue | 4 +- .../RecipePageParts/RecipePageHeader.vue | 2 +- frontend/components/Domain/SearchFilter.vue | 73 +++++++--- frontend/components/global/CrudTable.vue | 10 ++ frontend/composables/api/index.ts | 2 +- frontend/composables/partials/types.ts | 3 + .../partials/use-actions-factory.ts | 15 +- .../composables/partials/use-store-factory.ts | 53 ++++++++ frontend/composables/recipes/use-recipes.ts | 1 + frontend/composables/store/index.ts | 11 +- .../composables/store/use-category-store.ts | 79 +++-------- frontend/composables/store/use-food-store.ts | 73 ++-------- .../composables/store/use-household-store.ts | 18 +++ frontend/composables/store/use-label-store.ts | 45 ++---- frontend/composables/store/use-tag-store.ts | 78 +++-------- frontend/composables/store/use-tool-store.ts | 79 +++-------- frontend/composables/store/use-unit-store.ts | 49 ++----- frontend/composables/use-households.ts | 6 +- .../composables/use-users/user-ratings.ts | 53 ++++---- frontend/lang/messages/en-US.json | 1 + frontend/lib/api/admin/admin-households.ts | 13 ++ frontend/lib/api/client-admin.ts | 3 + frontend/lib/api/public/explore.ts | 3 + frontend/lib/api/public/explore/households.ts | 20 +++ frontend/lib/api/user/groups.ts | 11 -- frontend/lib/api/user/households.ts | 13 +- frontend/lib/api/user/recipes/recipe.ts | 3 +- .../pages/admin/manage/households/_id.vue | 8 +- .../pages/admin/manage/households/index.vue | 4 +- frontend/pages/admin/manage/users/_id.vue | 4 +- frontend/pages/admin/manage/users/create.vue | 4 +- frontend/pages/admin/setup.vue | 23 ++-- .../_groupSlug/r/_slug/ingredient-parser.vue | 2 - .../g/_groupSlug/recipes/categories/index.vue | 8 +- .../pages/g/_groupSlug/recipes/tags/index.vue | 8 +- .../g/_groupSlug/recipes/tools/index.vue | 2 +- frontend/pages/group/data/categories.vue | 3 +- frontend/pages/group/data/foods.vue | 25 +++- frontend/pages/group/data/labels.vue | 3 +- frontend/pages/group/data/recipe-actions.vue | 1 + frontend/pages/group/data/tags.vue | 3 +- frontend/pages/group/data/tools.vue | 3 +- frontend/pages/group/data/units.vue | 31 ++++- frontend/pages/shopping-lists/_id.vue | 6 +- frontend/pages/user/_id/favorites.vue | 22 +-- mealie/repos/repository_recipes.py | 8 ++ mealie/routes/_base/base_controllers.py | 8 +- mealie/routes/explore/__init__.py | 2 + .../explore/controller_public_households.py | 35 +++++ .../explore/controller_public_recipes.py | 2 + mealie/routes/groups/__init__.py | 2 + .../groups/controller_group_households.py | 27 ++++ .../groups/controller_group_self_service.py | 21 +-- mealie/routes/recipe/recipe_crud_routes.py | 2 + .../test_public_households.py | 81 +++++++++++ .../test_group_self_service.py | 30 ++-- .../test_group_cookbooks.py | 6 +- .../test_recipe_cross_household.py | 81 +++++++++++ .../user_recipe_tests/test_recipe_crud.py | 45 ++++++ tests/utils/api_routes/__init__.py | 16 ++- 65 files changed, 896 insertions(+), 590 deletions(-) create mode 100644 frontend/composables/partials/types.ts create mode 100644 frontend/composables/partials/use-store-factory.ts create mode 100644 frontend/composables/store/use-household-store.ts create mode 100644 frontend/lib/api/admin/admin-households.ts create mode 100644 frontend/lib/api/public/explore/households.ts create mode 100644 mealie/routes/explore/controller_public_households.py create mode 100644 mealie/routes/groups/controller_group_households.py create mode 100644 tests/integration_tests/public_explorer_tests/test_public_households.py diff --git a/frontend/components/Domain/Recipe/RecipeCardSection.vue b/frontend/components/Domain/Recipe/RecipeCardSection.vue index d065e0e80d6c..62ca93d5431f 100644 --- a/frontend/components/Domain/Recipe/RecipeCardSection.vue +++ b/frontend/components/Domain/Recipe/RecipeCardSection.vue @@ -69,50 +69,52 @@ @toggle-dense-view="toggleMobileCards()" /> -
- - - - - - - - - - - - - - +
+
+ + + + + + + + + + + + + + +
+ + + +
- - - -
@@ -223,36 +225,42 @@ export default defineComponent({ const queryFilter = computed(() => { const orderBy = props.query?.orderBy || preferences.value.orderBy; - return preferences.value.filterNull && orderBy ? `${orderBy} IS NOT NULL` : null; + const orderByFilter = preferences.value.filterNull && orderBy ? `${orderBy} IS NOT NULL` : null; + + if (props.query.queryFilter && orderByFilter) { + return `(${props.query.queryFilter}) AND ${orderByFilter}`; + } else if (props.query.queryFilter) { + return props.query.queryFilter; + } else { + return orderByFilter; + } }); 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 + // we use a computed queryFilter to filter out recipes that have a null value for the property we're sorting by queryFilter.value ); } onMounted(async () => { - if (props.query) { - await initRecipes(); - ready.value = true; - } + await initRecipes(); + ready.value = true; }); - let lastQuery: string | undefined; + let lastQuery: string | undefined = JSON.stringify(props.query); watch( () => props.query, async (newValue: RecipeSearchQuery | undefined) => { const newValueString = JSON.stringify(newValue) - if (newValue && (!ready.value || lastQuery !== newValueString)) { + if (lastQuery !== newValueString) { lastQuery = newValueString; + ready.value = false; await initRecipes(); ready.value = true; } @@ -261,8 +269,12 @@ export default defineComponent({ async function initRecipes() { page.value = 1; - const newRecipes = await fetchRecipes(2); - if (!newRecipes.length) { + hasMore.value = true; + + // we double-up the first call to avoid a bug with large screens that render + // the entire first page without scrolling, preventing additional loading + const newRecipes = await fetchRecipes(page.value + 1); + if (newRecipes.length < perPage) { hasMore.value = false; } @@ -274,7 +286,7 @@ export default defineComponent({ const infiniteScroll = useThrottleFn(() => { useAsync(async () => { - if (!ready.value || !hasMore.value || loading.value) { + if (!hasMore.value || loading.value) { return; } @@ -282,9 +294,10 @@ export default defineComponent({ page.value = page.value + 1; const newRecipes = await fetchRecipes(); - if (!newRecipes.length) { + if (newRecipes.length < perPage) { hasMore.value = false; - } else { + } + if (newRecipes.length) { context.emit(APPEND_RECIPES_EVENT, newRecipes); } @@ -379,6 +392,7 @@ export default defineComponent({ displayTitleIcon, EVENTS, infiniteScroll, + ready, loading, navigateRandom, preferences, diff --git a/frontend/components/Domain/Recipe/RecipeDataTable.vue b/frontend/components/Domain/Recipe/RecipeDataTable.vue index fd8900004ced..152b08d06c60 100644 --- a/frontend/components/Domain/Recipe/RecipeDataTable.vue +++ b/frontend/components/Domain/Recipe/RecipeDataTable.vue @@ -3,6 +3,8 @@ v-model="selected" item-key="id" show-select + sort-by="dateAdded" + sort-desc :headers="headers" :items="recipes" :items-per-page="15" @@ -39,6 +41,9 @@ + @@ -132,6 +137,14 @@ export default defineComponent({ return hdrs; }); + function formatDate(date: string) { + try { + return i18n.d(Date.parse(date), "medium"); + } catch { + return ""; + } + } + // ============ // Group Members const api = useUserApi(); @@ -160,6 +173,7 @@ export default defineComponent({ groupSlug, setValue, headers, + formatDate, members, getMember, }; diff --git a/frontend/components/Domain/Recipe/RecipeExplorerPage.vue b/frontend/components/Domain/Recipe/RecipeExplorerPage.vue index 03cb0f7f2131..a9c212dff57c 100644 --- a/frontend/components/Domain/Recipe/RecipeExplorerPage.vue +++ b/frontend/components/Domain/Recipe/RecipeExplorerPage.vue @@ -53,6 +53,14 @@ {{ $t("general.foods") }} + + + + {{ $globals.icons.household }} + + {{ $t("household.households") }} + +