1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-07-24 07:39:41 +02:00

fix: Recipe Search URL State (#3332)

* 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
This commit is contained in:
Michael Genson 2024-03-25 11:04:42 -05:00 committed by GitHub
parent 21886ab4b8
commit dfbc890f2c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 101 additions and 55 deletions

View file

@ -9,7 +9,7 @@
color="accent" color="accent"
:small="small" :small="small"
dark dark
:to="isOwnGroup ? `${baseRecipeRoute}?${urlPrefix}=${category.id}` : undefined" :to="`${baseRecipeRoute}?${urlPrefix}=${category.id}`"
> >
{{ truncateText(category.name) }} {{ truncateText(category.name) }}
</v-chip> </v-chip>
@ -18,7 +18,6 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, useContext, useRoute } from "@nuxtjs/composition-api"; 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"; import { RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/user";
export type UrlPrefixParam = "tags" | "categories" | "tools"; export type UrlPrefixParam = "tags" | "categories" | "tools";
@ -56,7 +55,6 @@ export default defineComponent({
}, },
setup(props) { setup(props) {
const { $auth } = useContext(); const { $auth } = useContext();
const { isOwnGroup } = useLoggedInState();
const route = useRoute(); const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "") const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "")
@ -74,7 +72,6 @@ export default defineComponent({
return { return {
baseRecipeRoute, baseRecipeRoute,
isOwnGroup,
truncateText, truncateText,
}; };
}, },

View file

@ -124,11 +124,12 @@
<v-divider></v-divider> <v-divider></v-divider>
<v-container class="mt-6 px-md-6"> <v-container class="mt-6 px-md-6">
<RecipeCardSection <RecipeCardSection
v-if="state.ready"
class="mt-n5" class="mt-n5"
:icon="$globals.icons.search" :icon="$globals.icons.search"
:title="$tc('search.results')" :title="$tc('search.results')"
:recipes="recipes" :recipes="recipes"
:query="passedQuery" :query="passedQueryWithSeed"
@replaceRecipes="replaceRecipes" @replaceRecipes="replaceRecipes"
@appendRecipes="appendRecipes" @appendRecipes="appendRecipes"
/> />
@ -137,11 +138,12 @@
</template> </template>
<script lang="ts"> <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 { watchDebounced } from "@vueuse/shared";
import SearchFilter from "~/components/Domain/SearchFilter.vue"; import SearchFilter from "~/components/Domain/SearchFilter.vue";
import { useLoggedInState } from "~/composables/use-logged-in-state"; import { useLoggedInState } from "~/composables/use-logged-in-state";
import { useCategoryStore, useFoodStore, useTagStore, useToolStore } from "~/composables/store"; import { useCategoryStore, useFoodStore, useTagStore, useToolStore } from "~/composables/store";
import { useUserSortPreferences } from "~/composables/use-users/preferences";
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue"; import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
import { IngredientFood, RecipeCategory, 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";
@ -161,6 +163,7 @@ export default defineComponent({
const { isOwnGroup } = useLoggedInState(); const { isOwnGroup } = useLoggedInState();
const state = ref({ const state = ref({
auto: true, auto: true,
ready: false,
search: "", search: "",
orderBy: "created_at", orderBy: "created_at",
orderDirection: "desc" as "asc" | "desc", orderDirection: "desc" as "asc" | "desc",
@ -174,6 +177,7 @@ export default defineComponent({
const route = useRoute(); const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || ""); 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 { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
const categories = isOwnGroup.value ? useCategoryStore() : usePublicCategoryStore(groupSlug.value); const categories = isOwnGroup.value ? useCategoryStore() : usePublicCategoryStore(groupSlug.value);
@ -188,7 +192,30 @@ export default defineComponent({
const tools = isOwnGroup.value ? useToolStore() : usePublicToolStore(groupSlug.value); const tools = isOwnGroup.value ? useToolStore() : usePublicToolStore(groupSlug.value);
const selectedTools = ref<NoUndefinedField<RecipeTool>[]>([]); 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() { function reset() {
state.value.search = ""; state.value.search = "";
@ -203,10 +230,6 @@ export default defineComponent({
selectedTags.value = []; selectedTags.value = [];
selectedTools.value = []; selectedTools.value = [];
router.push({
query: {},
});
search(); search();
} }
@ -215,7 +238,8 @@ export default defineComponent({
} }
function toIDArray(array: { id: string }[]) { 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() { function hideKeyboard() {
@ -225,40 +249,33 @@ export default defineComponent({
const input: Ref<any> = ref(null); const input: Ref<any> = ref(null);
async function search() { async function search() {
await router.push({ const oldQueryValueString = JSON.stringify(passedQuery.value);
query: { const newQueryValue = calcPassedQuery();
categories: toIDArray(selectedCategories.value), const newQueryValueString = JSON.stringify(newQueryValue);
foods: toIDArray(selectedFoods.value), if (oldQueryValueString === newQueryValueString) {
tags: toIDArray(selectedTags.value), return;
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,
},
},
});
passedQuery.value = { passedQuery.value = newQueryValue;
search: state.value.search, const query = {
categories: toIDArray(selectedCategories.value), categories: passedQuery.value.categories,
foods: toIDArray(selectedFoods.value), foods: passedQuery.value.foods,
tags: toIDArray(selectedTags.value), tags: passedQuery.value.tags,
tools: toIDArray(selectedTools.value), tools: passedQuery.value.tools,
requireAllCategories: state.value.requireAllCategories, // Only add the query param if it's or not default
requireAllTags: state.value.requireAllTags, ...{
requireAllTools: state.value.requireAllTools, auto: state.value.auto ? undefined : "false",
requireAllFoods: state.value.requireAllFoods, search: passedQuery.value.search === "" ? undefined : passedQuery.value.search,
orderBy: state.value.orderBy, orderBy: passedQuery.value.orderBy === "created_at" ? undefined : passedQuery.value.orderBy,
orderDirection: state.value.orderDirection, orderDirection: passedQuery.value.orderDirection === "desc" ? undefined : passedQuery.value.orderDirection,
_searchSeed: Date.now().toString() 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( function waitUntilAndExecute(
@ -329,13 +346,20 @@ export default defineComponent({
}, },
]; ];
onMounted(() => { watch(
// Hydrate Search () => route.value.query,
// wait for stores to be hydrated () => {
if (state.value.ready) {
hydrateSearch();
}
},
{
deep: true,
},
)
// read query params async function hydrateSearch() {
const query = router.currentRoute.query; const query = router.currentRoute.query;
if (query.auto) { if (query.auto) {
state.value.auto = query.auto === "true"; state.value.auto = query.auto === "true";
} }
@ -367,6 +391,8 @@ export default defineComponent({
} }
) )
); );
} else {
selectedCategories.value = [];
} }
if (query.foods) { if (query.foods) {
@ -384,6 +410,8 @@ export default defineComponent({
} }
) )
); );
} else {
selectedFoods.value = [];
} }
if (query.tags) { if (query.tags) {
@ -396,6 +424,8 @@ export default defineComponent({
} }
) )
); );
} else {
selectedTags.value = [];
} }
if (query.tools) { if (query.tools) {
@ -408,11 +438,28 @@ export default defineComponent({
} }
) )
); );
} else {
selectedTools.value = [];
} }
Promise.allSettled(promises).then(() => { await Promise.allSettled(promises);
search(); };
});
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( watchDebounced(
@ -430,7 +477,7 @@ export default defineComponent({
selectedTools, selectedTools,
], ],
async () => { async () => {
if (state.value.auto) { if (state.value.ready && state.value.auto) {
await search(); await search();
} }
}, },
@ -463,7 +510,7 @@ export default defineComponent({
recipes, recipes,
removeRecipe, removeRecipe,
replaceRecipes, replaceRecipes,
passedQuery, passedQueryWithSeed,
}; };
}, },
head: {}, head: {},

View file

@ -20,6 +20,7 @@ export interface UserRecipePreferences {
filterNull: boolean; filterNull: boolean;
sortIcon: string; sortIcon: string;
useMobileCards: boolean; useMobileCards: boolean;
searchQuery: string;
} }
export interface UserShoppingListPreferences { export interface UserShoppingListPreferences {
@ -59,6 +60,7 @@ export function useUserSortPreferences(): Ref<UserRecipePreferences> {
filterNull: false, filterNull: false,
sortIcon: $globals.icons.sortAlphabeticalAscending, sortIcon: $globals.icons.sortAlphabeticalAscending,
useMobileCards: false, useMobileCards: false,
searchQuery: "",
}, },
{ mergeDefaults: true } { mergeDefaults: true }
// we cast to a Ref because by default it will return an optional type ref // we cast to a Ref because by default it will return an optional type ref