Skip to content

Commit

Permalink
fix: Recipe Search URL State (#3332)
Browse files Browse the repository at this point in the history
* fix several state issues with explore page
- update state when there are no query params
- only call search if the query params actually changed
- wait until ready to call API

* store last search query in user prefs

* restore chip tag click to anonymous user
  • Loading branch information
michael-genson authored Mar 25, 2024
1 parent 21886ab commit dfbc890
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 55 deletions.
5 changes: 1 addition & 4 deletions frontend/components/Domain/Recipe/RecipeChips.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
color="accent"
:small="small"
dark
:to="isOwnGroup ? `${baseRecipeRoute}?${urlPrefix}=${category.id}` : undefined"
:to="`${baseRecipeRoute}?${urlPrefix}=${category.id}`"
>
{{ truncateText(category.name) }}
</v-chip>
Expand All @@ -18,7 +18,6 @@

<script lang="ts">
import { computed, defineComponent, useContext, useRoute } from "@nuxtjs/composition-api";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import { RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/user";
export type UrlPrefixParam = "tags" | "categories" | "tools";
Expand Down Expand Up @@ -56,7 +55,6 @@ export default defineComponent({
},
setup(props) {
const { $auth } = useContext();
const { isOwnGroup } = useLoggedInState();
const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "")
Expand All @@ -74,7 +72,6 @@ export default defineComponent({
return {
baseRecipeRoute,
isOwnGroup,
truncateText,
};
},
Expand Down
149 changes: 98 additions & 51 deletions frontend/components/Domain/Recipe/RecipeExplorerPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,12 @@
<v-divider></v-divider>
<v-container class="mt-6 px-md-6">
<RecipeCardSection
v-if="state.ready"
class="mt-n5"
:icon="$globals.icons.search"
:title="$tc('search.results')"
:recipes="recipes"
:query="passedQuery"
:query="passedQueryWithSeed"
@replaceRecipes="replaceRecipes"
@appendRecipes="appendRecipes"
/>
Expand All @@ -137,11 +138,12 @@
</template>

<script lang="ts">
import { ref, defineComponent, useRouter, onMounted, useContext, computed, Ref, useRoute } from "@nuxtjs/composition-api";
import { ref, defineComponent, useRouter, onMounted, useContext, computed, Ref, useRoute, watch } from "@nuxtjs/composition-api";
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 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 All @@ -161,6 +163,7 @@ export default defineComponent({
const { isOwnGroup } = useLoggedInState();
const state = ref({
auto: true,
ready: false,
search: "",
orderBy: "created_at",
orderDirection: "desc" as "asc" | "desc",
Expand All @@ -174,6 +177,7 @@ export default defineComponent({
const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const preferences = useUserSortPreferences();
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
const categories = isOwnGroup.value ? useCategoryStore() : usePublicCategoryStore(groupSlug.value);
Expand All @@ -188,7 +192,30 @@ export default defineComponent({
const tools = isOwnGroup.value ? useToolStore() : usePublicToolStore(groupSlug.value);
const selectedTools = ref<NoUndefinedField<RecipeTool>[]>([]);
const passedQuery = ref<RecipeSearchQuery | null>(null);
function calcPassedQuery(): RecipeSearchQuery {
return {
search: state.value.search,
categories: toIDArray(selectedCategories.value),
foods: toIDArray(selectedFoods.value),
tags: toIDArray(selectedTags.value),
tools: toIDArray(selectedTools.value),
requireAllCategories: state.value.requireAllCategories,
requireAllTags: state.value.requireAllTags,
requireAllTools: state.value.requireAllTools,
requireAllFoods: state.value.requireAllFoods,
orderBy: state.value.orderBy,
orderDirection: state.value.orderDirection,
};
}
const passedQuery = ref<RecipeSearchQuery>(calcPassedQuery());
// we calculate this separately because otherwise we can't check for query changes
const passedQueryWithSeed = computed(() => {
return {
...passedQuery.value,
_searchSeed: Date.now().toString()
};
})
function reset() {
state.value.search = "";
Expand All @@ -203,10 +230,6 @@ export default defineComponent({
selectedTags.value = [];
selectedTools.value = [];
router.push({
query: {},
});
search();
}
Expand All @@ -215,7 +238,8 @@ export default defineComponent({
}
function toIDArray(array: { id: string }[]) {
return array.map((item) => item.id);
// we sort the array to make sure the query is always the same
return array.map((item) => item.id).sort();
}
function hideKeyboard() {
Expand All @@ -225,40 +249,33 @@ export default defineComponent({
const input: Ref<any> = ref(null);
async function search() {
await router.push({
query: {
categories: toIDArray(selectedCategories.value),
foods: toIDArray(selectedFoods.value),
tags: toIDArray(selectedTags.value),
tools: toIDArray(selectedTools.value),
// Only add the query param if it's or not default
...{
auto: state.value.auto ? undefined : "false",
search: state.value.search === "" ? undefined : state.value.search,
orderBy: state.value.orderBy === "createdAt" ? undefined : state.value.orderBy,
orderDirection: state.value.orderDirection === "desc" ? undefined : state.value.orderDirection,
requireAllCategories: state.value.requireAllCategories ? "true" : undefined,
requireAllTags: state.value.requireAllTags ? "true" : undefined,
requireAllTools: state.value.requireAllTools ? "true" : undefined,
requireAllFoods: state.value.requireAllFoods ? "true" : undefined,
},
},
});
const oldQueryValueString = JSON.stringify(passedQuery.value);
const newQueryValue = calcPassedQuery();
const newQueryValueString = JSON.stringify(newQueryValue);
if (oldQueryValueString === newQueryValueString) {
return;
}
passedQuery.value = {
search: state.value.search,
categories: toIDArray(selectedCategories.value),
foods: toIDArray(selectedFoods.value),
tags: toIDArray(selectedTags.value),
tools: toIDArray(selectedTools.value),
requireAllCategories: state.value.requireAllCategories,
requireAllTags: state.value.requireAllTags,
requireAllTools: state.value.requireAllTools,
requireAllFoods: state.value.requireAllFoods,
orderBy: state.value.orderBy,
orderDirection: state.value.orderDirection,
_searchSeed: Date.now().toString()
};
passedQuery.value = newQueryValue;
const query = {
categories: passedQuery.value.categories,
foods: passedQuery.value.foods,
tags: passedQuery.value.tags,
tools: passedQuery.value.tools,
// Only add the query param if it's or not default
...{
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,
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);
}
function waitUntilAndExecute(
Expand Down Expand Up @@ -329,13 +346,20 @@ export default defineComponent({
},
];
onMounted(() => {
// Hydrate Search
// wait for stores to be hydrated
watch(
() => route.value.query,
() => {
if (state.value.ready) {
hydrateSearch();
}
},
{
deep: true,
},
)
// read query params
async function hydrateSearch() {
const query = router.currentRoute.query;
if (query.auto) {
state.value.auto = query.auto === "true";
}
Expand Down Expand Up @@ -367,6 +391,8 @@ export default defineComponent({
}
)
);
} else {
selectedCategories.value = [];
}
if (query.foods) {
Expand All @@ -384,6 +410,8 @@ export default defineComponent({
}
)
);
} else {
selectedFoods.value = [];
}
if (query.tags) {
Expand All @@ -396,6 +424,8 @@ export default defineComponent({
}
)
);
} else {
selectedTags.value = [];
}
if (query.tools) {
Expand All @@ -408,11 +438,28 @@ export default defineComponent({
}
)
);
} else {
selectedTools.value = [];
}
Promise.allSettled(promises).then(() => {
search();
});
await Promise.allSettled(promises);
};
onMounted(async () => {
// restore the user's last search query
if (preferences.value.searchQuery && !(Object.keys(route.value.query).length > 0)) {
try {
const query = JSON.parse(preferences.value.searchQuery);
await router.replace({ query });
} catch (error) {
preferences.value.searchQuery = "";
router.replace({ query: {} });
}
}
await hydrateSearch();
await search();
state.value.ready = true;
});
watchDebounced(
Expand All @@ -430,7 +477,7 @@ export default defineComponent({
selectedTools,
],
async () => {
if (state.value.auto) {
if (state.value.ready && state.value.auto) {
await search();
}
},
Expand Down Expand Up @@ -463,7 +510,7 @@ export default defineComponent({
recipes,
removeRecipe,
replaceRecipes,
passedQuery,
passedQueryWithSeed,
};
},
head: {},
Expand Down
2 changes: 2 additions & 0 deletions frontend/composables/use-users/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface UserRecipePreferences {
filterNull: boolean;
sortIcon: string;
useMobileCards: boolean;
searchQuery: string;
}

export interface UserShoppingListPreferences {
Expand Down Expand Up @@ -59,6 +60,7 @@ 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 Down

0 comments on commit dfbc890

Please sign in to comment.