diff --git a/frontend/components/Domain/Recipe/RecipeExplorerPage.vue b/frontend/components/Domain/Recipe/RecipeExplorerPage.vue index 527eb29b6a9..39e2ca2395d 100644 --- a/frontend/components/Domain/Recipe/RecipeExplorerPage.vue +++ b/frontend/components/Domain/Recipe/RecipeExplorerPage.vue @@ -143,7 +143,7 @@ import { watchDebounced } from "@vueuse/shared"; import SearchFilter from "~/components/Domain/SearchFilter.vue"; import { useLoggedInState } from "~/composables/use-logged-in-state"; import { useCategoryStore, useFoodStore, useTagStore, useToolStore } from "~/composables/store"; -import { useUserSortPreferences } from "~/composables/use-users/preferences"; +import { useUserSearchQuerySession } from "~/composables/use-users/preferences"; import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue"; import { IngredientFood, RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe"; import { NoUndefinedField } from "~/lib/api/types/non-generated"; @@ -177,7 +177,7 @@ export default defineComponent({ const route = useRoute(); const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || ""); - const preferences = useUserSortPreferences(); + const searchQuerySession = useUserSearchQuerySession(); const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value); const categories = isOwnGroup.value ? useCategoryStore() : usePublicCategoryStore(groupSlug.value); @@ -194,7 +194,9 @@ export default defineComponent({ function calcPassedQuery(): RecipeSearchQuery { return { - search: state.value.search, + // the search clear button sets search to null, which still renders the query param for a moment, + // whereas an empty string is not rendered + search: state.value.search ? state.value.search : "", categories: toIDArray(selectedCategories.value), foods: toIDArray(selectedFoods.value), tags: toIDArray(selectedTags.value), @@ -217,14 +219,24 @@ export default defineComponent({ }; }) + const queryDefaults = { + search: "", + orderBy: "created_at", + orderDirection: "desc" as "asc" | "desc", + requireAllCategories: false, + requireAllTags: false, + requireAllTools: false, + requireAllFoods: false, + } + function reset() { - state.value.search = ""; - state.value.orderBy = "created_at"; - state.value.orderDirection = "desc"; - state.value.requireAllCategories = false; - state.value.requireAllTags = false; - state.value.requireAllTools = false; - state.value.requireAllFoods = false; + state.value.search = queryDefaults.search; + state.value.orderBy = queryDefaults.orderBy; + state.value.orderDirection = queryDefaults.orderDirection; + state.value.requireAllCategories = queryDefaults.requireAllCategories; + state.value.requireAllTags = queryDefaults.requireAllTags; + state.value.requireAllTools = queryDefaults.requireAllTools; + state.value.requireAllFoods = queryDefaults.requireAllFoods; selectedCategories.value = []; selectedFoods.value = []; selectedTags.value = []; @@ -262,12 +274,12 @@ export default defineComponent({ foods: passedQuery.value.foods, tags: passedQuery.value.tags, tools: passedQuery.value.tools, - // Only add the query param if it's or not default + // Only add the query param if it's not the default value ...{ auto: state.value.auto ? undefined : "false", - search: passedQuery.value.search === "" ? undefined : passedQuery.value.search, - orderBy: passedQuery.value.orderBy === "created_at" ? undefined : passedQuery.value.orderBy, - orderDirection: passedQuery.value.orderDirection === "desc" ? undefined : passedQuery.value.orderDirection, + search: passedQuery.value.search === queryDefaults.search ? undefined : passedQuery.value.search, + orderBy: passedQuery.value.orderBy === queryDefaults.orderBy ? undefined : passedQuery.value.orderBy, + orderDirection: passedQuery.value.orderDirection === queryDefaults.orderDirection ? undefined : passedQuery.value.orderDirection, requireAllCategories: passedQuery.value.requireAllCategories ? "true" : undefined, requireAllTags: passedQuery.value.requireAllTags ? "true" : undefined, requireAllTools: passedQuery.value.requireAllTools ? "true" : undefined, @@ -275,7 +287,7 @@ export default defineComponent({ }, } await router.push({ query }); - preferences.value.searchQuery = JSON.stringify(query); + searchQuerySession.value.recipe = JSON.stringify(query); } function waitUntilAndExecute( @@ -360,25 +372,55 @@ export default defineComponent({ async function hydrateSearch() { const query = router.currentRoute.query; - if (query.auto) { + if (query.auto?.length) { state.value.auto = query.auto === "true"; } - if (query.search) { + if (query.search?.length) { state.value.search = query.search as string; + } else { + state.value.search = queryDefaults.search; } - if (query.orderBy) { + if (query.orderBy?.length) { state.value.orderBy = query.orderBy as string; + } else { + state.value.orderBy = queryDefaults.orderBy; } - if (query.orderDirection) { + if (query.orderDirection?.length) { state.value.orderDirection = query.orderDirection as "asc" | "desc"; + } else { + state.value.orderDirection = queryDefaults.orderDirection; + } + + if (query.requireAllCategories?.length) { + state.value.requireAllCategories = query.requireAllCategories === "true"; + } else { + state.value.requireAllCategories = queryDefaults.requireAllCategories; + } + + if (query.requireAllTags?.length) { + state.value.requireAllTags = query.requireAllTags === "true"; + } else { + state.value.requireAllTags = queryDefaults.requireAllTags; + } + + if (query.requireAllTools?.length) { + state.value.requireAllTools = query.requireAllTools === "true"; + } else { + state.value.requireAllTools = queryDefaults.requireAllTools; + } + + if (query.requireAllFoods?.length) { + state.value.requireAllFoods = query.requireAllFoods === "true"; + } else { + state.value.requireAllFoods = queryDefaults.requireAllFoods; } const promises: Promise[] = []; - if (query.categories) { + if (query.categories?.length) { promises.push( waitUntilAndExecute( () => categories.items.value.length > 0, @@ -395,51 +437,51 @@ export default defineComponent({ selectedCategories.value = []; } - if (query.foods) { + if (query.tags?.length) { promises.push( waitUntilAndExecute( + () => tags.items.value.length > 0, () => { - if (foods.foods.value) { - return foods.foods.value.length > 0; - } - return false; - }, - () => { - const result = foods.foods.value?.filter((item) => (query.foods as string[]).includes(item.id)); - selectedFoods.value = result ?? []; + const result = tags.items.value.filter((item) => (query.tags as string[]).includes(item.id as string)); + selectedTags.value = result as NoUndefinedField[]; } ) ); } else { - selectedFoods.value = []; + selectedTags.value = []; } - if (query.tags) { + if (query.tools?.length) { promises.push( waitUntilAndExecute( - () => tags.items.value.length > 0, + () => tools.items.value.length > 0, () => { - const result = tags.items.value.filter((item) => (query.tags as string[]).includes(item.id as string)); - selectedTags.value = result as NoUndefinedField[]; + const result = tools.items.value.filter((item) => (query.tools as string[]).includes(item.id)); + selectedTools.value = result as NoUndefinedField[]; } ) ); } else { - selectedTags.value = []; + selectedTools.value = []; } - if (query.tools) { + if (query.foods?.length) { promises.push( waitUntilAndExecute( - () => tools.items.value.length > 0, () => { - const result = tools.items.value.filter((item) => (query.tools as string[]).includes(item.id)); - selectedTools.value = result as NoUndefinedField[]; + if (foods.foods.value) { + return foods.foods.value.length > 0; + } + return false; + }, + () => { + const result = foods.foods.value?.filter((item) => (query.foods as string[]).includes(item.id)); + selectedFoods.value = result ?? []; } ) ); } else { - selectedTools.value = []; + selectedFoods.value = []; } await Promise.allSettled(promises); @@ -447,12 +489,12 @@ export default defineComponent({ onMounted(async () => { // restore the user's last search query - if (preferences.value.searchQuery && !(Object.keys(route.value.query).length > 0)) { + if (searchQuerySession.value.recipe && !(Object.keys(route.value.query).length > 0)) { try { - const query = JSON.parse(preferences.value.searchQuery); + const query = JSON.parse(searchQuerySession.value.recipe); await router.replace({ query }); } catch (error) { - preferences.value.searchQuery = ""; + searchQuerySession.value.recipe = ""; router.replace({ query: {} }); } } diff --git a/frontend/composables/use-users/preferences.ts b/frontend/composables/use-users/preferences.ts index 84b78a361cc..88b0ba1e92a 100644 --- a/frontend/composables/use-users/preferences.ts +++ b/frontend/composables/use-users/preferences.ts @@ -1,5 +1,5 @@ import { Ref, useContext } from "@nuxtjs/composition-api"; -import { useLocalStorage } from "@vueuse/core"; +import { useLocalStorage, useSessionStorage } from "@vueuse/core"; import { TimelineEventType } from "~/lib/api/types/recipe"; export interface UserPrintPreferences { @@ -8,6 +8,10 @@ export interface UserPrintPreferences { showNotes: boolean; } +export interface UserSearchQuery { + recipe: string; +} + export enum ImagePosition { hidden = "hidden", left = "left", @@ -20,7 +24,6 @@ export interface UserRecipePreferences { filterNull: boolean; sortIcon: string; useMobileCards: boolean; - searchQuery: string; } export interface UserShoppingListPreferences { @@ -60,7 +63,6 @@ export function useUserSortPreferences(): Ref { filterNull: false, sortIcon: $globals.icons.sortAlphabeticalAscending, useMobileCards: false, - searchQuery: "", }, { mergeDefaults: true } // we cast to a Ref because by default it will return an optional type ref @@ -70,6 +72,20 @@ export function useUserSortPreferences(): Ref { return fromStorage; } +export function useUserSearchQuerySession(): Ref { + const fromStorage = useSessionStorage( + "search-query", + { + recipe: "", + }, + { mergeDefaults: true } + // we cast to a Ref because by default it will return an optional type ref + // but since we pass defaults we know all properties are set. + ) as unknown as Ref; + + return fromStorage; +} + export function useShoppingListPreferences(): Ref { const fromStorage = useLocalStorage(