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"
|
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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -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),
|
}
|
||||||
|
|
||||||
|
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
|
// Only add the query param if it's or not default
|
||||||
...{
|
...{
|
||||||
auto: state.value.auto ? undefined : "false",
|
auto: state.value.auto ? undefined : "false",
|
||||||
search: state.value.search === "" ? undefined : state.value.search,
|
search: passedQuery.value.search === "" ? undefined : passedQuery.value.search,
|
||||||
orderBy: state.value.orderBy === "createdAt" ? undefined : state.value.orderBy,
|
orderBy: passedQuery.value.orderBy === "created_at" ? undefined : passedQuery.value.orderBy,
|
||||||
orderDirection: state.value.orderDirection === "desc" ? undefined : state.value.orderDirection,
|
orderDirection: passedQuery.value.orderDirection === "desc" ? undefined : passedQuery.value.orderDirection,
|
||||||
requireAllCategories: state.value.requireAllCategories ? "true" : undefined,
|
requireAllCategories: passedQuery.value.requireAllCategories ? "true" : undefined,
|
||||||
requireAllTags: state.value.requireAllTags ? "true" : undefined,
|
requireAllTags: passedQuery.value.requireAllTags ? "true" : undefined,
|
||||||
requireAllTools: state.value.requireAllTools ? "true" : undefined,
|
requireAllTools: passedQuery.value.requireAllTools ? "true" : undefined,
|
||||||
requireAllFoods: state.value.requireAllFoods ? "true" : undefined,
|
requireAllFoods: passedQuery.value.requireAllFoods ? "true" : undefined,
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
});
|
await router.push({ query });
|
||||||
|
preferences.value.searchQuery = JSON.stringify(query);
|
||||||
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()
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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: {},
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user