mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-06-23 15:31:37 -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>
|
</v-icon>
|
||||||
{{ $vuetify.breakpoint.xsOnly ? null : $t("general.random") }}
|
{{ $vuetify.breakpoint.xsOnly ? null : $t("general.random") }}
|
||||||
</v-btn>
|
</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>
|
<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 }">
|
<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">
|
||||||
@ -59,14 +91,19 @@
|
|||||||
</v-icon>
|
</v-icon>
|
||||||
<v-list-item-title>{{ $t("general.updated") }}</v-list-item-title>
|
<v-list-item-title>{{ $t("general.updated") }}</v-list-item-title>
|
||||||
</v-list-item>
|
</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-list>
|
||||||
</v-menu>
|
</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>
|
</v-app-bar>
|
||||||
<div v-if="recipes" class="mt-2">
|
<div v-if="recipes" class="mt-2">
|
||||||
<v-row v-if="!viewScale">
|
<v-row v-if="!viewScale">
|
||||||
@ -110,17 +147,37 @@
|
|||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<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 RecipeCard from "./RecipeCard.vue";
|
||||||
import RecipeCardMobile from "./RecipeCardMobile.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";
|
import { Recipe } from "~/types/api-types/recipe";
|
||||||
|
|
||||||
const SORT_EVENT = "sort";
|
const SORT_EVENT = "sort";
|
||||||
|
const REPLACE_RECIPES_EVENT = "replaceRecipes";
|
||||||
|
const APPEND_RECIPES_EVENT = "appendRecipes";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@ -148,6 +205,10 @@ export default defineComponent({
|
|||||||
type: Array as () => Recipe[],
|
type: Array as () => Recipe[],
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
usePagination: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
setup(props, context) {
|
setup(props, context) {
|
||||||
const mobileCards = ref(false);
|
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) {
|
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;
|
state.sortLoading = true;
|
||||||
const sortTarget = [...props.recipes];
|
const sortTarget = [...props.recipes];
|
||||||
switch (sortType) {
|
switch (sortType) {
|
||||||
@ -217,8 +385,11 @@ export default defineComponent({
|
|||||||
EVENTS,
|
EVENTS,
|
||||||
viewScale,
|
viewScale,
|
||||||
displayTitleIcon,
|
displayTitleIcon,
|
||||||
|
infiniteScroll,
|
||||||
|
loading,
|
||||||
navigateRandom,
|
navigateRandom,
|
||||||
sortRecipes,
|
sortRecipes,
|
||||||
|
sortRecipesFrontend,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -63,11 +63,7 @@ export const useLazyRecipes = function () {
|
|||||||
|
|
||||||
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") {
|
||||||
const { data } = await api.recipes.getAll(page, perPage, { orderBy, orderDirection });
|
const { data } = await api.recipes.getAll(page, perPage, { orderBy, orderDirection });
|
||||||
if (data) {
|
return data ? data.items : [];
|
||||||
data.items.forEach((recipe) => {
|
|
||||||
recipes.value?.push(recipe);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -4,48 +4,35 @@
|
|||||||
: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"
|
||||||
|
@replaceRecipes="replaceRecipes"
|
||||||
|
@appendRecipes="appendRecipes"
|
||||||
@delete="removeRecipe"
|
@delete="removeRecipe"
|
||||||
></RecipeCardSection>
|
></RecipeCardSection>
|
||||||
<v-card v-intersect="infiniteScroll"></v-card>
|
|
||||||
<v-fade-transition>
|
|
||||||
<AppLoader v-if="loading" :loading="loading" />
|
|
||||||
</v-fade-transition>
|
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, onMounted, ref } from "@nuxtjs/composition-api";
|
import { defineComponent } from "@nuxtjs/composition-api";
|
||||||
import { useThrottleFn } from "@vueuse/core";
|
|
||||||
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 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();
|
const { recipes, fetchMore } = useLazyRecipes();
|
||||||
|
|
||||||
onMounted(async () => {
|
function appendRecipes(val: Array<Recipe>) {
|
||||||
await fetchMore(page.value, perPage.value, orderBy, orderDirection);
|
val.forEach((recipe) => {
|
||||||
ready.value = true;
|
recipes.value.push(recipe);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const infiniteScroll = useThrottleFn(() => {
|
function assignSorted(val: Array<Recipe>) {
|
||||||
if (!ready.value) {
|
recipes.value = val;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
loading.value = true;
|
|
||||||
page.value = page.value + 1;
|
|
||||||
fetchMore(page.value, perPage.value, orderBy, orderDirection);
|
|
||||||
loading.value = false;
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
function removeRecipe(slug: string) {
|
function removeRecipe(slug: string) {
|
||||||
for (let i = 0; i < recipes?.value?.length; i++) {
|
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() {
|
head() {
|
||||||
return {
|
return {
|
||||||
|
@ -30,7 +30,7 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
head: {
|
head: {
|
||||||
title: "Tags",
|
title: "Categories",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user