mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -04: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:
parent
21886ab4b8
commit
dfbc890f2c
@ -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>
|
||||
@ -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";
|
||||
@ -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 || "")
|
||||
@ -74,7 +72,6 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
baseRecipeRoute,
|
||||
isOwnGroup,
|
||||
truncateText,
|
||||
};
|
||||
},
|
||||
|
@ -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"
|
||||
/>
|
||||
@ -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";
|
||||
@ -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",
|
||||
@ -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);
|
||||
@ -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 = "";
|
||||
@ -203,10 +230,6 @@ export default defineComponent({
|
||||
selectedTags.value = [];
|
||||
selectedTools.value = [];
|
||||
|
||||
router.push({
|
||||
query: {},
|
||||
});
|
||||
|
||||
search();
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
@ -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(
|
||||
@ -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";
|
||||
}
|
||||
@ -367,6 +391,8 @@ export default defineComponent({
|
||||
}
|
||||
)
|
||||
);
|
||||
} else {
|
||||
selectedCategories.value = [];
|
||||
}
|
||||
|
||||
if (query.foods) {
|
||||
@ -384,6 +410,8 @@ export default defineComponent({
|
||||
}
|
||||
)
|
||||
);
|
||||
} else {
|
||||
selectedFoods.value = [];
|
||||
}
|
||||
|
||||
if (query.tags) {
|
||||
@ -396,6 +424,8 @@ export default defineComponent({
|
||||
}
|
||||
)
|
||||
);
|
||||
} else {
|
||||
selectedTags.value = [];
|
||||
}
|
||||
|
||||
if (query.tools) {
|
||||
@ -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(
|
||||
@ -430,7 +477,7 @@ export default defineComponent({
|
||||
selectedTools,
|
||||
],
|
||||
async () => {
|
||||
if (state.value.auto) {
|
||||
if (state.value.ready && state.value.auto) {
|
||||
await search();
|
||||
}
|
||||
},
|
||||
@ -463,7 +510,7 @@ export default defineComponent({
|
||||
recipes,
|
||||
removeRecipe,
|
||||
replaceRecipes,
|
||||
passedQuery,
|
||||
passedQueryWithSeed,
|
||||
};
|
||||
},
|
||||
head: {},
|
||||
|
@ -20,6 +20,7 @@ export interface UserRecipePreferences {
|
||||
filterNull: boolean;
|
||||
sortIcon: string;
|
||||
useMobileCards: boolean;
|
||||
searchQuery: string;
|
||||
}
|
||||
|
||||
export interface UserShoppingListPreferences {
|
||||
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user