mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-05-24 01:12:54 -04:00
feat: restore frontend sorting for all recipes (#1497)
* fixed typo * merged "all recipes" pagination into recipe card created custom sort card for all recipes refactored backend calls for all recipes to sort/paginate * frontend lint fixes * restored recipes reference * replaced "this" with reference * fix linting errors * re-order context menu * add todo Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
This commit is contained in:
parent
703ee32653
commit
07fef8af9f
@ -14,18 +14,50 @@
|
||||
</v-icon>
|
||||
{{ $vuetify.breakpoint.xsOnly ? null : $t("general.random") }}
|
||||
</v-btn>
|
||||
<ContextMenu
|
||||
v-if="!$vuetify.breakpoint.xsOnly"
|
||||
:items="[
|
||||
{
|
||||
title: 'Toggle View',
|
||||
icon: $globals.icons.eye,
|
||||
event: 'toggle-dense-view',
|
||||
},
|
||||
]"
|
||||
@toggle-dense-view="mobileCards = !mobileCards"
|
||||
/>
|
||||
|
||||
<v-menu v-if="$listeners.sort" 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 }">
|
||||
<v-btn text :icon="$vuetify.breakpoint.xsOnly" v-bind="attrs" :loading="sortLoading" v-on="on">
|
||||
<v-icon :left="!$vuetify.breakpoint.xsOnly">
|
||||
@ -59,14 +91,19 @@
|
||||
</v-icon>
|
||||
<v-list-item-title>{{ $t("general.updated") }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="sortRecipes(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>
|
||||
<ContextMenu
|
||||
v-if="!$vuetify.breakpoint.xsOnly"
|
||||
:items="[
|
||||
{
|
||||
title: 'Toggle View',
|
||||
icon: $globals.icons.eye,
|
||||
event: 'toggle-dense-view',
|
||||
},
|
||||
]"
|
||||
@toggle-dense-view="mobileCards = !mobileCards"
|
||||
/>
|
||||
</v-app-bar>
|
||||
<div v-if="recipes" class="mt-2">
|
||||
<v-row v-if="!viewScale">
|
||||
@ -110,17 +147,37 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
<div v-if="usePagination">
|
||||
<v-card v-intersect="infiniteScroll"></v-card>
|
||||
<v-fade-transition>
|
||||
<AppLoader v-if="loading" :loading="loading" />
|
||||
</v-fade-transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, reactive, toRefs, useContext, useRouter, ref } from "@nuxtjs/composition-api";
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
onMounted,
|
||||
reactive,
|
||||
ref,
|
||||
toRefs,
|
||||
useAsync,
|
||||
useContext,
|
||||
useRouter,
|
||||
} from "@nuxtjs/composition-api";
|
||||
import { useThrottleFn } from "@vueuse/core";
|
||||
import RecipeCard from "./RecipeCard.vue";
|
||||
import RecipeCardMobile from "./RecipeCardMobile.vue";
|
||||
import { useSorter } from "~/composables/recipes";
|
||||
import { useAsyncKey } from "~/composables/use-utils";
|
||||
import { useLazyRecipes, useSorter } from "~/composables/recipes";
|
||||
import { Recipe } from "~/types/api-types/recipe";
|
||||
|
||||
const SORT_EVENT = "sort";
|
||||
const REPLACE_RECIPES_EVENT = "replaceRecipes";
|
||||
const APPEND_RECIPES_EVENT = "appendRecipes";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@ -148,6 +205,10 @@ export default defineComponent({
|
||||
type: Array as () => Recipe[],
|
||||
default: () => [],
|
||||
},
|
||||
usePagination: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const mobileCards = ref(false);
|
||||
@ -184,7 +245,114 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
const page = ref(1);
|
||||
const perPage = ref(30);
|
||||
const orderBy = ref("name");
|
||||
const orderDirection = ref("asc");
|
||||
const hasMore = ref(true);
|
||||
|
||||
const ready = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
const { recipes, fetchMore } = useLazyRecipes();
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.usePagination) {
|
||||
const newRecipes = await fetchMore(page.value, perPage.value, orderBy.value, orderDirection.value);
|
||||
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
|
||||
ready.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
const infiniteScroll = useThrottleFn(() => {
|
||||
useAsync(async () => {
|
||||
if (!ready.value || !hasMore.value || loading.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
page.value = page.value + 1;
|
||||
|
||||
const newRecipes = await fetchMore(page.value, perPage.value, orderBy.value, orderDirection.value);
|
||||
if (!newRecipes.length) {
|
||||
hasMore.value = false;
|
||||
} else {
|
||||
context.emit(APPEND_RECIPES_EVENT, newRecipes);
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
}, 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)
|
||||
TODO: use indicator to show asc / desc order
|
||||
*/
|
||||
|
||||
function sortRecipes(sortType: string) {
|
||||
if (state.sortLoading || loading.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (sortType) {
|
||||
case EVENTS.az:
|
||||
if (orderBy.value !== "name") {
|
||||
orderBy.value = "name";
|
||||
orderDirection.value = "asc";
|
||||
} else {
|
||||
orderDirection.value = orderDirection.value === "asc" ? "desc" : "asc";
|
||||
}
|
||||
break;
|
||||
case EVENTS.rating:
|
||||
if (orderBy.value !== "rating") {
|
||||
orderBy.value = "rating";
|
||||
orderDirection.value = "desc";
|
||||
} else {
|
||||
orderDirection.value = orderDirection.value === "asc" ? "desc" : "asc";
|
||||
}
|
||||
break;
|
||||
case EVENTS.created:
|
||||
if (orderBy.value !== "created_at") {
|
||||
orderBy.value = "created_at";
|
||||
orderDirection.value = "desc";
|
||||
} else {
|
||||
orderDirection.value = orderDirection.value === "asc" ? "desc" : "asc";
|
||||
}
|
||||
break;
|
||||
case EVENTS.updated:
|
||||
if (orderBy.value !== "update_at") {
|
||||
orderBy.value = "update_at";
|
||||
orderDirection.value = "desc";
|
||||
} else {
|
||||
orderDirection.value = orderDirection.value === "asc" ? "desc" : "asc";
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.log("Unknown Event", sortType);
|
||||
return;
|
||||
}
|
||||
|
||||
useAsync(async () => {
|
||||
// reset pagination
|
||||
page.value = 1;
|
||||
hasMore.value = true;
|
||||
|
||||
state.sortLoading = true;
|
||||
loading.value = true;
|
||||
|
||||
// fetch new recipes
|
||||
const newRecipes = await fetchMore(page.value, perPage.value, orderBy.value, orderDirection.value);
|
||||
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
|
||||
|
||||
state.sortLoading = false;
|
||||
loading.value = false;
|
||||
}, useAsyncKey());
|
||||
}
|
||||
|
||||
function sortRecipesFrontend(sortType: string) {
|
||||
state.sortLoading = true;
|
||||
const sortTarget = [...props.recipes];
|
||||
switch (sortType) {
|
||||
@ -217,8 +385,11 @@ export default defineComponent({
|
||||
EVENTS,
|
||||
viewScale,
|
||||
displayTitleIcon,
|
||||
infiniteScroll,
|
||||
loading,
|
||||
navigateRandom,
|
||||
sortRecipes,
|
||||
sortRecipesFrontend,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -63,11 +63,7 @@ export const useLazyRecipes = function () {
|
||||
|
||||
async function fetchMore(page: number, perPage: number, orderBy: string | null = null, orderDirection = "desc") {
|
||||
const { data } = await api.recipes.getAll(page, perPage, { orderBy, orderDirection });
|
||||
if (data) {
|
||||
data.items.forEach((recipe) => {
|
||||
recipes.value?.push(recipe);
|
||||
});
|
||||
}
|
||||
return data ? data.items : [];
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -4,48 +4,35 @@
|
||||
:icon="$globals.icons.primary"
|
||||
:title="$t('page.all-recipes')"
|
||||
:recipes="recipes"
|
||||
:use-pagination="true"
|
||||
@sortRecipes="assignSorted"
|
||||
@replaceRecipes="replaceRecipes"
|
||||
@appendRecipes="appendRecipes"
|
||||
@delete="removeRecipe"
|
||||
></RecipeCardSection>
|
||||
<v-card v-intersect="infiniteScroll"></v-card>
|
||||
<v-fade-transition>
|
||||
<AppLoader v-if="loading" :loading="loading" />
|
||||
</v-fade-transition>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, ref } from "@nuxtjs/composition-api";
|
||||
import { useThrottleFn } from "@vueuse/core";
|
||||
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 page = ref(1);
|
||||
const perPage = ref(30);
|
||||
const orderBy = "name";
|
||||
const orderDirection = "asc";
|
||||
|
||||
const ready = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
const { recipes, fetchMore } = useLazyRecipes();
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchMore(page.value, perPage.value, orderBy, orderDirection);
|
||||
ready.value = true;
|
||||
});
|
||||
function appendRecipes(val: Array<Recipe>) {
|
||||
val.forEach((recipe) => {
|
||||
recipes.value.push(recipe);
|
||||
});
|
||||
}
|
||||
|
||||
const infiniteScroll = useThrottleFn(() => {
|
||||
if (!ready.value) {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
page.value = page.value + 1;
|
||||
fetchMore(page.value, perPage.value, orderBy, orderDirection);
|
||||
loading.value = false;
|
||||
}, 500);
|
||||
function assignSorted(val: Array<Recipe>) {
|
||||
recipes.value = val;
|
||||
}
|
||||
|
||||
function removeRecipe(slug: string) {
|
||||
for (let i = 0; i < recipes?.value?.length; i++) {
|
||||
@ -56,7 +43,11 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
return { recipes, infiniteScroll, loading, removeRecipe };
|
||||
function replaceRecipes(val: Array<Recipe>) {
|
||||
recipes.value = val;
|
||||
}
|
||||
|
||||
return { appendRecipes, assignSorted, recipes, removeRecipe, replaceRecipes };
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
|
@ -30,7 +30,7 @@ export default defineComponent({
|
||||
};
|
||||
},
|
||||
head: {
|
||||
title: "Tags",
|
||||
title: "Categories",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
Loading…
x
Reference in New Issue
Block a user