Files
recipes/vue3/src/pages/SearchPage.vue
2025-03-31 08:58:32 +02:00

964 lines
33 KiB
Vue

<template>
<v-container>
<v-row>
<v-col cols="12" md="6" offset-md="3">
<v-text-field :label="$t('Search')"
v-model="query"
:loading="loading"
@submit="searchRecipes({page: 1})"
@keydown.enter="searchRecipes({page: 1})"
@click:clear="query = ''"
clearable hide-details>
<template v-slot:append>
<v-btn @click="panel ='search' " v-if="panel == ''" color="primary" icon><i class="fa-solid fa-caret-down"></i></v-btn>
<v-btn @click="panel ='' " v-if="panel == 'search'" color="primary" icon><i class="fa-solid fa-caret-up"></i></v-btn>
</template>
</v-text-field>
</v-col>
</v-row>
<v-row dense>
<v-col>
<v-expansion-panels v-model="panel">
<v-expansion-panel value="search">
<v-expansion-panel-text>
<v-form :disabled="loading" class="mt-4">
<div v-for="filter in Object.values(filters)" :key="filter.id">
<template v-if="filter.enabled">
<component :="filter" :is="filter.is" density="compact" v-model="filter.modelValue">
<template #append>
<v-btn icon="fa-solid fa-times" size="small" variant="plain"
@click="filter.enabled = false; filter.modelValue = filter.default"></v-btn>
</template>
</component>
</template>
</div>
<v-divider class="mt-2 mb-2"></v-divider>
<v-autocomplete :items="availableFilters"
@update:model-value="(item:string) =>{ filters[item].enabled = true; nextTick(() => {addFilterSelect = null})}" density="compact"
:label="$t('AddFilter')" v-model="addFilterSelect"></v-autocomplete>
<model-select model="CustomFilter" v-model="selectedCustomFilter" density="compact">
<template #append>
<v-btn icon="fa-solid fa-upload" color="warning" :disabled="Object.keys(selectedCustomFilter).length == 0"
@click="loadCustomFilter()"></v-btn>
<v-btn icon="$save" class="ms-1" color="save" @click="saveCustomFilter()"></v-btn>
</template>
</model-select>
</v-form>
<v-row>
<v-col cols="6">
<v-select :label="$t('View')" v-model="useUserPreferenceStore().deviceSettings.search_viewMode"
:items="[{title: $t('Table'), value: 'table'}, {title: $t('Cards'), value: 'grid'},]" density="compact"></v-select>
</v-col>
<v-col cols="6">
<v-select class="float-right" :label="$t('PerPage')" v-model="pageSize" :items="[10,25,50,100]" density="compact"
width="100%"></v-select>
</v-col>
</v-row>
</v-expansion-panel-text>
<v-card-actions v-if="panel == 'search'">
<v-btn @click="reset()" prepend-icon="fa-solid fa-circle-xmark">{{ $t('Reset') }}</v-btn>
<v-btn @click="searchRecipes({page: 1})" prepend-icon="$search">{{ $t('Search') }}</v-btn>
</v-card-actions>
</v-expansion-panel>
</v-expansion-panels>
</v-col>
</v-row>
<v-row v-if="recipes.length > 0 && useUserPreferenceStore().deviceSettings.search_viewMode == 'table'">
<v-col>
<v-card>
<v-data-table-server
@update:options="searchRecipes"
:loading="loading"
:items="recipes"
:headers="tableHeaders"
:page="page"
:items-per-page="pageSize"
:items-length="tableItemCount"
@click:row="handleRowClick"
disable-sort
hide-default-header
hide-default-footer
>
<template #item.image="{item}">
<v-avatar :image="item.image" size="x-large" class="mt-1 mb-1" v-if="item.image"></v-avatar>
<v-avatar color="primary" variant="tonal" size="x-large" class="mt-1 mb-1" v-else>
<random-icon></random-icon>
</v-avatar>
</template>
<template #item.keywords="{item}">
<keywords-bar :keywords="item.keywords"></keywords-bar>
</template>
<template #item.action="{item}">
<recipe-context-menu :recipe="item"></recipe-context-menu>
</template>
</v-data-table-server>
</v-card>
</v-col>
</v-row>
<template v-if="recipes.length > 0 && useUserPreferenceStore().deviceSettings.search_viewMode == 'grid'">
<v-row>
<v-col cols="6" md="4" v-for="r in recipes" :key="r.id" class="pa-0">
<recipe-card :recipe="r"></recipe-card>
</v-col>
</v-row>
</template>
<v-row>
<v-col cols="12" md="6" offset-md="3">
<v-pagination v-model="page" :length="Math.ceil(tableItemCount/pageSize)"
@update:modelValue="searchRecipes({page: page})" class="ms-2 me-2" size="small"
></v-pagination>
</v-col>
</v-row>
<v-dialog v-model="dialog">
<v-card>
<v-closable-card-title :title="$t('SavedSearch')" v-model="dialog"></v-closable-card-title>
<v-card-text>
<v-text-field :label="$t('Name')" v-model="newFilterName"></v-text-field>
</v-card-text>
<v-card-actions>
<v-btn prepend-icon="$create" color="create" @click="createCustomFilter()">{{ $t('Create') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
<script setup lang="ts">
import {computed, nextTick, onMounted, ref, toRaw, watch} from "vue";
import {ApiApi, ApiRecipeListRequest, CustomFilter, RecipeOverview} from "@/openapi";
import {useI18n} from "vue-i18n";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
import ModelSelect from "@/components/inputs/ModelSelect.vue";
import {VNumberInput} from 'vuetify/labs/VNumberInput'
import {VDateInput} from 'vuetify/labs/VDateInput'
import RecipeContextMenu from "@/components/inputs/RecipeContextMenu.vue";
import {useRouter} from "vue-router";
import KeywordsBar from "@/components/display/KeywordsBar.vue";
import {VDataTableUpdateOptions} from "@/vuetify";
import VClosableCardTitle from "@/components/dialogs/VClosableCardTitle.vue";
import RecipeCard from "@/components/display/RecipeCard.vue";
import {useDisplay} from "vuetify";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import {useRouteQuery} from "@vueuse/router";
import {routeQueryDateTransformer, stringToBool, toNumberArray} from "@/utils/utils";
import RandomIcon from "@/components/display/RandomIcon.vue";
import {VSelect, VTextField} from "vuetify/components";
import RatingField from "@/components/inputs/RatingField.vue";
const {t} = useI18n()
const router = useRouter()
const {mdAndUp} = useDisplay()
const query = useRouteQuery('query', "")
const page = useRouteQuery('page', 1, {transform: Number})
const pageSize = useRouteQuery('pageSize', useUserPreferenceStore().deviceSettings.general_tableItemsPerPage, {transform: Number})
/**
* filters that are not yet enabled
*/
const availableFilters = computed(() => {
let f: Array<{ value: string, title: string }> = []
Object.entries(filters.value).forEach((entry) => {
let [key, filter] = entry
if (!filter.enabled) {
f.push({value: filter.id, title: filter.label})
}
})
return f
})
const loading = ref(false)
const dialog = ref(false)
const panel = ref('')
const addFilterSelect = ref<string | null>(null)
const tableHeaders = computed(() => {
let headers = [
{title: t('Image'), width: '1%', noBreak: true, key: 'image',},
{title: t('Name'), key: 'name',},
]
if (mdAndUp.value) {
headers.push({title: t('Keywords'), key: 'keywords',},)
}
headers.push({title: t('Actions'), key: 'action', width: '1%', noBreak: true, align: 'end'},)
return headers
})
const tableItemCount = ref(0)
const recipes = ref([] as RecipeOverview[])
const selectedCustomFilter = ref({} as CustomFilter)
const newFilterName = ref('')
/**
* handle query updates when using the GlobalSearchDialog on the search page directly
*/
// TODO this also makes the search update on every stroke, do we want this?
watch(() => query.value, () => {
searchRecipes({page: 1})
})
/**
* perform initial search on mounted
*/
onMounted(() => {
enableFiltersWithValues()
searchRecipes({page: page.value})
})
/**
* perform the recipe search with the given options
* @param options
*/
function searchRecipes(options: VDataTableUpdateOptions) {
let api = new ApiApi()
loading.value = true
let searchParameters = {
query: query.value,
page: page.value,
pageSize: pageSize.value,
} as ApiRecipeListRequest
Object.values(filters.value).forEach((filter) => {
if (!isFilterDefaultValue(filter)) {
searchParameters[filter.id] = filter.modelValue
}
})
api.apiRecipeList(searchParameters).then((r) => {
recipes.value = r.results
tableItemCount.value = r.count
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
}).finally(() => {
loading.value = false
window.scrollTo({top: 0, behavior: 'smooth'})
})
}
/**
* reset all search parameters and perform emtpy searchj
*/
function reset() {
page.value = 1
query.value = ''
Object.values(filters.value).forEach((filter) => {
filter.enabled = false
filter.modelValue = filter.default
})
recipes.value = []
searchRecipes({page: 1})
}
/**
* handle clicking a table row by opening the selected recipe
* @param event
* @param data
*/
function handleRowClick(event: PointerEvent, data: any) {
router.push({name: 'RecipeViewPage', params: {id: recipes.value[data.index].id}})
}
/**
* enable UI of filters that have a value that is not the default for the given filter
*/
function enableFiltersWithValues() {
Object.values(filters.value).forEach((filter) => {
if (!isFilterDefaultValue(filter)) {
filter.enabled = true
}
})
}
/**
* determines if the current value of a filter is its default value
* @param filter
*/
function isFilterDefaultValue(filter: any) {
if (Array.isArray(filter.default) && Array.isArray(filter.modelValue)) {
return filter.default.length == filter.modelValue.length
} else if (isNaN(filter.default) && isNaN(filter.modelValue)) {
return true
} else {
return toRaw(filter.default) === filter.modelValue
}
}
// -------------------------------------------
// --------- Logic for saved filters ---------
// -------------------------------------------
/**
* triggered by save button, if filter exists update it, if not open dialog to create a new filter
*/
function saveCustomFilter() {
let api = new ApiApi()
if (Object.keys(selectedCustomFilter.value).length > 0) {
loading.value = true
selectedCustomFilter.value.search = JSON.stringify(apiRecipeListRequestToCustomFilter())
api.apiCustomFilterUpdate({id: selectedCustomFilter.value.id!, customFilter: selectedCustomFilter.value}).then((r) => {
selectedCustomFilter.value = r
}).catch(err => {
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
}).finally(() => {
loading.value = false
})
} else {
dialog.value = true
}
}
/**
* create new filter
*/
function createCustomFilter() {
let api = new ApiApi()
dialog.value = false
loading.value = true
api.apiCustomFilterCreate({customFilter: {name: newFilterName.value, search: JSON.stringify(apiRecipeListRequestToCustomFilter())} as CustomFilter}).then((r) => {
selectedCustomFilter.value = r
}).catch(err => {
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
}).finally(() => {
loading.value = false
})
}
function loadCustomFilter() {
let obj = customFilterToApiRecipeListRequest(selectedCustomFilter.value.search)
console.log(obj, selectedCustomFilter.value.search)
Object.keys(obj).forEach((key) => {
urlSearchParams[key] = obj[key]
})
}
// TODO temporary function to convert old saved search format, either make proper db table or convert to native new format
/**
* turn data in the format of a CustomFilter into the format needed for api request
* @param customFilterParams
*/
function customFilterToApiRecipeListRequest(customFilterParams: any) {
let recipeListRequestParams: any = {};
customFilterParams = JSON.parse(customFilterParams)
if (customFilterParams['books'] != null) {
recipeListRequestParams['books'] = customFilterParams['books'];
}
if (customFilterParams['books_and'] != null) {
recipeListRequestParams['booksAnd'] = customFilterParams['books_and'];
}
if (customFilterParams['books_and_not'] != null) {
recipeListRequestParams['booksAndNot'] = customFilterParams['books_and_not'];
}
if (customFilterParams['books_or'] != null) {
recipeListRequestParams['booksOr'] = customFilterParams['books_or'];
}
if (customFilterParams['books_or_not'] != null) {
recipeListRequestParams['booksOrNot'] = customFilterParams['books_or_not'];
}
if (customFilterParams['cookedon'] != null) {
recipeListRequestParams['cookedon'] = customFilterParams['cookedon'];
}
if (customFilterParams['createdon'] != null) {
recipeListRequestParams['createdon'] = customFilterParams['createdon'];
}
if (customFilterParams['foods'] != null) {
recipeListRequestParams['foods'] = customFilterParams['foods'];
}
if (customFilterParams['foods_and'] != null) {
recipeListRequestParams['foodsAnd'] = customFilterParams['foods_and'];
}
if (customFilterParams['foods_and_not'] != null) {
recipeListRequestParams['foodsAndNot'] = customFilterParams['foods_and_not'];
}
if (customFilterParams['foods_or'] != null) {
recipeListRequestParams['foodsOr'] = customFilterParams['foods_or'];
}
if (customFilterParams['foods_or_not'] != null) {
recipeListRequestParams['foodsOrNot'] = customFilterParams['foods_or_not'];
}
if (customFilterParams['internal'] != null) {
recipeListRequestParams['internal'] = customFilterParams['internal'];
}
if (customFilterParams['keywords'] != null) {
recipeListRequestParams['keywords'] = customFilterParams['keywords'];
}
if (customFilterParams['keywords_and'] != null) {
recipeListRequestParams['keywordsAnd'] = customFilterParams['keywords_and'];
}
if (customFilterParams['keywords_and_not'] != null) {
recipeListRequestParams['keywordsAndNot'] = customFilterParams['keywords_and_not'];
}
if (customFilterParams['keywords_or'] != null) {
recipeListRequestParams['keywordsOr'] = customFilterParams['keywords_or'];
}
if (customFilterParams['keywords_or_not'] != null) {
recipeListRequestParams['keywordsOrNot'] = customFilterParams['keywords_or_not'];
}
if (customFilterParams['makenow'] != null) {
recipeListRequestParams['makenow'] = customFilterParams['makenow'];
}
if (customFilterParams['new'] != null) {
recipeListRequestParams['_new'] = customFilterParams['new'];
}
if (customFilterParams['num_recent'] != null) {
recipeListRequestParams['numRecent'] = customFilterParams['num_recent'];
}
if (customFilterParams['page'] != null) {
recipeListRequestParams['page'] = customFilterParams['page'];
}
if (customFilterParams['page_size'] != null) {
recipeListRequestParams['pageSize'] = customFilterParams['page_size'];
}
if (customFilterParams['query'] != null) {
recipeListRequestParams['query'] = customFilterParams['query'];
}
if (customFilterParams['random'] != null) {
recipeListRequestParams['random'] = customFilterParams['random'];
}
if (customFilterParams['rating'] != null) {
recipeListRequestParams['rating'] = customFilterParams['rating'];
}
if (customFilterParams['timescooked'] != null) {
recipeListRequestParams['timescooked'] = customFilterParams['timescooked'];
}
if (customFilterParams['units'] != null) {
recipeListRequestParams['units'] = customFilterParams['units'];
}
if (customFilterParams['updatedon'] != null) {
recipeListRequestParams['updatedon'] = customFilterParams['updatedon'];
}
if (customFilterParams['viewedon'] != null) {
recipeListRequestParams['viewedon'] = customFilterParams['viewedon'];
}
return recipeListRequestParams
}
function apiRecipeListRequestToCustomFilter() {
let customFilterParams: any = {};
if (urlSearchParams['books'] != null) {
customFilterParams['books'] = urlSearchParams['books'];
}
if (urlSearchParams['booksAnd'] != null) {
customFilterParams['books_and'] = urlSearchParams['booksAnd'];
}
if (urlSearchParams['booksAndNot'] != null) {
customFilterParams['books_and_not'] = urlSearchParams['booksAndNot'];
}
if (urlSearchParams['booksOr'] != null) {
customFilterParams['books_or'] = urlSearchParams['booksOr'];
}
if (urlSearchParams['booksOrNot'] != null) {
customFilterParams['books_or_not'] = urlSearchParams['booksOrNot'];
}
if (urlSearchParams['cookedon'] != null) {
customFilterParams['cookedon'] = urlSearchParams['cookedon'];
}
if (urlSearchParams['createdon'] != null) {
customFilterParams['createdon'] = urlSearchParams['createdon'];
}
if (urlSearchParams['foods'] != null) {
customFilterParams['foods'] = urlSearchParams['foods'];
}
if (urlSearchParams['foodsAnd'] != null) {
customFilterParams['foods_and'] = urlSearchParams['foodsAnd'];
}
if (urlSearchParams['foodsAndNot'] != null) {
customFilterParams['foods_and_not'] = urlSearchParams['foodsAndNot'];
}
if (urlSearchParams['foodsOr'] != null) {
customFilterParams['foods_or'] = urlSearchParams['foodsOr'];
}
if (urlSearchParams['foodsOrNot'] != null) {
customFilterParams['foods_or_not'] = urlSearchParams['foodsOrNot'];
}
if (urlSearchParams['internal'] != null) {
customFilterParams['internal'] = urlSearchParams['internal'];
}
if (urlSearchParams['keywords'] != null) {
customFilterParams['keywords'] = urlSearchParams['keywords'];
}
if (urlSearchParams['keywordsAnd'] != null) {
customFilterParams['keywords_and'] = urlSearchParams['keywordsAnd'];
}
if (urlSearchParams['keywordsAndNot'] != null) {
customFilterParams['keywords_and_not'] = urlSearchParams['keywordsAndNot'];
}
if (urlSearchParams['keywordsOr'] != null) {
customFilterParams['keywords_or'] = urlSearchParams['keywordsOr'];
}
if (urlSearchParams['keywordsOrNot'] != null) {
customFilterParams['keywords_or_not'] = urlSearchParams['keywordsOrNot'];
}
if (urlSearchParams['makenow'] != null) {
customFilterParams['makenow'] = urlSearchParams['makenow'];
}
if (urlSearchParams['_new'] != null) {
customFilterParams['new'] = urlSearchParams['_new'];
}
if (urlSearchParams['numRecent'] != null) {
customFilterParams['num_recent'] = urlSearchParams['numRecent'];
}
if (urlSearchParams['page'] != null) {
customFilterParams['page'] = urlSearchParams['page'];
}
if (urlSearchParams['pageSize'] != null) {
customFilterParams['page_size'] = urlSearchParams['pageSize'];
}
if (urlSearchParams['query'] != null) {
customFilterParams['query'] = urlSearchParams['query'];
}
if (urlSearchParams['random'] != null) {
customFilterParams['random'] = urlSearchParams['random'];
}
if (urlSearchParams['rating'] != null) {
customFilterParams['rating'] = urlSearchParams['rating'];
}
if (urlSearchParams['timescooked'] != null) {
customFilterParams['timescooked'] = urlSearchParams['timescooked'];
}
if (urlSearchParams['units'] != null) {
customFilterParams['units'] = urlSearchParams['units'];
}
if (urlSearchParams['updatedon'] != null) {
customFilterParams['updatedon'] = urlSearchParams['updatedon'];
}
if (urlSearchParams['viewedon'] != null) {
customFilterParams['viewedon'] = urlSearchParams['viewedon'];
}
return customFilterParams
}
/**
* all filters available to enable
*/
const filters = ref({
keywords: {
id: 'keywords',
label: 'Keyword (any)',
hint: 'Any of the given keywords',
enabled: false,
default: [],
is: ModelSelect,
model: 'Keyword',
modelValue: useRouteQuery('keywords', [], {transform: toNumberArray}),
mode: 'tags',
object: false,
searchOnLoad: true
},
keywordsAnd: {
id: 'keywordsAnd',
label: 'Keyword (all)',
hint: 'All of the given keywords',
enabled: false,
default: [],
is: ModelSelect,
model: 'Keyword',
modelValue: useRouteQuery('keywordsAnd', [], {transform: toNumberArray}),
mode: 'tags',
object: false,
searchOnLoad: true
},
keywordsOrNot: {
id: 'keywordsOrNot',
label: 'Keyword exclude (any)',
hint: 'Exclude recipes with any of the given keywords',
enabled: false,
default: [],
is: ModelSelect,
model: 'Keyword',
modelValue: useRouteQuery('keywordsOrNot', [], {transform: toNumberArray}),
mode: 'tags',
object: false,
searchOnLoad: true
},
keywordsAndNot: {
id: 'keywordsAndNot',
label: 'Keyword exclude (all)',
hint: 'Exclude recipes with all of the given keywords',
enabled: false,
default: [],
is: ModelSelect,
model: 'Keyword',
modelValue: useRouteQuery('keywordsAndNot', [], {transform: toNumberArray}),
mode: 'tags',
object: false,
searchOnLoad: true
},
foods: {
id: 'foods',
label: 'Foods (any)',
hint: 'Any of the given foods',
enabled: false,
default: [],
is: ModelSelect,
model: 'Food',
modelValue: useRouteQuery('foods', [], {transform: toNumberArray}),
mode: 'tags',
object: false,
searchOnLoad: true
},
foodsAnd: {
id: 'foodsAnd',
label: 'Food (all)',
hint: 'All of the given foods',
enabled: false,
default: [],
is: ModelSelect,
model: 'Food',
modelValue: useRouteQuery('foodsAnd', [], {transform: toNumberArray}),
mode: 'tags',
object: false,
searchOnLoad: true
},
foodsOrNot: {
id: 'foodsOrNot',
label: 'Food exclude (any)',
hint: 'Exclude recipes with any of the given foods',
enabled: false,
default: [],
is: ModelSelect,
model: 'Food',
modelValue: useRouteQuery('foodsOrNot', [], {transform: toNumberArray}),
mode: 'tags',
object: false,
searchOnLoad: true
},
foodsAndNot: {
id: 'foodsAndNot',
label: 'Food exclude (all)',
hint: 'Exclude recipes with all of the given foods',
enabled: false,
default: [],
is: ModelSelect,
model: 'Food',
modelValue: useRouteQuery('foodsAndNot', [], {transform: toNumberArray}),
mode: 'tags',
object: false,
searchOnLoad: true
},
books: {
id: 'books',
label: 'Book (any)',
hint: 'Recipes that are in any of the given books',
enabled: false,
default: [],
is: ModelSelect,
model: 'RecipeBook',
modelValue: useRouteQuery('books', [], {transform: toNumberArray}),
mode: 'tags',
object: false,
searchOnLoad: true
},
booksAnd: {
id: 'booksAnd',
label: 'Book (all)',
hint: 'Recipes that are in all of the given books',
enabled: false,
default: [],
is: ModelSelect,
model: 'RecipeBook',
modelValue: useRouteQuery('booksAnd', [], {transform: toNumberArray}),
mode: 'tags',
object: false,
searchOnLoad: true
},
booksOrNot: {
id: 'booksOrNot',
label: 'Book exclude (any)',
hint: 'Exclude recipes with any of the given books',
enabled: false,
default: [],
is: ModelSelect,
model: 'RecipeBook',
modelValue: useRouteQuery('booksOrNot', [], {transform: toNumberArray}),
mode: 'tags',
object: false,
searchOnLoad: true
},
booksAndNot: {
id: 'booksAndNot',
label: 'Book exclude (all)',
hint: 'Exclude recipes with all of the given books',
enabled: false,
default: [],
is: ModelSelect,
model: 'RecipeBook',
modelValue: useRouteQuery('booksAndNot', [], {transform: toNumberArray}),
mode: 'tags',
object: false,
searchOnLoad: true
},
createdby: {
id: 'createdby',
label: 'Created By',
hint: 'Recipes created by the selected user',
enabled: false,
default: undefined,
is: ModelSelect,
model: 'User',
modelValue: useRouteQuery('createdby', undefined, {transform: Number}),
mode: 'single',
object: false,
searchOnLoad: true
},
units: {
id: 'units',
label: 'Unit (any)',
hint: 'Recipes that contain any of the given units',
enabled: false,
default: [],
is: ModelSelect,
model: 'Unit',
modelValue: useRouteQuery('units', [], {transform: toNumberArray}),
mode: 'tags',
object: false,
searchOnLoad: true
},
internal: {
id: 'internal',
label: 'Hide External',
hint: 'Hide external recipes',
enabled: false,
default: "false",
is: VSelect,
items: [{value: "true", title: 'Yes'}, {value: "false", title: 'No'}],
modelValue: useRouteQuery('internal', "false"),
},
rating: {
id: 'rating',
label: 'Rating (exact)',
hint: 'Recipes with the exact rating',
enabled: false,
default: undefined,
is: RatingField,
modelValue: useRouteQuery('rating', undefined, {transform: Number}),
},
ratingGte: {
id: 'ratingGte',
label: 'Rating (>=)',
hint: 'Recipes with the given or a greater rating',
enabled: false,
default: undefined,
is: RatingField,
modelValue: useRouteQuery('ratingGte', undefined, {transform: Number}),
},
ratingLte: {
id: 'ratingLte',
label: 'Rating (<=)',
hint: 'Recipes with the given or a smaller rating',
enabled: false,
default: undefined,
is: RatingField,
modelValue: useRouteQuery('ratingLte', undefined, {transform: Number}),
},
timescookedGte: {
id: 'timescookedGte',
label: 'Times Cooked (>=)',
hint: 'Recipes that were cooked at least X times',
enabled: false,
default: undefined,
is: VNumberInput,
modelValue: useRouteQuery('timescookedGte', undefined, {transform: Number}),
},
timescookedLte: {
id: 'timescookedLte',
label: 'Times Cooked (<=)',
hint: 'Recipes that were cooked at most X times',
enabled: false,
default: undefined,
is: VNumberInput,
modelValue: useRouteQuery('timescookedLte', undefined, {transform: Number}),
},
makenow: {
id: 'makenow',
label: 'Foods on Hand',
hint: 'Only recipes were all foods (or its substitutes) are marked as on hand',
enabled: false,
default: "false",
is: VSelect,
items: [{value: "true", title: 'Yes'}, {value: "false", title: 'No'}],
modelValue: useRouteQuery('makenow', "false"),
},
cookedonGte: {
id: 'cookedonGte',
label: 'Cooked after',
hint: 'Only recipes that were cooked on or after the given date.',
enabled: false,
default: null,
is: VDateInput,
modelValue: useRouteQuery('cookedonGte', null, {transform: routeQueryDateTransformer}),
},
cookedonLte: {
id: 'cookedonLte',
label: 'Cooked before',
hint: 'Only recipes that were cooked on or before the given date.',
enabled: false,
default: null,
is: VDateInput,
modelValue: useRouteQuery('cookedonLte', null, {transform: routeQueryDateTransformer}),
},
viewedonGte: {
id: 'viewedonGte',
label: 'Viewed after',
hint: 'Only recipes that were viewed on or after the given date.',
enabled: false,
default: null,
is: VDateInput,
modelValue: useRouteQuery('viewedonGte', null, {transform: routeQueryDateTransformer}),
},
viewedonLte: {
id: 'viewedonLte',
label: 'Viewed before',
hint: 'Only recipes that were viewed on or before the given date.',
enabled: false,
default: null,
is: VDateInput,
modelValue: useRouteQuery('viewedonLte', null, {transform: routeQueryDateTransformer}),
},
createdon: {
id: 'createdon',
label: 'Created on',
hint: 'Only recipes that were created on the given date.',
enabled: false,
default: null,
is: VDateInput,
modelValue: useRouteQuery('createdon', null, {transform: routeQueryDateTransformer}),
},
createdonGte: {
id: 'createdonGte',
label: 'Created on/after',
hint: 'Only recipes that were created on or after the given date.',
enabled: false,
default: null,
is: VDateInput,
modelValue: useRouteQuery('createdonGte', null, {transform: routeQueryDateTransformer}),
},
createdonLte: {
id: 'createdonLte',
label: 'Created on/before',
hint: 'Only recipes that were created on or before the given date.',
enabled: false,
default: null,
is: VDateInput,
modelValue: useRouteQuery('createdonLte', null, {transform: routeQueryDateTransformer}),
},
updatedon: {
id: 'updatedon',
label: 'Updated on',
hint: 'Only recipes that were updated on the given date.',
enabled: false,
default: null,
is: VDateInput,
modelValue: useRouteQuery('updatedon', null, {transform: routeQueryDateTransformer}),
},
updatedonGte: {
id: 'updatedonGte',
label: 'Updated on/after',
hint: 'Only recipes that were updated on or after the given date.',
enabled: false,
default: null,
is: VDateInput,
modelValue: useRouteQuery('updatedonGte', null, {transform: routeQueryDateTransformer}),
},
updatedonLte: {
id: 'updatedonLte',
label: 'Updated on/before',
hint: 'Only recipes that were updated on or before the given date.',
enabled: false,
default: null,
is: VDateInput,
modelValue: useRouteQuery('updatedonLte', null, {transform: routeQueryDateTransformer}),
},
})
</script>
<style scoped>
</style>