mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -04:00
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
This commit is contained in:
parent
85448b8a18
commit
aaeb162dd5
@ -15,49 +15,7 @@
|
|||||||
{{ $vuetify.breakpoint.xsOnly ? null : $t("general.random") }}
|
{{ $vuetify.breakpoint.xsOnly ? null : $t("general.random") }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<v-menu v-if="$listeners.sort" offset-y left>
|
<v-menu offset-y left>
|
||||||
<template #activator="{ on, attrs }">
|
|
||||||
<v-btn text :icon="$vuetify.breakpoint.xsOnly" v-bind="attrs" :loading="sortLoading" v-on="on">
|
|
||||||
<v-icon :left="!$vuetify.breakpoint.xsOnly">
|
|
||||||
{{ $globals.icons.sort }}
|
|
||||||
</v-icon>
|
|
||||||
{{ $vuetify.breakpoint.xsOnly ? null : $t("general.sort") }}
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
<v-list>
|
|
||||||
<v-list-item @click="sortRecipesFrontend(EVENTS.az)">
|
|
||||||
<v-icon left>
|
|
||||||
{{ $globals.icons.orderAlphabeticalAscending }}
|
|
||||||
</v-icon>
|
|
||||||
<v-list-item-title>{{ $t("general.sort-alphabetically") }}</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
<v-list-item @click="sortRecipesFrontend(EVENTS.rating)">
|
|
||||||
<v-icon left>
|
|
||||||
{{ $globals.icons.star }}
|
|
||||||
</v-icon>
|
|
||||||
<v-list-item-title>{{ $t("general.rating") }}</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
<v-list-item @click="sortRecipesFrontend(EVENTS.created)">
|
|
||||||
<v-icon left>
|
|
||||||
{{ $globals.icons.newBox }}
|
|
||||||
</v-icon>
|
|
||||||
<v-list-item-title>{{ $t("general.created") }}</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
<v-list-item @click="sortRecipesFrontend(EVENTS.updated)">
|
|
||||||
<v-icon left>
|
|
||||||
{{ $globals.icons.update }}
|
|
||||||
</v-icon>
|
|
||||||
<v-list-item-title>{{ $t("general.updated") }}</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
<v-list-item @click="sortRecipesFrontend(EVENTS.shuffle)">
|
|
||||||
<v-icon left>
|
|
||||||
{{ $globals.icons.shuffleVariant }}
|
|
||||||
</v-icon>
|
|
||||||
<v-list-item-title>{{ $t("general.shuffle") }}</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-menu>
|
|
||||||
<v-menu v-if="$listeners.sortRecipes" offset-y left>
|
|
||||||
<template #activator="{ on, attrs }">
|
<template #activator="{ on, attrs }">
|
||||||
<v-btn text :icon="$vuetify.breakpoint.xsOnly" v-bind="attrs" :loading="sortLoading" v-on="on">
|
<v-btn text :icon="$vuetify.breakpoint.xsOnly" v-bind="attrs" :loading="sortLoading" v-on="on">
|
||||||
<v-icon :left="!$vuetify.breakpoint.xsOnly">
|
<v-icon :left="!$vuetify.breakpoint.xsOnly">
|
||||||
@ -147,12 +105,10 @@
|
|||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="usePagination">
|
<v-card v-intersect="infiniteScroll"></v-card>
|
||||||
<v-card v-intersect="infiniteScroll"></v-card>
|
<v-fade-transition>
|
||||||
<v-fade-transition>
|
<AppLoader v-if="loading" :loading="loading" />
|
||||||
<AppLoader v-if="loading" :loading="loading" />
|
</v-fade-transition>
|
||||||
</v-fade-transition>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -172,11 +128,10 @@ import { useThrottleFn } from "@vueuse/core";
|
|||||||
import RecipeCard from "./RecipeCard.vue";
|
import RecipeCard from "./RecipeCard.vue";
|
||||||
import RecipeCardMobile from "./RecipeCardMobile.vue";
|
import RecipeCardMobile from "./RecipeCardMobile.vue";
|
||||||
import { useAsyncKey } from "~/composables/use-utils";
|
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 { Recipe } from "~/types/api-types/recipe";
|
||||||
import { useUserSortPreferences } from "~/composables/use-users/preferences";
|
import { useUserSortPreferences } from "~/composables/use-users/preferences";
|
||||||
|
|
||||||
const SORT_EVENT = "sort";
|
|
||||||
const REPLACE_RECIPES_EVENT = "replaceRecipes";
|
const REPLACE_RECIPES_EVENT = "replaceRecipes";
|
||||||
const APPEND_RECIPES_EVENT = "appendRecipes";
|
const APPEND_RECIPES_EVENT = "appendRecipes";
|
||||||
|
|
||||||
@ -206,16 +161,22 @@ export default defineComponent({
|
|||||||
type: Array as () => Recipe[],
|
type: Array as () => Recipe[],
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
usePagination: {
|
categorySlug: {
|
||||||
type: Boolean,
|
type: String,
|
||||||
default: false,
|
default: null,
|
||||||
|
},
|
||||||
|
tagSlug: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
toolSlug: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup(props, context) {
|
setup(props, context) {
|
||||||
const preferences = useUserSortPreferences();
|
const preferences = useUserSortPreferences();
|
||||||
|
|
||||||
const utils = useSorter();
|
|
||||||
|
|
||||||
const EVENTS = {
|
const EVENTS = {
|
||||||
az: "az",
|
az: "az",
|
||||||
rating: "rating",
|
rating: "rating",
|
||||||
@ -252,26 +213,30 @@ export default defineComponent({
|
|||||||
const hasMore = ref(true);
|
const hasMore = ref(true);
|
||||||
const ready = ref(false);
|
const ready = ref(false);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const category = ref<string>(props.categorySlug);
|
||||||
|
const tag = ref<string>(props.tagSlug);
|
||||||
|
const tool = ref<string>(props.toolSlug);
|
||||||
|
|
||||||
const { fetchMore } = useLazyRecipes();
|
const { fetchMore } = useLazyRecipes();
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (props.usePagination) {
|
const newRecipes = await fetchMore(
|
||||||
const newRecipes = await fetchMore(
|
page.value,
|
||||||
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
|
// 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,
|
perPage.value*2,
|
||||||
preferences.value.orderBy,
|
preferences.value.orderBy,
|
||||||
preferences.value.orderDirection
|
preferences.value.orderDirection,
|
||||||
);
|
category.value,
|
||||||
|
tag.value,
|
||||||
|
tool.value,
|
||||||
|
);
|
||||||
|
|
||||||
// since we doubled the first call, we also need to advance the page
|
// since we doubled the first call, we also need to advance the page
|
||||||
page.value = page.value + 1;
|
page.value = page.value + 1;
|
||||||
|
|
||||||
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
|
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
|
||||||
ready.value = true;
|
ready.value = true;
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const infiniteScroll = useThrottleFn(() => {
|
const infiniteScroll = useThrottleFn(() => {
|
||||||
@ -287,7 +252,10 @@ export default defineComponent({
|
|||||||
page.value,
|
page.value,
|
||||||
perPage.value,
|
perPage.value,
|
||||||
preferences.value.orderBy,
|
preferences.value.orderBy,
|
||||||
preferences.value.orderDirection
|
preferences.value.orderDirection,
|
||||||
|
category.value,
|
||||||
|
tag.value,
|
||||||
|
tool.value,
|
||||||
);
|
);
|
||||||
if (!newRecipes.length) {
|
if (!newRecipes.length) {
|
||||||
hasMore.value = false;
|
hasMore.value = false;
|
||||||
@ -299,12 +267,6 @@ export default defineComponent({
|
|||||||
}, useAsyncKey());
|
}, useAsyncKey());
|
||||||
}, 500);
|
}, 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) {
|
function sortRecipes(sortType: string) {
|
||||||
if (state.sortLoading || loading.value) {
|
if (state.sortLoading || loading.value) {
|
||||||
return;
|
return;
|
||||||
@ -351,7 +313,10 @@ export default defineComponent({
|
|||||||
page.value,
|
page.value,
|
||||||
perPage.value,
|
perPage.value,
|
||||||
preferences.value.orderBy,
|
preferences.value.orderBy,
|
||||||
preferences.value.orderDirection
|
preferences.value.orderDirection,
|
||||||
|
category.value,
|
||||||
|
tag.value,
|
||||||
|
tool.value,
|
||||||
);
|
);
|
||||||
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
|
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
|
||||||
|
|
||||||
@ -360,33 +325,6 @@ export default defineComponent({
|
|||||||
}, useAsyncKey());
|
}, 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() {
|
function toggleMobileCards() {
|
||||||
preferences.value.useMobileCards = !preferences.value.useMobileCards;
|
preferences.value.useMobileCards = !preferences.value.useMobileCards;
|
||||||
}
|
}
|
||||||
@ -400,7 +338,6 @@ export default defineComponent({
|
|||||||
navigateRandom,
|
navigateRandom,
|
||||||
preferences,
|
preferences,
|
||||||
sortRecipes,
|
sortRecipes,
|
||||||
sortRecipesFrontend,
|
|
||||||
toggleMobileCards,
|
toggleMobileCards,
|
||||||
useMobileCards,
|
useMobileCards,
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export { useFraction } from "./use-fraction";
|
export { useFraction } from "./use-fraction";
|
||||||
export { useRecipe } from "./use-recipe";
|
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 { parseIngredientText } from "./use-recipe-ingredients";
|
||||||
export { useRecipeSearch } from "./use-recipe-search";
|
export { useRecipeSearch } from "./use-recipe-search";
|
||||||
export { useTools } from "./use-recipe-tools";
|
export { useTools } from "./use-recipe-tools";
|
||||||
|
@ -6,69 +6,46 @@ import { Recipe } from "~/types/api-types/recipe";
|
|||||||
export const allRecipes = ref<Recipe[]>([]);
|
export const allRecipes = ref<Recipe[]>([]);
|
||||||
export const recentRecipes = ref<Recipe[]>([]);
|
export const recentRecipes = ref<Recipe[]>([]);
|
||||||
|
|
||||||
const rand = (n: number) => Math.floor(Math.random() * n);
|
|
||||||
|
|
||||||
function swap(t: Array<unknown>, i: number, j: number) {
|
|
||||||
const q = t[i];
|
|
||||||
t[i] = t[j];
|
|
||||||
t[j] = q;
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useSorter = () => {
|
|
||||||
function sortAToZ(list: Array<Recipe>) {
|
|
||||||
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<Recipe>) {
|
|
||||||
list.sort((a, b) => ((a.dateAdded ?? "") > (b.dateAdded ?? "") ? -1 : 1));
|
|
||||||
}
|
|
||||||
function sortByUpdated(list: Array<Recipe>) {
|
|
||||||
list.sort((a, b) => ((a.dateUpdated ?? "") > (b.dateUpdated ?? "") ? -1 : 1));
|
|
||||||
}
|
|
||||||
function sortByRating(list: Array<Recipe>) {
|
|
||||||
list.sort((a, b) => ((a.rating ?? 0) > (b.rating ?? 0) ? -1 : 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
function randomRecipe(list: Array<Recipe>): Recipe {
|
|
||||||
return list[Math.floor(Math.random() * list.length)];
|
|
||||||
}
|
|
||||||
|
|
||||||
function shuffle(list: Array<Recipe>) {
|
|
||||||
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 () {
|
export const useLazyRecipes = function () {
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
|
|
||||||
const recipes = ref<Recipe[]>([]);
|
const recipes = ref<Recipe[]>([]);
|
||||||
|
|
||||||
async function fetchMore(page: number, perPage: number, orderBy: string | null = null, orderDirection = "desc") {
|
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 });
|
const { data } = await api.recipes.getAll(page, perPage, { orderBy, orderDirection, "categories": category, "tags": tag, "tools": tool });
|
||||||
return data ? data.items : [];
|
return data ? data.items : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appendRecipes(val: Array<Recipe>) {
|
||||||
|
val.forEach((recipe) => {
|
||||||
|
recipes.value.push(recipe);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignSorted(val: Array<Recipe>) {
|
||||||
|
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<Recipe>) {
|
||||||
|
recipes.value = val;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
recipes,
|
recipes,
|
||||||
fetchMore,
|
fetchMore,
|
||||||
|
appendRecipes,
|
||||||
|
assignSorted,
|
||||||
|
removeRecipe,
|
||||||
|
replaceRecipes
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
:icon="$globals.icons.primary"
|
:icon="$globals.icons.primary"
|
||||||
:title="$t('page.all-recipes')"
|
:title="$t('page.all-recipes')"
|
||||||
:recipes="recipes"
|
:recipes="recipes"
|
||||||
:use-pagination="true"
|
|
||||||
@sortRecipes="assignSorted"
|
@sortRecipes="assignSorted"
|
||||||
@replaceRecipes="replaceRecipes"
|
@replaceRecipes="replaceRecipes"
|
||||||
@appendRecipes="appendRecipes"
|
@appendRecipes="appendRecipes"
|
||||||
@ -17,36 +16,11 @@
|
|||||||
import { defineComponent } from "@nuxtjs/composition-api";
|
import { defineComponent } from "@nuxtjs/composition-api";
|
||||||
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
||||||
import { useLazyRecipes } from "~/composables/recipes";
|
import { useLazyRecipes } from "~/composables/recipes";
|
||||||
import { Recipe } from "~/types/api-types/recipe";
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { RecipeCardSection },
|
components: { RecipeCardSection },
|
||||||
setup() {
|
setup() {
|
||||||
const { recipes, fetchMore } = useLazyRecipes();
|
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes();
|
||||||
|
|
||||||
function appendRecipes(val: Array<Recipe>) {
|
|
||||||
val.forEach((recipe) => {
|
|
||||||
recipes.value.push(recipe);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function assignSorted(val: Array<Recipe>) {
|
|
||||||
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<Recipe>) {
|
|
||||||
recipes.value = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { appendRecipes, assignSorted, recipes, removeRecipe, replaceRecipes };
|
return { appendRecipes, assignSorted, recipes, removeRecipe, replaceRecipes };
|
||||||
},
|
},
|
||||||
head() {
|
head() {
|
||||||
|
@ -4,8 +4,12 @@
|
|||||||
v-if="category"
|
v-if="category"
|
||||||
:icon="$globals.icons.tags"
|
:icon="$globals.icons.tags"
|
||||||
:title="category.name"
|
:title="category.name"
|
||||||
:recipes="category.recipes"
|
:recipes="recipes"
|
||||||
@sort="assignSorted"
|
:category-slug="category.slug"
|
||||||
|
@sortRecipes="assignSorted"
|
||||||
|
@replaceRecipes="replaceRecipes"
|
||||||
|
@appendRecipes="appendRecipes"
|
||||||
|
@delete="removeRecipe"
|
||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<v-btn icon class="mr-1">
|
<v-btn icon class="mr-1">
|
||||||
@ -54,13 +58,15 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, useAsync, useRoute, reactive, toRefs, useRouter } from "@nuxtjs/composition-api";
|
import { defineComponent, useAsync, useRoute, reactive, toRefs, useRouter } from "@nuxtjs/composition-api";
|
||||||
|
import { useLazyRecipes } from "~/composables/recipes";
|
||||||
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { Recipe } from "~/types/api-types/recipe";
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { RecipeCardSection },
|
components: { RecipeCardSection },
|
||||||
setup() {
|
setup() {
|
||||||
|
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes();
|
||||||
|
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -105,6 +111,11 @@ export default defineComponent({
|
|||||||
reset,
|
reset,
|
||||||
...toRefs(state),
|
...toRefs(state),
|
||||||
updateCategory,
|
updateCategory,
|
||||||
|
appendRecipes,
|
||||||
|
assignSorted,
|
||||||
|
recipes,
|
||||||
|
removeRecipe,
|
||||||
|
replaceRecipes,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
head() {
|
head() {
|
||||||
@ -112,12 +123,5 @@ export default defineComponent({
|
|||||||
title: this.$t("category.categories") as string,
|
title: this.$t("category.categories") as string,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
assignSorted(val: Array<Recipe>) {
|
|
||||||
if (this.category) {
|
|
||||||
this.category.recipes = val;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container>
|
<v-container>
|
||||||
<RecipeCardSection
|
<RecipeCardSection
|
||||||
v-if="tags"
|
v-if="tag"
|
||||||
:icon="$globals.icons.tags"
|
:icon="$globals.icons.tags"
|
||||||
:title="tags.name"
|
:title="tag.name"
|
||||||
:recipes="tags.recipes"
|
:recipes="recipes"
|
||||||
@sort="assignSorted"
|
:tag-slug="tag.slug"
|
||||||
|
@sortRecipes="assignSorted"
|
||||||
|
@replaceRecipes="replaceRecipes"
|
||||||
|
@appendRecipes="appendRecipes"
|
||||||
|
@delete="removeRecipe"
|
||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<v-btn icon class="mr-1">
|
<v-btn icon class="mr-1">
|
||||||
@ -16,7 +20,7 @@
|
|||||||
|
|
||||||
<template v-if="edit">
|
<template v-if="edit">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="tags.name"
|
v-model="tag.name"
|
||||||
autofocus
|
autofocus
|
||||||
single-line
|
single-line
|
||||||
dense
|
dense
|
||||||
@ -41,7 +45,7 @@
|
|||||||
<v-tooltip top>
|
<v-tooltip top>
|
||||||
<template #activator="{ on, attrs }">
|
<template #activator="{ on, attrs }">
|
||||||
<v-toolbar-title v-bind="attrs" style="cursor: pointer" class="headline" v-on="on" @click="edit = true">
|
<v-toolbar-title v-bind="attrs" style="cursor: pointer" class="headline" v-on="on" @click="edit = true">
|
||||||
{{ tags.name }}
|
{{ tag.name }}
|
||||||
</v-toolbar-title>
|
</v-toolbar-title>
|
||||||
</template>
|
</template>
|
||||||
<span> Click to Edit </span>
|
<span> Click to Edit </span>
|
||||||
@ -54,13 +58,15 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, useAsync, useRoute, reactive, toRefs, useRouter } from "@nuxtjs/composition-api";
|
import { defineComponent, useAsync, useRoute, reactive, toRefs, useRouter } from "@nuxtjs/composition-api";
|
||||||
|
import { useLazyRecipes } from "~/composables/recipes";
|
||||||
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { Recipe } from "~/types/api-types/recipe";
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { RecipeCardSection },
|
components: { RecipeCardSection },
|
||||||
setup() {
|
setup() {
|
||||||
|
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes();
|
||||||
|
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -71,7 +77,7 @@ export default defineComponent({
|
|||||||
edit: false,
|
edit: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const tags = useAsync(async () => {
|
const tag = useAsync(async () => {
|
||||||
const { data } = await api.tags.bySlug(slug);
|
const { data } = await api.tags.bySlug(slug);
|
||||||
if (data) {
|
if (data) {
|
||||||
state.initialValue = data.name;
|
state.initialValue = data.name;
|
||||||
@ -82,18 +88,18 @@ export default defineComponent({
|
|||||||
function reset() {
|
function reset() {
|
||||||
state.edit = false;
|
state.edit = false;
|
||||||
|
|
||||||
if (tags.value) {
|
if (tag.value) {
|
||||||
tags.value.name = state.initialValue;
|
tag.value.name = state.initialValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateTags() {
|
async function updateTags() {
|
||||||
state.edit = false;
|
state.edit = false;
|
||||||
|
|
||||||
if (!tags.value) {
|
if (!tag.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { data } = await api.tags.updateOne(tags.value.id, tags.value);
|
const { data } = await api.tags.updateOne(tag.value.id, tag.value);
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
router.push("/recipes/tags/" + data.slug);
|
router.push("/recipes/tags/" + data.slug);
|
||||||
@ -101,10 +107,15 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tags,
|
tag,
|
||||||
reset,
|
reset,
|
||||||
...toRefs(state),
|
...toRefs(state),
|
||||||
updateTags,
|
updateTags,
|
||||||
|
appendRecipes,
|
||||||
|
assignSorted,
|
||||||
|
recipes,
|
||||||
|
removeRecipe,
|
||||||
|
replaceRecipes,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
head() {
|
head() {
|
||||||
@ -112,12 +123,5 @@ export default defineComponent({
|
|||||||
title: this.$t("tag.tags") as string,
|
title: this.$t("tag.tags") as string,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
assignSorted(val: Array<Recipe>) {
|
|
||||||
if (this.tags) {
|
|
||||||
this.tags.recipes = val;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,6 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container>
|
<v-container>
|
||||||
<RecipeCardSection v-if="tools" :title="tools.name" :recipes="tools.recipes" @sort="assignSorted">
|
<RecipeCardSection
|
||||||
|
v-if="tool"
|
||||||
|
:icon="$globals.icons.potSteam"
|
||||||
|
:title="tool.name"
|
||||||
|
:recipes="recipes"
|
||||||
|
:tool-slug="tool.slug"
|
||||||
|
@sortRecipes="assignSorted"
|
||||||
|
@replaceRecipes="replaceRecipes"
|
||||||
|
@appendRecipes="appendRecipes"
|
||||||
|
@delete="removeRecipe"
|
||||||
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<v-btn icon class="mr-1">
|
<v-btn icon class="mr-1">
|
||||||
<v-icon dark large @click="reset">
|
<v-icon dark large @click="reset">
|
||||||
@ -10,7 +20,7 @@
|
|||||||
|
|
||||||
<template v-if="edit">
|
<template v-if="edit">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="tools.name"
|
v-model="tool.name"
|
||||||
autofocus
|
autofocus
|
||||||
single-line
|
single-line
|
||||||
dense
|
dense
|
||||||
@ -35,7 +45,7 @@
|
|||||||
<v-tooltip top>
|
<v-tooltip top>
|
||||||
<template #activator="{ on, attrs }">
|
<template #activator="{ on, attrs }">
|
||||||
<v-toolbar-title v-bind="attrs" style="cursor: pointer" class="headline" v-on="on" @click="edit = true">
|
<v-toolbar-title v-bind="attrs" style="cursor: pointer" class="headline" v-on="on" @click="edit = true">
|
||||||
{{ tools.name }}
|
{{ tool.name }}
|
||||||
</v-toolbar-title>
|
</v-toolbar-title>
|
||||||
</template>
|
</template>
|
||||||
<span> Click to Edit </span>
|
<span> Click to Edit </span>
|
||||||
@ -48,13 +58,15 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, useAsync, useRoute, reactive, toRefs, useRouter } from "@nuxtjs/composition-api";
|
import { defineComponent, useAsync, useRoute, reactive, toRefs, useRouter } from "@nuxtjs/composition-api";
|
||||||
|
import { useLazyRecipes } from "~/composables/recipes";
|
||||||
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { Recipe } from "~/types/api-types/recipe";
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { RecipeCardSection },
|
components: { RecipeCardSection },
|
||||||
setup() {
|
setup() {
|
||||||
|
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes();
|
||||||
|
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -65,7 +77,7 @@ export default defineComponent({
|
|||||||
edit: false,
|
edit: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const tools = useAsync(async () => {
|
const tool = useAsync(async () => {
|
||||||
const { data } = await api.tools.bySlug(slug);
|
const { data } = await api.tools.bySlug(slug);
|
||||||
if (data) {
|
if (data) {
|
||||||
state.initialValue = data.name;
|
state.initialValue = data.name;
|
||||||
@ -76,18 +88,18 @@ export default defineComponent({
|
|||||||
function reset() {
|
function reset() {
|
||||||
state.edit = false;
|
state.edit = false;
|
||||||
|
|
||||||
if (tools.value) {
|
if (tool.value) {
|
||||||
tools.value.name = state.initialValue;
|
tool.value.name = state.initialValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateTools() {
|
async function updateTools() {
|
||||||
state.edit = false;
|
state.edit = false;
|
||||||
|
|
||||||
if (!tools.value) {
|
if (!tool.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { data } = await api.tools.updateOne(tools.value.id, tools.value);
|
const { data } = await api.tools.updateOne(tool.value.id, tool.value);
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
router.push("/recipes/tools/" + data.slug);
|
router.push("/recipes/tools/" + data.slug);
|
||||||
@ -95,10 +107,15 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tools,
|
tool,
|
||||||
reset,
|
reset,
|
||||||
...toRefs(state),
|
...toRefs(state),
|
||||||
updateTools,
|
updateTools,
|
||||||
|
appendRecipes,
|
||||||
|
assignSorted,
|
||||||
|
recipes,
|
||||||
|
removeRecipe,
|
||||||
|
replaceRecipes,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
head() {
|
head() {
|
||||||
@ -106,12 +123,5 @@ export default defineComponent({
|
|||||||
title: this.$t("tool.tools") as string,
|
title: this.$t("tool.tools") as string,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
assignSorted(val: Array<Recipe>) {
|
|
||||||
if (this.tools) {
|
|
||||||
this.tools.recipes = val;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -136,6 +136,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
|||||||
load_food=False,
|
load_food=False,
|
||||||
categories: Optional[list[UUID4 | str]] = None,
|
categories: Optional[list[UUID4 | str]] = None,
|
||||||
tags: Optional[list[UUID4 | str]] = None,
|
tags: Optional[list[UUID4 | str]] = None,
|
||||||
|
tools: Optional[list[UUID4 | str]] = None,
|
||||||
) -> RecipePagination:
|
) -> RecipePagination:
|
||||||
q = self.session.query(self.model)
|
q = self.session.query(self.model)
|
||||||
|
|
||||||
@ -169,6 +170,14 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
|||||||
else:
|
else:
|
||||||
q = q.filter(RecipeModel.tags.any(Tag.slug == tag))
|
q = q.filter(RecipeModel.tags.any(Tag.slug == tag))
|
||||||
|
|
||||||
|
if tools:
|
||||||
|
for tool in tools:
|
||||||
|
if isinstance(tool, UUID):
|
||||||
|
q = q.filter(RecipeModel.tools.any(Tool.id == tool))
|
||||||
|
|
||||||
|
else:
|
||||||
|
q = q.filter(RecipeModel.tools.any(Tool.slug == tool))
|
||||||
|
|
||||||
q, count, total_pages = self.add_pagination_to_query(q, pagination)
|
q, count, total_pages = self.add_pagination_to_query(q, pagination)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -213,12 +213,14 @@ class RecipeController(BaseRecipeController):
|
|||||||
q: RecipePaginationQuery = Depends(RecipePaginationQuery),
|
q: RecipePaginationQuery = Depends(RecipePaginationQuery),
|
||||||
categories: Optional[list[UUID4 | str]] = Query(None),
|
categories: Optional[list[UUID4 | str]] = Query(None),
|
||||||
tags: Optional[list[UUID4 | str]] = Query(None),
|
tags: Optional[list[UUID4 | str]] = Query(None),
|
||||||
|
tools: Optional[list[UUID4 | str]] = Query(None),
|
||||||
):
|
):
|
||||||
response = self.repo.page_all(
|
response = self.repo.page_all(
|
||||||
pagination=q,
|
pagination=q,
|
||||||
load_food=q.load_food,
|
load_food=q.load_food,
|
||||||
categories=categories,
|
categories=categories,
|
||||||
tags=tags,
|
tags=tags,
|
||||||
|
tools=tools,
|
||||||
)
|
)
|
||||||
|
|
||||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||||
|
@ -4,6 +4,7 @@ from mealie.repos.repository_factory import AllRepositories
|
|||||||
from mealie.repos.repository_recipes import RepositoryRecipes
|
from mealie.repos.repository_recipes import RepositoryRecipes
|
||||||
from mealie.schema.recipe.recipe import Recipe, RecipeCategory, RecipePaginationQuery, RecipeSummary
|
from mealie.schema.recipe.recipe import Recipe, RecipeCategory, RecipePaginationQuery, RecipeSummary
|
||||||
from mealie.schema.recipe.recipe_category import CategoryOut, CategorySave, TagSave
|
from mealie.schema.recipe.recipe_category import CategoryOut, CategorySave, TagSave
|
||||||
|
from mealie.schema.recipe.recipe_tool import RecipeToolSave
|
||||||
from tests.utils.factories import random_string
|
from tests.utils.factories import random_string
|
||||||
from tests.utils.fixture_schemas import TestUser
|
from tests.utils.fixture_schemas import TestUser
|
||||||
|
|
||||||
@ -276,3 +277,84 @@ def test_recipe_repo_pagination_by_tags(database: AllRepositories, unique_user:
|
|||||||
tag_ids = [tag.id for tag in recipe_summary.tags]
|
tag_ids = [tag.id for tag in recipe_summary.tags]
|
||||||
for tag in created_tags:
|
for tag in created_tags:
|
||||||
assert tag.id in tag_ids
|
assert tag.id in tag_ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_recipe_repo_pagination_by_tools(database: AllRepositories, unique_user: TestUser):
|
||||||
|
slug1, slug2 = [random_string(10) for _ in range(2)]
|
||||||
|
|
||||||
|
tools = [
|
||||||
|
RecipeToolSave(group_id=unique_user.group_id, name=slug1, slug=slug1),
|
||||||
|
RecipeToolSave(group_id=unique_user.group_id, name=slug2, slug=slug2),
|
||||||
|
]
|
||||||
|
|
||||||
|
created_tools = [database.tools.create(tool) for tool in tools]
|
||||||
|
|
||||||
|
# Bootstrap the database with recipes
|
||||||
|
recipes = []
|
||||||
|
|
||||||
|
for i in range(10):
|
||||||
|
# None of the tools
|
||||||
|
recipes.append(
|
||||||
|
Recipe(
|
||||||
|
user_id=unique_user.user_id,
|
||||||
|
group_id=unique_user.group_id,
|
||||||
|
name=random_string(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only one of the tools
|
||||||
|
recipes.append(
|
||||||
|
Recipe(
|
||||||
|
user_id=unique_user.user_id,
|
||||||
|
group_id=unique_user.group_id,
|
||||||
|
name=random_string(),
|
||||||
|
tools=[created_tools[i % 2]],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Both of the tools
|
||||||
|
recipes.append(
|
||||||
|
Recipe(
|
||||||
|
user_id=unique_user.user_id,
|
||||||
|
group_id=unique_user.group_id,
|
||||||
|
name=random_string(),
|
||||||
|
tools=created_tools,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for recipe in recipes:
|
||||||
|
database.recipes.create(recipe)
|
||||||
|
|
||||||
|
pagination_query = RecipePaginationQuery(
|
||||||
|
page=1,
|
||||||
|
per_page=-1,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get all recipes with only one tool by UUID
|
||||||
|
tool_id = created_tools[0].id
|
||||||
|
recipes_with_one_tool = database.recipes.page_all(pagination_query, tools=[tool_id]).items
|
||||||
|
assert len(recipes_with_one_tool) == 15
|
||||||
|
|
||||||
|
for recipe_summary in recipes_with_one_tool:
|
||||||
|
tool_ids = [tool.id for tool in recipe_summary.tools]
|
||||||
|
assert tool_id in tool_ids
|
||||||
|
|
||||||
|
# Get all recipes with only one tool by slug
|
||||||
|
tool_slug = created_tools[1].slug
|
||||||
|
recipes_with_one_tool = database.recipes.page_all(pagination_query, tools=[tool_slug]).items
|
||||||
|
assert len(recipes_with_one_tool) == 15
|
||||||
|
|
||||||
|
for recipe_summary in recipes_with_one_tool:
|
||||||
|
tool_slugs = [tool.slug for tool in recipe_summary.tools]
|
||||||
|
assert tool_slug in tool_slugs
|
||||||
|
|
||||||
|
# Get all recipes with both tools
|
||||||
|
recipes_with_both_tools = database.recipes.page_all(
|
||||||
|
pagination_query, tools=[tool.id for tool in created_tools]
|
||||||
|
).items
|
||||||
|
assert len(recipes_with_both_tools) == 10
|
||||||
|
|
||||||
|
for recipe_summary in recipes_with_both_tools:
|
||||||
|
tool_ids = [tool.id for tool in recipe_summary.tools]
|
||||||
|
for tool in created_tools:
|
||||||
|
assert tool.id in tool_ids
|
||||||
|
Loading…
x
Reference in New Issue
Block a user