mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-08-02 20:15:24 +02:00
* feat: server side search API (#2112) * refactor repository_recipes filter building * add food filter to recipe repository page_all * fix query type annotations * working search * add tests and make sure title matches are ordered correctly * remove instruction matching again * fix formatting and small issues * fix another linting error * make search test no rely on actual words * fix failing postgres compiled query * revise incorrectly ordered migration * automatically extract latest migration version * test migration orderes * run type generators * new search function * wip: new search page * sortable field options * fix virtual scroll issue * fix search casing bug * finalize search filters/sorts * remove old composable * fix type errors --------- Co-authored-by: Sören <fleshgolem@gmx.net>
This commit is contained in:
parent
fc105dcebc
commit
71f8c1066a
36 changed files with 1057 additions and 822 deletions
|
@ -35,7 +35,7 @@
|
|||
</v-card-actions>
|
||||
|
||||
<RecipeCardMobile
|
||||
v-for="(recipe, index) in results.slice(0, 10)"
|
||||
v-for="(recipe, index) in searchResults"
|
||||
:key="index"
|
||||
:tabindex="index"
|
||||
class="ma-1 arrow-nav"
|
||||
|
@ -55,9 +55,10 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent, toRefs, reactive, ref, watch, useRoute } from "@nuxtjs/composition-api";
|
||||
import { watchDebounced } from "@vueuse/shared";
|
||||
import RecipeCardMobile from "./RecipeCardMobile.vue";
|
||||
import { useRecipes, allRecipes, useRecipeSearch } from "~/composables/recipes";
|
||||
import { RecipeSummary } from "~/lib/api/types/recipe";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
const SELECTED_EVENT = "selected";
|
||||
export default defineComponent({
|
||||
components: {
|
||||
|
@ -65,12 +66,10 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
setup(_, context) {
|
||||
const { refreshRecipes } = useRecipes(true, false, true);
|
||||
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
selectedIndex: -1,
|
||||
searchResults: [],
|
||||
searchResults: [] as RecipeSummary[],
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
|
@ -78,14 +77,11 @@ export default defineComponent({
|
|||
const dialog = ref(false);
|
||||
|
||||
// Reset or Grab Recipes on Change
|
||||
watch(dialog, async (val) => {
|
||||
watch(dialog, (val) => {
|
||||
if (!val) {
|
||||
search.value = "";
|
||||
state.selectedIndex = -1;
|
||||
} else if (allRecipes.value && allRecipes.value.length <= 0) {
|
||||
state.loading = true;
|
||||
await refreshRecipes();
|
||||
state.loading = false;
|
||||
state.searchResults = [];
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -140,13 +136,33 @@ export default defineComponent({
|
|||
dialog.value = true;
|
||||
}
|
||||
function close() {
|
||||
|
||||
dialog.value = false;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Basic Search
|
||||
const api = useUserApi();
|
||||
const search = ref("")
|
||||
|
||||
watchDebounced(search, async (val) => {
|
||||
console.log(val)
|
||||
if (val) {
|
||||
state.loading = true;
|
||||
// @ts-expect-error - inferred type is wrong
|
||||
const { data, error } = await api.recipes.search({ search: val as string, page: 1, perPage: 10 });
|
||||
|
||||
if (error || !data) {
|
||||
console.error(error);
|
||||
state.searchResults = [];
|
||||
} else {
|
||||
state.searchResults = data.items;
|
||||
}
|
||||
|
||||
state.loading = false;
|
||||
}
|
||||
}, { debounce: 500, maxWait: 1000 });
|
||||
|
||||
const { search, results } = useRecipeSearch(allRecipes);
|
||||
// ===========================================================================
|
||||
// Select Handler
|
||||
|
||||
|
@ -155,7 +171,7 @@ export default defineComponent({
|
|||
context.emit(SELECTED_EVENT, recipe);
|
||||
}
|
||||
|
||||
return { allRecipes, refreshRecipes, ...toRefs(state), dialog, open, close, handleSelect, search, results };
|
||||
return { ...toRefs(state), dialog, open, close, handleSelect, search, };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
105
frontend/components/Domain/SearchFilter.vue
Normal file
105
frontend/components/Domain/SearchFilter.vue
Normal file
|
@ -0,0 +1,105 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-menu v-model="state.menu" offset-y bottom nudge-bottom="3" :close-on-content-click="false">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-badge :value="selected.length > 0" small overlap color="primary" :content="selected.length">
|
||||
<v-btn small color="accent" dark v-bind="attrs" v-on="on">
|
||||
<slot></slot>
|
||||
</v-btn>
|
||||
</v-badge>
|
||||
</template>
|
||||
<v-card width="400">
|
||||
<v-card-text>
|
||||
<v-text-field v-model="state.search" class="mb-2" hide-details dense label="Search" clearable />
|
||||
<v-switch
|
||||
v-if="requireAll != undefined"
|
||||
v-model="requireAllValue"
|
||||
dense
|
||||
small
|
||||
:label="`${requireAll ? $tc('search.has-all') : $tc('search.has-any')}`"
|
||||
>
|
||||
</v-switch>
|
||||
<v-card v-if="filtered.length > 0" flat outlined>
|
||||
<v-virtual-scroll :items="filtered" height="300" item-height="51">
|
||||
<template #default="{ item }">
|
||||
<v-list-item :key="item.id" dense :value="item">
|
||||
<v-list-item-action>
|
||||
<v-checkbox v-model="selected" :value="item"></v-checkbox>
|
||||
</v-list-item-action>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title> {{ item.name }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
</template>
|
||||
</v-virtual-scroll>
|
||||
</v-card>
|
||||
<div v-else>
|
||||
<v-alert type="info" text> No results found </v-alert>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, computed } from "@nuxtjs/composition-api";
|
||||
|
||||
export interface SelectableItem {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
items: {
|
||||
type: Array as () => SelectableItem[],
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: Array as () => any[],
|
||||
required: true,
|
||||
},
|
||||
requireAll: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const state = reactive({
|
||||
search: "",
|
||||
menu: false,
|
||||
});
|
||||
|
||||
const requireAllValue = computed({
|
||||
get: () => props.requireAll,
|
||||
set: (value) => {
|
||||
context.emit("update:requireAll", value);
|
||||
},
|
||||
});
|
||||
|
||||
const selected = computed({
|
||||
get: () => props.value as SelectableItem[],
|
||||
set: (value) => {
|
||||
context.emit("input", value);
|
||||
},
|
||||
});
|
||||
|
||||
const filtered = computed(() => {
|
||||
if (!state.search) {
|
||||
return props.items;
|
||||
}
|
||||
|
||||
return props.items.filter((item) => item.name.toLowerCase().includes(state.search.toLowerCase()));
|
||||
});
|
||||
|
||||
return {
|
||||
requireAllValue,
|
||||
state,
|
||||
selected,
|
||||
filtered,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -56,9 +56,8 @@ import { defineComponent, computed, ref, useContext } from "@nuxtjs/composition-
|
|||
import ShoppingListItemEditor from "./ShoppingListItemEditor.vue";
|
||||
import MultiPurposeLabel from "./MultiPurposeLabel.vue";
|
||||
import { ShoppingListItemOut } from "~/lib/api/types/group";
|
||||
import { MultiPurposeLabelOut } from "~/lib/api/types/labels";
|
||||
import { MultiPurposeLabelOut, MultiPurposeLabelSummary } from "~/lib/api/types/labels";
|
||||
import { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
|
||||
import { MultiPurposeLabelSummary } from "~/lib/api/types/user";
|
||||
|
||||
interface actions {
|
||||
text: string;
|
||||
|
|
|
@ -176,20 +176,19 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
// V-Model Support
|
||||
const drawer = computed({
|
||||
// V-Model Support
|
||||
const drawer = computed({
|
||||
get: () => {
|
||||
return props.value;
|
||||
},
|
||||
set: (val) => {
|
||||
if(window.innerWidth < 760 && state.hasOpenedBefore === false){
|
||||
state.hasOpenedBefore = true;
|
||||
val = false
|
||||
context.emit("input", val);
|
||||
}
|
||||
else{
|
||||
context.emit("input", val);
|
||||
}
|
||||
if (window.innerWidth < 760 && state.hasOpenedBefore === false) {
|
||||
state.hasOpenedBefore = true;
|
||||
val = false;
|
||||
context.emit("input", val);
|
||||
} else {
|
||||
context.emit("input", val);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue