From aaeb162dd57505161ba85fb517676b56f693a7cb Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Sat, 20 Aug 2022 13:59:49 -0500 Subject: [PATCH] feat: unify recipe card sections (#1560) * removed unused import * moved categories/tags to new recipe card section * nuked old frontend sort code minor refactoring * bug fixes * added backend recipes filter for tools * removed debug log * removed unusued props * fixed sort for recipes by tool * added tests for getting recipes by tool --- .../Domain/Recipe/RecipeCardSection.vue | 147 +++++------------- frontend/composables/recipes/index.ts | 2 +- frontend/composables/recipes/use-recipes.ts | 81 ++++------ frontend/pages/recipes/all.vue | 28 +--- frontend/pages/recipes/categories/_slug.vue | 24 +-- frontend/pages/recipes/tags/_slug.vue | 44 +++--- frontend/pages/recipes/tools/_slug.vue | 44 ++++-- mealie/repos/repository_recipes.py | 9 ++ mealie/routes/recipe/recipe_crud_routes.py | 2 + .../test_recipe_repository.py | 82 ++++++++++ 10 files changed, 231 insertions(+), 232 deletions(-) diff --git a/frontend/components/Domain/Recipe/RecipeCardSection.vue b/frontend/components/Domain/Recipe/RecipeCardSection.vue index 123ce953ad6a..5faad8e30ec9 100644 --- a/frontend/components/Domain/Recipe/RecipeCardSection.vue +++ b/frontend/components/Domain/Recipe/RecipeCardSection.vue @@ -15,49 +15,7 @@ {{ $vuetify.breakpoint.xsOnly ? null : $t("general.random") }} - - - - - - {{ $globals.icons.orderAlphabeticalAscending }} - - {{ $t("general.sort-alphabetically") }} - - - - {{ $globals.icons.star }} - - {{ $t("general.rating") }} - - - - {{ $globals.icons.newBox }} - - {{ $t("general.created") }} - - - - {{ $globals.icons.update }} - - {{ $t("general.updated") }} - - - - {{ $globals.icons.shuffleVariant }} - - {{ $t("general.shuffle") }} - - - - + @@ -172,11 +128,10 @@ import { useThrottleFn } from "@vueuse/core"; import RecipeCard from "./RecipeCard.vue"; import RecipeCardMobile from "./RecipeCardMobile.vue"; import { useAsyncKey } from "~/composables/use-utils"; -import { useLazyRecipes, useSorter } from "~/composables/recipes"; +import { useLazyRecipes } from "~/composables/recipes"; import { Recipe } from "~/types/api-types/recipe"; import { useUserSortPreferences } from "~/composables/use-users/preferences"; -const SORT_EVENT = "sort"; const REPLACE_RECIPES_EVENT = "replaceRecipes"; const APPEND_RECIPES_EVENT = "appendRecipes"; @@ -206,16 +161,22 @@ export default defineComponent({ type: Array as () => Recipe[], default: () => [], }, - usePagination: { - type: Boolean, - default: false, + categorySlug: { + type: String, + default: null, + }, + tagSlug: { + type: String, + default: null, + }, + toolSlug: { + type: String, + default: null, }, }, setup(props, context) { const preferences = useUserSortPreferences(); - const utils = useSorter(); - const EVENTS = { az: "az", rating: "rating", @@ -252,26 +213,30 @@ export default defineComponent({ const hasMore = ref(true); const ready = ref(false); const loading = ref(false); + const category = ref(props.categorySlug); + const tag = ref(props.tagSlug); + const tool = ref(props.toolSlug); const { fetchMore } = useLazyRecipes(); onMounted(async () => { - if (props.usePagination) { - const newRecipes = await fetchMore( - page.value, + 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 - ); + // 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, + category.value, + tag.value, + tool.value, + ); - // since we doubled the first call, we also need to advance the page - page.value = page.value + 1; + // 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; - } + context.emit(REPLACE_RECIPES_EVENT, newRecipes); + ready.value = true; }); const infiniteScroll = useThrottleFn(() => { @@ -287,7 +252,10 @@ export default defineComponent({ page.value, perPage.value, preferences.value.orderBy, - preferences.value.orderDirection + preferences.value.orderDirection, + category.value, + tag.value, + tool.value, ); if (!newRecipes.length) { hasMore.value = false; @@ -299,12 +267,6 @@ export default defineComponent({ }, useAsyncKey()); }, 500); - /** - * sortRecipes helps filter using the API. This will eventually replace the sortRecipesFrontend function which pulls all recipes - * (without pagination) and does the sorting in the frontend. - * TODO: remove sortRecipesFrontend and remove duplicate "sortRecipes" section in the template (above) - * @param sortType - */ function sortRecipes(sortType: string) { if (state.sortLoading || loading.value) { return; @@ -351,7 +313,10 @@ export default defineComponent({ page.value, perPage.value, preferences.value.orderBy, - preferences.value.orderDirection + preferences.value.orderDirection, + category.value, + tag.value, + tool.value, ); context.emit(REPLACE_RECIPES_EVENT, newRecipes); @@ -360,33 +325,6 @@ export default defineComponent({ }, useAsyncKey()); } - function sortRecipesFrontend(sortType: string) { - state.sortLoading = true; - const sortTarget = [...props.recipes]; - switch (sortType) { - case EVENTS.az: - utils.sortAToZ(sortTarget); - break; - case EVENTS.rating: - utils.sortByRating(sortTarget); - break; - case EVENTS.created: - utils.sortByCreated(sortTarget); - break; - case EVENTS.updated: - utils.sortByUpdated(sortTarget); - break; - case EVENTS.shuffle: - utils.shuffle(sortTarget); - break; - default: - console.log("Unknown Event", sortType); - return; - } - context.emit(SORT_EVENT, sortTarget); - state.sortLoading = false; - } - function toggleMobileCards() { preferences.value.useMobileCards = !preferences.value.useMobileCards; } @@ -400,7 +338,6 @@ export default defineComponent({ navigateRandom, preferences, sortRecipes, - sortRecipesFrontend, toggleMobileCards, useMobileCards, }; diff --git a/frontend/composables/recipes/index.ts b/frontend/composables/recipes/index.ts index 364ffe915575..e632cc4cfc21 100644 --- a/frontend/composables/recipes/index.ts +++ b/frontend/composables/recipes/index.ts @@ -1,6 +1,6 @@ export { useFraction } from "./use-fraction"; export { useRecipe } from "./use-recipe"; -export { useRecipes, recentRecipes, allRecipes, useLazyRecipes, useSorter } from "./use-recipes"; +export { useRecipes, recentRecipes, allRecipes, useLazyRecipes } from "./use-recipes"; export { parseIngredientText } from "./use-recipe-ingredients"; export { useRecipeSearch } from "./use-recipe-search"; export { useTools } from "./use-recipe-tools"; diff --git a/frontend/composables/recipes/use-recipes.ts b/frontend/composables/recipes/use-recipes.ts index d6b62f05b168..176507ea609d 100644 --- a/frontend/composables/recipes/use-recipes.ts +++ b/frontend/composables/recipes/use-recipes.ts @@ -6,69 +6,46 @@ import { Recipe } from "~/types/api-types/recipe"; export const allRecipes = ref([]); export const recentRecipes = ref([]); -const rand = (n: number) => Math.floor(Math.random() * n); - -function swap(t: Array, i: number, j: number) { - const q = t[i]; - t[i] = t[j]; - t[j] = q; - return t; -} - -export const useSorter = () => { - function sortAToZ(list: Array) { - list.sort((a, b) => { - const textA: string = a.name?.toUpperCase() ?? ""; - const textB: string = b.name?.toUpperCase() ?? ""; - return textA < textB ? -1 : textA > textB ? 1 : 0; - }); - } - function sortByCreated(list: Array) { - list.sort((a, b) => ((a.dateAdded ?? "") > (b.dateAdded ?? "") ? -1 : 1)); - } - function sortByUpdated(list: Array) { - list.sort((a, b) => ((a.dateUpdated ?? "") > (b.dateUpdated ?? "") ? -1 : 1)); - } - function sortByRating(list: Array) { - list.sort((a, b) => ((a.rating ?? 0) > (b.rating ?? 0) ? -1 : 1)); - } - - function randomRecipe(list: Array): Recipe { - return list[Math.floor(Math.random() * list.length)]; - } - - function shuffle(list: Array) { - let last = list.length; - let n; - while (last > 0) { - n = rand(last); - swap(list, n, --last); - } - } - - return { - sortAToZ, - sortByCreated, - sortByUpdated, - sortByRating, - randomRecipe, - shuffle, - }; -}; - export const useLazyRecipes = function () { const api = useUserApi(); const recipes = ref([]); - async function fetchMore(page: number, perPage: number, orderBy: string | null = null, orderDirection = "desc") { - const { data } = await api.recipes.getAll(page, perPage, { orderBy, orderDirection }); + async function fetchMore(page: number, perPage: number, orderBy: string | null = null, orderDirection = "desc", category: string | null = null, tag: string | null = null, tool: string | null = null) { + const { data } = await api.recipes.getAll(page, perPage, { orderBy, orderDirection, "categories": category, "tags": tag, "tools": tool }); return data ? data.items : []; } + function appendRecipes(val: Array) { + val.forEach((recipe) => { + recipes.value.push(recipe); + }); + } + + function assignSorted(val: Array) { + recipes.value = val; + } + + function removeRecipe(slug: string) { + for (let i = 0; i < recipes?.value?.length; i++) { + if (recipes?.value[i].slug === slug) { + recipes?.value.splice(i, 1); + break; + } + } + } + + function replaceRecipes(val: Array) { + recipes.value = val; + } + return { recipes, fetchMore, + appendRecipes, + assignSorted, + removeRecipe, + replaceRecipes }; }; diff --git a/frontend/pages/recipes/all.vue b/frontend/pages/recipes/all.vue index bfd22736b246..40c4984d5806 100644 --- a/frontend/pages/recipes/all.vue +++ b/frontend/pages/recipes/all.vue @@ -4,7 +4,6 @@ :icon="$globals.icons.primary" :title="$t('page.all-recipes')" :recipes="recipes" - :use-pagination="true" @sortRecipes="assignSorted" @replaceRecipes="replaceRecipes" @appendRecipes="appendRecipes" @@ -17,36 +16,11 @@ import { defineComponent } from "@nuxtjs/composition-api"; import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue"; import { useLazyRecipes } from "~/composables/recipes"; -import { Recipe } from "~/types/api-types/recipe"; export default defineComponent({ components: { RecipeCardSection }, setup() { - const { recipes, fetchMore } = useLazyRecipes(); - - function appendRecipes(val: Array) { - val.forEach((recipe) => { - recipes.value.push(recipe); - }); - } - - function assignSorted(val: Array) { - recipes.value = val; - } - - function removeRecipe(slug: string) { - for (let i = 0; i < recipes?.value?.length; i++) { - if (recipes?.value[i].slug === slug) { - recipes?.value.splice(i, 1); - break; - } - } - } - - function replaceRecipes(val: Array) { - recipes.value = val; - } - + const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(); return { appendRecipes, assignSorted, recipes, removeRecipe, replaceRecipes }; }, head() { diff --git a/frontend/pages/recipes/categories/_slug.vue b/frontend/pages/recipes/categories/_slug.vue index 95048d9fb994..a2a8f425c5dd 100644 --- a/frontend/pages/recipes/categories/_slug.vue +++ b/frontend/pages/recipes/categories/_slug.vue @@ -4,8 +4,12 @@ v-if="category" :icon="$globals.icons.tags" :title="category.name" - :recipes="category.recipes" - @sort="assignSorted" + :recipes="recipes" + :category-slug="category.slug" + @sortRecipes="assignSorted" + @replaceRecipes="replaceRecipes" + @appendRecipes="appendRecipes" + @delete="removeRecipe" >