mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-06-02 21:25:45 -04:00
fix: 2148 infinite scroll on search (#2173)
* refactor recipe card section to use unified query construct * rework search page so it uses lazy-loading of RecipeCardSection * remove RecipeQuery again * prettier reformatting * remove recipes/all page * remove max results setting from search * fix typing issues
This commit is contained in:
parent
afbee3a078
commit
541cdc79aa
@ -53,7 +53,7 @@
|
|||||||
<v-icon left>
|
<v-icon left>
|
||||||
{{ $globals.icons.chefHat }}
|
{{ $globals.icons.chefHat }}
|
||||||
</v-icon>
|
</v-icon>
|
||||||
<v-list-item-title>{{ $t('general.last-made') }}</v-list-item-title>
|
<v-list-item-title>{{ $t("general.last-made") }}</v-list-item-title>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
@ -129,6 +129,7 @@ import {
|
|||||||
useAsync,
|
useAsync,
|
||||||
useContext,
|
useContext,
|
||||||
useRouter,
|
useRouter,
|
||||||
|
watch,
|
||||||
} from "@nuxtjs/composition-api";
|
} from "@nuxtjs/composition-api";
|
||||||
import { useThrottleFn } from "@vueuse/core";
|
import { useThrottleFn } from "@vueuse/core";
|
||||||
import RecipeCard from "./RecipeCard.vue";
|
import RecipeCard from "./RecipeCard.vue";
|
||||||
@ -137,6 +138,7 @@ import { useAsyncKey } from "~/composables/use-utils";
|
|||||||
import { useLazyRecipes } from "~/composables/recipes";
|
import { useLazyRecipes } from "~/composables/recipes";
|
||||||
import { Recipe } from "~/lib/api/types/recipe";
|
import { Recipe } from "~/lib/api/types/recipe";
|
||||||
import { useUserSortPreferences } from "~/composables/use-users/preferences";
|
import { useUserSortPreferences } from "~/composables/use-users/preferences";
|
||||||
|
import { RecipeSearchQuery } from "~/lib/api/user/recipes/recipe";
|
||||||
|
|
||||||
const REPLACE_RECIPES_EVENT = "replaceRecipes";
|
const REPLACE_RECIPES_EVENT = "replaceRecipes";
|
||||||
const APPEND_RECIPES_EVENT = "appendRecipes";
|
const APPEND_RECIPES_EVENT = "appendRecipes";
|
||||||
@ -167,26 +169,10 @@ export default defineComponent({
|
|||||||
type: Array as () => Recipe[],
|
type: Array as () => Recipe[],
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
cookbookSlug: {
|
query: {
|
||||||
type: String,
|
type: Object as () => RecipeSearchQuery,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
categorySlug: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
tagSlug: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
toolSlug: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
skipLoad: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
setup(props, context) {
|
setup(props, context) {
|
||||||
const preferences = useUserSortPreferences();
|
const preferences = useUserSortPreferences();
|
||||||
@ -224,45 +210,59 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const page = ref(1);
|
const page = ref(1);
|
||||||
const perPage = ref(32);
|
const perPage = 32;
|
||||||
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 cookbook = ref<string>(props.cookbookSlug);
|
|
||||||
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 () => {
|
const queryFilter = computed(() => {
|
||||||
if (props.skipLoad) {
|
const orderBy = props.query?.orderBy || preferences.value.orderBy;
|
||||||
return;
|
return preferences.value.filterNull && orderBy ? `${orderBy} <> null` : null;
|
||||||
}
|
|
||||||
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,
|
|
||||||
cookbook.value,
|
|
||||||
category.value,
|
|
||||||
tag.value,
|
|
||||||
tool.value,
|
|
||||||
|
|
||||||
// filter out recipes that have a null value for the property we're sorting by
|
|
||||||
preferences.value.filterNull && preferences.value.orderBy ? `${preferences.value.orderBy} <> null` : null
|
|
||||||
);
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
queryFilter.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (props.query) {
|
||||||
|
const newRecipes = await fetchRecipes(2);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.query,
|
||||||
|
async (newValue: RecipeSearchQuery | undefined) => {
|
||||||
|
if (newValue) {
|
||||||
|
page.value = 1;
|
||||||
|
const newRecipes = await fetchRecipes(2);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const infiniteScroll = useThrottleFn(() => {
|
const infiniteScroll = useThrottleFn(() => {
|
||||||
useAsync(async () => {
|
useAsync(async () => {
|
||||||
if (!ready.value || !hasMore.value || loading.value) {
|
if (!ready.value || !hasMore.value || loading.value) {
|
||||||
@ -272,19 +272,7 @@ export default defineComponent({
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
page.value = page.value + 1;
|
page.value = page.value + 1;
|
||||||
|
|
||||||
const newRecipes = await fetchMore(
|
const newRecipes = await fetchRecipes();
|
||||||
page.value,
|
|
||||||
perPage.value,
|
|
||||||
preferences.value.orderBy,
|
|
||||||
preferences.value.orderDirection,
|
|
||||||
cookbook.value,
|
|
||||||
category.value,
|
|
||||||
tag.value,
|
|
||||||
tool.value,
|
|
||||||
|
|
||||||
// filter out recipes that have a null value for the property we're sorting by
|
|
||||||
preferences.value.filterNull && preferences.value.orderBy ? `${preferences.value.orderBy} <> null` : null
|
|
||||||
);
|
|
||||||
if (!newRecipes.length) {
|
if (!newRecipes.length) {
|
||||||
hasMore.value = false;
|
hasMore.value = false;
|
||||||
} else {
|
} else {
|
||||||
@ -300,7 +288,13 @@ export default defineComponent({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setter(orderBy: string, ascIcon: string, descIcon: string, defaultOrderDirection = "asc", filterNull = false) {
|
function setter(
|
||||||
|
orderBy: string,
|
||||||
|
ascIcon: string,
|
||||||
|
descIcon: string,
|
||||||
|
defaultOrderDirection = "asc",
|
||||||
|
filterNull = false
|
||||||
|
) {
|
||||||
if (preferences.value.orderBy !== orderBy) {
|
if (preferences.value.orderBy !== orderBy) {
|
||||||
preferences.value.orderBy = orderBy;
|
preferences.value.orderBy = orderBy;
|
||||||
preferences.value.orderDirection = defaultOrderDirection;
|
preferences.value.orderDirection = defaultOrderDirection;
|
||||||
@ -313,19 +307,37 @@ export default defineComponent({
|
|||||||
|
|
||||||
switch (sortType) {
|
switch (sortType) {
|
||||||
case EVENTS.az:
|
case EVENTS.az:
|
||||||
setter("name", $globals.icons.sortAlphabeticalAscending, $globals.icons.sortAlphabeticalDescending, "asc", false);
|
setter(
|
||||||
|
"name",
|
||||||
|
$globals.icons.sortAlphabeticalAscending,
|
||||||
|
$globals.icons.sortAlphabeticalDescending,
|
||||||
|
"asc",
|
||||||
|
false
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case EVENTS.rating:
|
case EVENTS.rating:
|
||||||
setter("rating", $globals.icons.sortAscending, $globals.icons.sortDescending, "desc", true);
|
setter("rating", $globals.icons.sortAscending, $globals.icons.sortDescending, "desc", true);
|
||||||
break;
|
break;
|
||||||
case EVENTS.created:
|
case EVENTS.created:
|
||||||
setter("created_at", $globals.icons.sortCalendarAscending, $globals.icons.sortCalendarDescending, "desc", false);
|
setter(
|
||||||
|
"created_at",
|
||||||
|
$globals.icons.sortCalendarAscending,
|
||||||
|
$globals.icons.sortCalendarDescending,
|
||||||
|
"desc",
|
||||||
|
false
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case EVENTS.updated:
|
case EVENTS.updated:
|
||||||
setter("update_at", $globals.icons.sortClockAscending, $globals.icons.sortClockDescending, "desc", false);
|
setter("update_at", $globals.icons.sortClockAscending, $globals.icons.sortClockDescending, "desc", false);
|
||||||
break;
|
break;
|
||||||
case EVENTS.lastMade:
|
case EVENTS.lastMade:
|
||||||
setter("last_made", $globals.icons.sortCalendarAscending, $globals.icons.sortCalendarDescending, "desc", true);
|
setter(
|
||||||
|
"last_made",
|
||||||
|
$globals.icons.sortCalendarAscending,
|
||||||
|
$globals.icons.sortCalendarDescending,
|
||||||
|
"desc",
|
||||||
|
true
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log("Unknown Event", sortType);
|
console.log("Unknown Event", sortType);
|
||||||
@ -341,19 +353,7 @@ export default defineComponent({
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
// fetch new recipes
|
// fetch new recipes
|
||||||
const newRecipes = await fetchMore(
|
const newRecipes = await fetchRecipes();
|
||||||
page.value,
|
|
||||||
perPage.value,
|
|
||||||
preferences.value.orderBy,
|
|
||||||
preferences.value.orderDirection,
|
|
||||||
cookbook.value,
|
|
||||||
category.value,
|
|
||||||
tag.value,
|
|
||||||
tool.value,
|
|
||||||
|
|
||||||
// filter out recipes that have a null value for the property we're sorting by
|
|
||||||
preferences.value.filterNull && preferences.value.orderBy ? `${preferences.value.orderBy} <> null` : null
|
|
||||||
);
|
|
||||||
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
|
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
|
||||||
|
|
||||||
state.sortLoading = false;
|
state.sortLoading = false;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Ref, useAsync } from "@nuxtjs/composition-api";
|
import { Ref, useAsync } from "@nuxtjs/composition-api";
|
||||||
import { useAsyncKey } from "../use-utils";
|
import { useAsyncKey } from "../use-utils";
|
||||||
import { BaseCRUDAPI } from "~/lib/api/base/base-clients";
|
import { BaseCRUDAPI } from "~/lib/api/base/base-clients";
|
||||||
|
import { QueryValue } from "~/lib/api/base/route";
|
||||||
|
|
||||||
type BoundT = {
|
type BoundT = {
|
||||||
id?: string | number;
|
id?: string | number;
|
||||||
@ -25,7 +26,7 @@ export function useStoreActions<T extends BoundT>(
|
|||||||
allRef: Ref<T[] | null> | null,
|
allRef: Ref<T[] | null> | null,
|
||||||
loading: Ref<boolean>
|
loading: Ref<boolean>
|
||||||
): StoreActions<T> {
|
): StoreActions<T> {
|
||||||
function getAll(page = 1, perPage = -1, params = {} as any) {
|
function getAll(page = 1, perPage = -1, params = {} as Record<string, QueryValue>) {
|
||||||
params.orderBy ??= "name";
|
params.orderBy ??= "name";
|
||||||
params.orderDirection ??= "asc";
|
params.orderDirection ??= "asc";
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { useAsync, ref } from "@nuxtjs/composition-api";
|
import { useAsync, ref } from "@nuxtjs/composition-api";
|
||||||
import { useAsyncKey } from "../use-utils";
|
import { useAsyncKey } from "../use-utils";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { Recipe } from "~/lib/api/types/recipe";
|
import {Recipe} from "~/lib/api/types/recipe";
|
||||||
|
import {RecipeSearchQuery} from "~/lib/api/user/recipes/recipe";
|
||||||
|
|
||||||
export const allRecipes = ref<Recipe[]>([]);
|
export const allRecipes = ref<Recipe[]>([]);
|
||||||
export const recentRecipes = ref<Recipe[]>([]);
|
export const recentRecipes = ref<Recipe[]>([]);
|
||||||
@ -16,19 +17,22 @@ export const useLazyRecipes = function () {
|
|||||||
perPage: number,
|
perPage: number,
|
||||||
orderBy: string | null = null,
|
orderBy: string | null = null,
|
||||||
orderDirection = "desc",
|
orderDirection = "desc",
|
||||||
cookbook: string | null = null,
|
query: RecipeSearchQuery | null = null,
|
||||||
category: string | null = null,
|
|
||||||
tag: string | null = null,
|
|
||||||
tool: string | null = null,
|
|
||||||
queryFilter: string | null = null,
|
queryFilter: string | null = null,
|
||||||
) {
|
) {
|
||||||
const { data } = await api.recipes.getAll(page, perPage, {
|
const { data } = await api.recipes.getAll(page, perPage, {
|
||||||
orderBy,
|
orderBy,
|
||||||
orderDirection,
|
orderDirection,
|
||||||
cookbook,
|
search: query?.search,
|
||||||
categories: category,
|
cookbook: query?.cookbook,
|
||||||
tags: tag,
|
categories: query?.categories,
|
||||||
tools: tool,
|
requireAllCategories: query?.requireAllCategories,
|
||||||
|
tags: query?.tags,
|
||||||
|
requireAllTags: query?.requireAllTags,
|
||||||
|
tools: query?.tools,
|
||||||
|
requireAllTools: query?.requireAllTools,
|
||||||
|
foods: query?.foods,
|
||||||
|
requireAllFoods: query?.requireAllFoods,
|
||||||
queryFilter,
|
queryFilter,
|
||||||
});
|
});
|
||||||
return data ? data.items : [];
|
return data ? data.items : [];
|
||||||
|
@ -169,11 +169,6 @@ export default defineComponent({
|
|||||||
to: "/shopping-lists",
|
to: "/shopping-lists",
|
||||||
restricted: true,
|
restricted: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
icon: $globals.icons.viewModule,
|
|
||||||
to: "/recipes/all",
|
|
||||||
title: i18n.t("sidebar.all-recipes"),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: $globals.icons.tags,
|
icon: $globals.icons.tags,
|
||||||
to: "/recipes/categories",
|
to: "/recipes/categories",
|
||||||
|
@ -50,7 +50,6 @@ export default defineComponent({
|
|||||||
|
|
||||||
const buttons = [
|
const buttons = [
|
||||||
{ icon: $globals.icons.home, to: "/", text: i18n.t("general.home") },
|
{ icon: $globals.icons.home, to: "/", text: i18n.t("general.home") },
|
||||||
{ icon: $globals.icons.primary, to: "/recipes/all", text: i18n.t("page.all-recipes") },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Recipe } from "../types/recipe";
|
import { Recipe } from "../types/recipe";
|
||||||
import { ApiRequestInstance, PaginationData } from "~/lib/api/types/non-generated";
|
import { ApiRequestInstance, PaginationData } from "~/lib/api/types/non-generated";
|
||||||
|
import { QueryValue, route } from "~/lib/api/base/route";
|
||||||
|
|
||||||
export interface CrudAPIInterface {
|
export interface CrudAPIInterface {
|
||||||
requests: ApiRequestInstance;
|
requests: ApiRequestInstance;
|
||||||
@ -21,14 +22,14 @@ export abstract class BaseAPI {
|
|||||||
|
|
||||||
export abstract class BaseCRUDAPI<CreateType, ReadType, UpdateType = CreateType>
|
export abstract class BaseCRUDAPI<CreateType, ReadType, UpdateType = CreateType>
|
||||||
extends BaseAPI
|
extends BaseAPI
|
||||||
implements CrudAPIInterface {
|
implements CrudAPIInterface
|
||||||
|
{
|
||||||
abstract baseRoute: string;
|
abstract baseRoute: string;
|
||||||
abstract itemRoute(itemId: string | number): string;
|
abstract itemRoute(itemId: string | number): string;
|
||||||
|
|
||||||
async getAll(page = 1, perPage = -1, params = {} as any) {
|
async getAll(page = 1, perPage = -1, params = {} as Record<string, QueryValue>) {
|
||||||
return await this.requests.get<PaginationData<ReadType>>(this.baseRoute, {
|
params = Object.fromEntries(Object.entries(params).filter(([_, v]) => v !== null && v !== undefined));
|
||||||
params: { page, perPage, ...params },
|
return await this.requests.get<PaginationData<ReadType>>(route(this.baseRoute, { page, perPage, ...params }));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOne(payload: CreateType) {
|
async createOne(payload: CreateType) {
|
||||||
|
@ -21,7 +21,6 @@ export type QueryValue = string | string[] | number | number[] | boolean | null
|
|||||||
*/
|
*/
|
||||||
export function route(rest: string, params: Record<string, QueryValue> | null = null): string {
|
export function route(rest: string, params: Record<string, QueryValue> | null = null): string {
|
||||||
const url = new URL(parts.prefix + rest, parts.host);
|
const url = new URL(parts.prefix + rest, parts.host);
|
||||||
|
|
||||||
if (params) {
|
if (params) {
|
||||||
for (const [key, value] of Object.entries(params)) {
|
for (const [key, value] of Object.entries(params)) {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
/**
|
/**
|
||||||
/* This file was automatically generated from pydantic models by running pydantic2ts.
|
/* This file was automatically generated from pydantic models by running pydantic2ts.
|
||||||
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type ExportTypes = "json";
|
export type ExportTypes = "json";
|
||||||
export type RegisteredParser = "nlp" | "brute";
|
export type RegisteredParser = "nlp" | "brute";
|
||||||
|
@ -55,9 +55,9 @@ const routes = {
|
|||||||
recipesSlugTimelineEventId: (slug: string, id: string) => `${prefix}/recipes/${slug}/timeline/events/${id}`,
|
recipesSlugTimelineEventId: (slug: string, id: string) => `${prefix}/recipes/${slug}/timeline/events/${id}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RecipeSearchQuery ={
|
export type RecipeSearchQuery = {
|
||||||
search: string;
|
search: string;
|
||||||
orderDirection? : "asc" | "desc";
|
orderDirection?: "asc" | "desc";
|
||||||
groupId?: string;
|
groupId?: string;
|
||||||
|
|
||||||
queryFilter?: string;
|
queryFilter?: string;
|
||||||
@ -76,11 +76,10 @@ export type RecipeSearchQuery ={
|
|||||||
foods?: string[];
|
foods?: string[];
|
||||||
requireAllFoods?: boolean;
|
requireAllFoods?: boolean;
|
||||||
|
|
||||||
page: number;
|
page?: number;
|
||||||
perPage: number;
|
perPage?: number;
|
||||||
orderBy?: string;
|
orderBy?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
export class RecipeAPI extends BaseCRUDAPI<CreateRecipe, Recipe, Recipe> {
|
export class RecipeAPI extends BaseCRUDAPI<CreateRecipe, Recipe, Recipe> {
|
||||||
baseRoute: string = routes.recipesBase;
|
baseRoute: string = routes.recipesBase;
|
||||||
@ -96,7 +95,7 @@ export class RecipeAPI extends BaseCRUDAPI<CreateRecipe, Recipe, Recipe> {
|
|||||||
this.share = new RecipeShareApi(requests);
|
this.share = new RecipeShareApi(requests);
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(rsq : RecipeSearchQuery) {
|
async search(rsq: RecipeSearchQuery) {
|
||||||
return await this.requests.get<PaginationData<Recipe>>(route(routes.recipesBase, rsq));
|
return await this.requests.get<PaginationData<Recipe>>(route(routes.recipesBase, rsq));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,7 +175,10 @@ export class RecipeAPI extends BaseCRUDAPI<CreateRecipe, Recipe, Recipe> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateTimelineEvent(recipeSlug: string, eventId: string, payload: RecipeTimelineEventUpdate) {
|
async updateTimelineEvent(recipeSlug: string, eventId: string, payload: RecipeTimelineEventUpdate) {
|
||||||
return await this.requests.put<RecipeTimelineEventOut, RecipeTimelineEventUpdate>(routes.recipesSlugTimelineEventId(recipeSlug, eventId), payload);
|
return await this.requests.put<RecipeTimelineEventOut, RecipeTimelineEventUpdate>(
|
||||||
|
routes.recipesSlugTimelineEventId(recipeSlug, eventId),
|
||||||
|
payload
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteTimelineEvent(recipeSlug: string, eventId: string) {
|
async deleteTimelineEvent(recipeSlug: string, eventId: string) {
|
||||||
@ -184,8 +186,11 @@ export class RecipeAPI extends BaseCRUDAPI<CreateRecipe, Recipe, Recipe> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAllTimelineEvents(recipeSlug: string, page = 1, perPage = -1, params = {} as any) {
|
async getAllTimelineEvents(recipeSlug: string, page = 1, perPage = -1, params = {} as any) {
|
||||||
return await this.requests.get<PaginationData<RecipeTimelineEventOut>>(routes.recipesSlugTimelineEvent(recipeSlug), {
|
return await this.requests.get<PaginationData<RecipeTimelineEventOut>>(
|
||||||
params: { page, perPage, ...params },
|
routes.recipesSlugTimelineEvent(recipeSlug),
|
||||||
});
|
{
|
||||||
|
params: { page, perPage, ...params },
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
<RecipeCardSection
|
<RecipeCardSection
|
||||||
class="mb-5 mx-1"
|
class="mb-5 mx-1"
|
||||||
:recipes="recipes"
|
:recipes="recipes"
|
||||||
:cookbook-slug="slug"
|
:query="{ cookbook: slug }"
|
||||||
@sortRecipes="assignSorted"
|
@sortRecipes="assignSorted"
|
||||||
@replaceRecipes="replaceRecipes"
|
@replaceRecipes="replaceRecipes"
|
||||||
@appendRecipes="appendRecipes"
|
@appendRecipes="appendRecipes"
|
||||||
|
@ -98,15 +98,6 @@
|
|||||||
</template>
|
</template>
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-text-field
|
|
||||||
v-model="state.maxResults"
|
|
||||||
class="mt-0 pt-0"
|
|
||||||
:label="$tc('search.max-results')"
|
|
||||||
type="number"
|
|
||||||
outlined
|
|
||||||
dense
|
|
||||||
hide-details
|
|
||||||
/>
|
|
||||||
<v-switch v-model="state.auto" label="Auto Search" single-line></v-switch>
|
<v-switch v-model="state.auto" label="Auto Search" single-line></v-switch>
|
||||||
<v-btn block color="primary" @click="reset">
|
<v-btn block color="primary" @click="reset">
|
||||||
{{ $tc("general.reset") }}
|
{{ $tc("general.reset") }}
|
||||||
@ -116,7 +107,7 @@
|
|||||||
</v-menu>
|
</v-menu>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!state.auto" class="search-button-container">
|
<div v-if="!state.auto" class="search-button-container">
|
||||||
<v-btn :loading="state.loading" x-large color="primary" type="submit" block>
|
<v-btn x-large color="primary" type="submit" block>
|
||||||
<v-icon left>
|
<v-icon left>
|
||||||
{{ $globals.icons.search }}
|
{{ $globals.icons.search }}
|
||||||
</v-icon>
|
</v-icon>
|
||||||
@ -131,38 +122,41 @@
|
|||||||
class="mt-n5"
|
class="mt-n5"
|
||||||
:icon="$globals.icons.search"
|
:icon="$globals.icons.search"
|
||||||
:title="$tc('search.results')"
|
:title="$tc('search.results')"
|
||||||
:recipes="state.results"
|
:recipes="recipes"
|
||||||
/>
|
:query="passedQuery"
|
||||||
|
@sortRecipes="assignSorted"
|
||||||
|
@replaceRecipes="replaceRecipes"
|
||||||
|
@appendRecipes="appendRecipes"
|
||||||
|
@delete="removeRecipe"
|
||||||
|
></RecipeCardSection>
|
||||||
</v-container>
|
</v-container>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ref, defineComponent, useRouter, onMounted, useContext, computed } from "@nuxtjs/composition-api";
|
import { ref, defineComponent, useRouter, onMounted, useContext, computed } from "@nuxtjs/composition-api";
|
||||||
// eslint-disable-next-line import/namespace
|
|
||||||
import { watchDebounced } from "@vueuse/shared";
|
import { watchDebounced } from "@vueuse/shared";
|
||||||
import SearchFilter from "~/components/Domain/SearchFilter.vue";
|
import SearchFilter from "~/components/Domain/SearchFilter.vue";
|
||||||
import { useUserApi } from "~/composables/api";
|
|
||||||
import { useCategoryStore, useFoodStore, useTagStore, useToolStore } from "~/composables/store";
|
import { useCategoryStore, useFoodStore, useTagStore, useToolStore } from "~/composables/store";
|
||||||
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
||||||
import { IngredientFood, RecipeCategory, RecipeSummary, RecipeTag, RecipeTool } from "~/lib/api/types/recipe";
|
import { IngredientFood, RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe";
|
||||||
import { NoUndefinedField } from "~/lib/api/types/non-generated";
|
import { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||||
|
import { useLazyRecipes } from "~/composables/recipes";
|
||||||
|
import { RecipeSearchQuery } from "~/lib/api/user/recipes/recipe";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { SearchFilter, RecipeCardSection },
|
components: { SearchFilter, RecipeCardSection },
|
||||||
setup() {
|
setup() {
|
||||||
|
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const api = useUserApi();
|
|
||||||
const { $globals, i18n } = useContext();
|
const { $globals, i18n } = useContext();
|
||||||
|
|
||||||
const state = ref({
|
const state = ref({
|
||||||
auto: true,
|
auto: true,
|
||||||
loading: false,
|
|
||||||
search: "",
|
search: "",
|
||||||
orderBy: "created_at",
|
orderBy: "created_at",
|
||||||
orderDirection: "desc" as "asc" | "desc",
|
orderDirection: "desc" as "asc" | "desc",
|
||||||
maxResults: 21,
|
|
||||||
results: [] as RecipeSummary[],
|
|
||||||
|
|
||||||
// and/or
|
// and/or
|
||||||
requireAllCategories: false,
|
requireAllCategories: false,
|
||||||
@ -183,9 +177,10 @@ export default defineComponent({
|
|||||||
const tools = useToolStore();
|
const tools = useToolStore();
|
||||||
const selectedTools = ref<NoUndefinedField<RecipeTool>[]>([]);
|
const selectedTools = ref<NoUndefinedField<RecipeTool>[]>([]);
|
||||||
|
|
||||||
|
const passedQuery = ref<RecipeSearchQuery | null>(null);
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
state.value.search = "";
|
state.value.search = "";
|
||||||
state.value.maxResults = 21;
|
|
||||||
state.value.orderBy = "created_at";
|
state.value.orderBy = "created_at";
|
||||||
state.value.orderDirection = "desc";
|
state.value.orderDirection = "desc";
|
||||||
state.value.requireAllCategories = false;
|
state.value.requireAllCategories = false;
|
||||||
@ -213,7 +208,6 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function search() {
|
async function search() {
|
||||||
state.value.loading = true;
|
|
||||||
await router.push({
|
await router.push({
|
||||||
query: {
|
query: {
|
||||||
categories: toIDArray(selectedCategories.value),
|
categories: toIDArray(selectedCategories.value),
|
||||||
@ -225,7 +219,6 @@ export default defineComponent({
|
|||||||
...{
|
...{
|
||||||
auto: state.value.auto ? undefined : "false",
|
auto: state.value.auto ? undefined : "false",
|
||||||
search: state.value.search === "" ? undefined : state.value.search,
|
search: state.value.search === "" ? undefined : state.value.search,
|
||||||
maxResults: state.value.maxResults === 21 ? undefined : state.value.maxResults.toString(),
|
|
||||||
orderBy: state.value.orderBy === "createdAt" ? undefined : state.value.orderBy,
|
orderBy: state.value.orderBy === "createdAt" ? undefined : state.value.orderBy,
|
||||||
orderDirection: state.value.orderDirection === "desc" ? undefined : state.value.orderDirection,
|
orderDirection: state.value.orderDirection === "desc" ? undefined : state.value.orderDirection,
|
||||||
requireAllCategories: state.value.requireAllCategories ? "true" : undefined,
|
requireAllCategories: state.value.requireAllCategories ? "true" : undefined,
|
||||||
@ -236,35 +229,19 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data, error } = await api.recipes.search({
|
passedQuery.value = {
|
||||||
search: state.value.search,
|
search: state.value.search,
|
||||||
page: 1,
|
|
||||||
orderBy: state.value.orderBy,
|
|
||||||
orderDirection: state.value.orderDirection,
|
|
||||||
perPage: state.value.maxResults,
|
|
||||||
categories: toIDArray(selectedCategories.value),
|
categories: toIDArray(selectedCategories.value),
|
||||||
foods: toIDArray(selectedFoods.value),
|
foods: toIDArray(selectedFoods.value),
|
||||||
tags: toIDArray(selectedTags.value),
|
tags: toIDArray(selectedTags.value),
|
||||||
tools: toIDArray(selectedTools.value),
|
tools: toIDArray(selectedTools.value),
|
||||||
|
|
||||||
requireAllCategories: state.value.requireAllCategories,
|
requireAllCategories: state.value.requireAllCategories,
|
||||||
requireAllTags: state.value.requireAllTags,
|
requireAllTags: state.value.requireAllTags,
|
||||||
requireAllTools: state.value.requireAllTools,
|
requireAllTools: state.value.requireAllTools,
|
||||||
requireAllFoods: state.value.requireAllFoods,
|
requireAllFoods: state.value.requireAllFoods,
|
||||||
});
|
orderBy: state.value.orderBy,
|
||||||
|
orderDirection: state.value.orderDirection,
|
||||||
if (error) {
|
};
|
||||||
console.error(error);
|
|
||||||
state.value.loading = false;
|
|
||||||
state.value.results = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
state.value.results = data.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.value.loading = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function waitUntilAndExecute(
|
function waitUntilAndExecute(
|
||||||
@ -345,10 +322,6 @@ export default defineComponent({
|
|||||||
state.value.search = query.search as string;
|
state.value.search = query.search as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.maxResults) {
|
|
||||||
state.value.maxResults = parseInt(query.maxResults as string);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.orderBy) {
|
if (query.orderBy) {
|
||||||
state.value.orderBy = query.orderBy as string;
|
state.value.orderBy = query.orderBy as string;
|
||||||
}
|
}
|
||||||
@ -429,7 +402,6 @@ export default defineComponent({
|
|||||||
() => state.value.requireAllFoods,
|
() => state.value.requireAllFoods,
|
||||||
() => state.value.orderBy,
|
() => state.value.orderBy,
|
||||||
() => state.value.orderDirection,
|
() => state.value.orderDirection,
|
||||||
() => state.value.maxResults,
|
|
||||||
selectedCategories,
|
selectedCategories,
|
||||||
selectedFoods,
|
selectedFoods,
|
||||||
selectedTags,
|
selectedTags,
|
||||||
@ -462,6 +434,12 @@ export default defineComponent({
|
|||||||
selectedFoods,
|
selectedFoods,
|
||||||
selectedTags,
|
selectedTags,
|
||||||
selectedTools,
|
selectedTools,
|
||||||
|
appendRecipes,
|
||||||
|
assignSorted,
|
||||||
|
recipes,
|
||||||
|
removeRecipe,
|
||||||
|
replaceRecipes,
|
||||||
|
passedQuery,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
<template>
|
|
||||||
<v-container>
|
|
||||||
<RecipeCardSection
|
|
||||||
:icon="$globals.icons.primary"
|
|
||||||
:title="$t('page.all-recipes')"
|
|
||||||
:recipes="recipes"
|
|
||||||
@sortRecipes="assignSorted"
|
|
||||||
@replaceRecipes="replaceRecipes"
|
|
||||||
@appendRecipes="appendRecipes"
|
|
||||||
@delete="removeRecipe"
|
|
||||||
></RecipeCardSection>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from "@nuxtjs/composition-api";
|
|
||||||
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
|
||||||
import { useLazyRecipes } from "~/composables/recipes";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: { RecipeCardSection },
|
|
||||||
setup() {
|
|
||||||
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes();
|
|
||||||
return { appendRecipes, assignSorted, recipes, removeRecipe, replaceRecipes };
|
|
||||||
},
|
|
||||||
head() {
|
|
||||||
return {
|
|
||||||
title: this.$t("page.all-recipes") as string,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
@ -5,7 +5,7 @@
|
|||||||
:icon="$globals.icons.tags"
|
:icon="$globals.icons.tags"
|
||||||
:title="category.name"
|
:title="category.name"
|
||||||
:recipes="recipes"
|
:recipes="recipes"
|
||||||
:category-slug="category.slug"
|
:query="{ categories: [category.slug] }"
|
||||||
@sortRecipes="assignSorted"
|
@sortRecipes="assignSorted"
|
||||||
@replaceRecipes="replaceRecipes"
|
@replaceRecipes="replaceRecipes"
|
||||||
@appendRecipes="appendRecipes"
|
@appendRecipes="appendRecipes"
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
:icon="$globals.icons.tags"
|
:icon="$globals.icons.tags"
|
||||||
:title="tag.name"
|
:title="tag.name"
|
||||||
:recipes="recipes"
|
:recipes="recipes"
|
||||||
:tag-slug="tag.slug"
|
:query="{ tags: [tag.slug] }"
|
||||||
@sortRecipes="assignSorted"
|
@sortRecipes="assignSorted"
|
||||||
@replaceRecipes="replaceRecipes"
|
@replaceRecipes="replaceRecipes"
|
||||||
@appendRecipes="appendRecipes"
|
@appendRecipes="appendRecipes"
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
:icon="$globals.icons.potSteam"
|
:icon="$globals.icons.potSteam"
|
||||||
:title="tool.name"
|
:title="tool.name"
|
||||||
:recipes="recipes"
|
:recipes="recipes"
|
||||||
:tool-slug="tool.slug"
|
:query="{ tools: [tool.slug] }"
|
||||||
@sortRecipes="assignSorted"
|
@sortRecipes="assignSorted"
|
||||||
@replaceRecipes="replaceRecipes"
|
@replaceRecipes="replaceRecipes"
|
||||||
@appendRecipes="appendRecipes"
|
@appendRecipes="appendRecipes"
|
||||||
|
@ -301,7 +301,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const statsTo: { [key: string]: string } = {
|
const statsTo: { [key: string]: string } = {
|
||||||
totalRecipes: "/recipes/all",
|
totalRecipes: "/",
|
||||||
totalUsers: "/group/members",
|
totalUsers: "/group/members",
|
||||||
totalCategories: "/recipes/categories",
|
totalCategories: "/recipes/categories",
|
||||||
totalTags: "/recipes/tags",
|
totalTags: "/recipes/tags",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user