Skip to content

Commit

Permalink
fix: Recipe Search Quirks and Session Storage (#3541)
Browse files Browse the repository at this point in the history
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
  • Loading branch information
3 people authored May 6, 2024
1 parent 770630b commit 418a8ec
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 46 deletions.
128 changes: 85 additions & 43 deletions frontend/components/Domain/Recipe/RecipeExplorerPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand All @@ -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),
Expand All @@ -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 = [];
Expand Down Expand Up @@ -262,20 +274,20 @@ 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,
requireAllFoods: passedQuery.value.requireAllFoods ? "true" : undefined,
},
}
await router.push({ query });
preferences.value.searchQuery = JSON.stringify(query);
searchQuerySession.value.recipe = JSON.stringify(query);
}
function waitUntilAndExecute(
Expand Down Expand Up @@ -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<void>[] = [];
if (query.categories) {
if (query.categories?.length) {
promises.push(
waitUntilAndExecute(
() => categories.items.value.length > 0,
Expand All @@ -395,64 +437,64 @@ 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<RecipeTag>[];
}
)
);
} 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<RecipeTag>[];
const result = tools.items.value.filter((item) => (query.tools as string[]).includes(item.id));
selectedTools.value = result as NoUndefinedField<RecipeTool>[];
}
)
);
} 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<RecipeTool>[];
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);
};
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: {} });
}
}
Expand Down
22 changes: 19 additions & 3 deletions frontend/composables/use-users/preferences.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -8,6 +8,10 @@ export interface UserPrintPreferences {
showNotes: boolean;
}

export interface UserSearchQuery {
recipe: string;
}

export enum ImagePosition {
hidden = "hidden",
left = "left",
Expand All @@ -20,7 +24,6 @@ export interface UserRecipePreferences {
filterNull: boolean;
sortIcon: string;
useMobileCards: boolean;
searchQuery: string;
}

export interface UserShoppingListPreferences {
Expand Down Expand Up @@ -60,7 +63,6 @@ export function useUserSortPreferences(): Ref<UserRecipePreferences> {
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
Expand All @@ -70,6 +72,20 @@ export function useUserSortPreferences(): Ref<UserRecipePreferences> {
return fromStorage;
}

export function useUserSearchQuerySession(): Ref<UserSearchQuery> {
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<UserSearchQuery>;

return fromStorage;
}


export function useShoppingListPreferences(): Ref<UserShoppingListPreferences> {
const fromStorage = useLocalStorage(
Expand Down

0 comments on commit 418a8ec

Please sign in to comment.