export recipes from saved filter

This commit is contained in:
smilerz 2022-02-22 15:18:39 -06:00
parent c1605454dd
commit a7d66fa850
No known key found for this signature in database
GPG Key ID: 39444C7606D47126
5 changed files with 333 additions and 513 deletions

View File

@ -179,6 +179,7 @@ class ImportForm(ImportExportBase):
class ExportForm(ImportExportBase): class ExportForm(ImportExportBase):
recipes = forms.ModelMultipleChoiceField(widget=MultiSelectWidget, queryset=Recipe.objects.none(), required=False) recipes = forms.ModelMultipleChoiceField(widget=MultiSelectWidget, queryset=Recipe.objects.none(), required=False)
all = forms.BooleanField(required=False) all = forms.BooleanField(required=False)
filter = forms.IntegerField(required=False)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
space = kwargs.pop('space') space = kwargs.pop('space')

View File

@ -13,8 +13,8 @@ from cookbook.filters import RecipeFilter
from cookbook.helper.HelperFunctions import Round, str2bool from cookbook.helper.HelperFunctions import Round, str2bool
from cookbook.helper.permission_helper import has_group_permission from cookbook.helper.permission_helper import has_group_permission
from cookbook.managers import DICTIONARY from cookbook.managers import DICTIONARY
from cookbook.models import (CookLog, CustomFilter, Food, Keyword, Recipe, SearchFields, from cookbook.models import (CookLog, CustomFilter, Food, Keyword, Recipe, RecipeBook, SearchFields,
SearchPreference, ViewLog, RecipeBook) SearchPreference, ViewLog)
from recipes import settings from recipes import settings
@ -28,7 +28,7 @@ class RecipeSearch():
self._queryset = None self._queryset = None
if f := params.get('filter', None): if f := params.get('filter', None):
custom_filter = CustomFilter.objects.filter(id=f, space=self._request.space).filter(Q(created_by=self._request.user) | custom_filter = CustomFilter.objects.filter(id=f, space=self._request.space).filter(Q(created_by=self._request.user) |
Q(shared=self._request.user) | Q(recipebook__shared=self._request.user)).first() Q(shared=self._request.user) | Q(recipebook__shared=self._request.user)).first()
if custom_filter: if custom_filter:
self._params = {**json.loads(custom_filter.search)} self._params = {**json.loads(custom_filter.search)}
self._original_params = {**(params or {})} self._original_params = {**(params or {})}
@ -40,7 +40,7 @@ class RecipeSearch():
self._search_prefs = request.user.searchpreference self._search_prefs = request.user.searchpreference
else: else:
self._search_prefs = SearchPreference() self._search_prefs = SearchPreference()
self._string = params.get('query').strip() if params.get('query', None) else None self._string = self._params.get('query').strip() if self._params.get('query', None) else None
self._rating = self._params.get('rating', None) self._rating = self._params.get('rating', None)
self._keywords = { self._keywords = {
'or': self._params.get('keywords_or', None) or self._params.get('keywords', None), 'or': self._params.get('keywords_or', None) or self._params.get('keywords', None),
@ -205,7 +205,7 @@ class RecipeSearch():
else: else:
self._queryset = self._queryset.annotate(simularity=Coalesce(Subquery(simularity), 0.0)) self._queryset = self._queryset.annotate(simularity=Coalesce(Subquery(simularity), 0.0))
if self._sort_includes('score') and self._fulltext_include and self._fuzzy_match is not None: if self._sort_includes('score') and self._fulltext_include and self._fuzzy_match is not None:
self._queryset = self._queryset.annotate(score=Sum(F('rank')+F('simularity'))) self._queryset = self._queryset.annotate(score=F('rank')+F('simularity'))
else: else:
query_filter = Q() query_filter = Q()
for f in [x + '__unaccent__iexact' if x in self._unaccent_include else x + '__iexact' for x in SearchFields.objects.all().values_list('field', flat=True)]: for f in [x + '__unaccent__iexact' if x in self._unaccent_include else x + '__iexact' for x in SearchFields.objects.all().values_list('field', flat=True)]:

View File

@ -11,6 +11,7 @@ from django.utils.translation import gettext as _
from cookbook.forms import ExportForm, ImportExportBase, ImportForm from cookbook.forms import ExportForm, ImportExportBase, ImportForm
from cookbook.helper.permission_helper import group_required from cookbook.helper.permission_helper import group_required
from cookbook.helper.recipe_search import RecipeSearch
from cookbook.integration.cheftap import ChefTap from cookbook.integration.cheftap import ChefTap
from cookbook.integration.chowdown import Chowdown from cookbook.integration.chowdown import Chowdown
from cookbook.integration.cookbookapp import CookBookApp from cookbook.integration.cookbookapp import CookBookApp
@ -123,6 +124,9 @@ def export_recipe(request):
recipes = form.cleaned_data['recipes'] recipes = form.cleaned_data['recipes']
if form.cleaned_data['all']: if form.cleaned_data['all']:
recipes = Recipe.objects.filter(space=request.space, internal=True).all() recipes = Recipe.objects.filter(space=request.space, internal=True).all()
elif filter := form.cleaned_data['filter']:
search = RecipeSearch(request, filter=filter)
recipes = search.get_queryset(Recipe.objects.filter(space=request.space, internal=True))
integration = get_integration(request, form.cleaned_data['type']) integration = get_integration(request, form.cleaned_data['type'])

View File

@ -1,174 +1,174 @@
<template> <template>
<div id="app"> <div id="app">
<h2>{{ $t("Export") }}</h2>
<div class="row">
<div class="col col-md-12">
<br />
<!-- TODO get option dynamicaly -->
<select class="form-control" v-model="recipe_app">
<option value="DEFAULT">Default</option>
<option value="SAFFRON">Saffron</option>
<option value="RECIPESAGE">Recipe Sage</option>
<option value="PDF">PDF (experimental)</option>
</select>
<h2>{{ $t('Export') }}</h2> <br />
<div class="row"> <b-form-checkbox v-model="export_all" @change="disabled_multiselect = $event" name="check-button" switch style="margin-top: 1vh">
<div class="col col-md-12"> {{ $t("All recipes") }}
</b-form-checkbox>
<br/> <multiselect
<!-- TODO get option dynamicaly --> :searchable="true"
<select class="form-control" v-model="recipe_app"> :disabled="disabled_multiselect"
<option value="DEFAULT">Default</option> v-model="recipe_list"
<option value="SAFFRON">Saffron</option> :options="recipes"
<option value="RECIPESAGE">Recipe Sage</option> :close-on-select="false"
<option value="PDF">PDF (experimental)</option> :clear-on-select="true"
</select> :hide-selected="true"
:preserve-search="true"
placeholder="Select Recipes"
:taggable="false"
label="name"
track-by="id"
id="id_recipes"
:multiple="true"
:loading="recipes_loading"
@search-change="searchRecipes"
>
</multiselect>
<generic-multiselect
@change="filter = $event.val"
:model="Models.CUSTOM_FILTER"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
:placeholder="$t('Custom Filter')"
:multiple="false"
:limit="50"
/>
<br/> <br />
<b-form-checkbox v-model="export_all" @change="disabled_multiselect=$event" name="check-button" switch style="margin-top: 1vh"> <button @click="exportRecipe()" class="btn btn-primary shadow-none"><i class="fas fa-file-export"></i> {{ $t("Export") }}</button>
{{ $t('All recipes') }} </div>
</b-form-checkbox>
<multiselect
:searchable="true"
:disabled="disabled_multiselect"
v-model="recipe_list"
:options="recipes"
:close-on-select="false"
:clear-on-select="true"
:hide-selected="true"
:preserve-search="true"
placeholder="Select Recipes"
:taggable="false"
label="name"
track-by="id"
id="id_recipes"
:multiple="true"
:loading="recipes_loading"
@search-change="searchRecipes">
</multiselect>
<br/>
<button @click="exportRecipe()" class="btn btn-primary shadow-none"><i class="fas fa-file-export"></i> {{ $t('Export') }}
</button>
</div> </div>
</div> </div>
</div>
</template> </template>
<script> <script>
import Vue from 'vue' import Vue from "vue"
import {BootstrapVue} from 'bootstrap-vue' import { BootstrapVue } from "bootstrap-vue"
import 'bootstrap-vue/dist/bootstrap-vue.css' import "bootstrap-vue/dist/bootstrap-vue.css"
import LoadingSpinner from "@/components/LoadingSpinner"
import LoadingSpinner from "@/components/LoadingSpinner"; import { StandardToasts, makeToast, resolveDjangoUrl, ApiMixin } from "@/utils/utils"
import Multiselect from "vue-multiselect"
import {StandardToasts, makeToast, resolveDjangoUrl} from "@/utils/utils"; import GenericMultiselect from "@/components/GenericMultiselect"
import Multiselect from "vue-multiselect"; import { ApiApiFactory } from "@/utils/openapi/api.ts"
import {ApiApiFactory} from "@/utils/openapi/api.ts"; import axios from "axios"
import axios from "axios";
Vue.use(BootstrapVue) Vue.use(BootstrapVue)
export default { export default {
name: 'ExportView', name: "ExportView",
/*mixins: [ /*mixins: [
ResolveUrlMixin, ResolveUrlMixin,
ToastMixin, ToastMixin,
],*/ ],*/
components: {Multiselect}, components: { Multiselect, GenericMultiselect },
data() { mixins: [ApiMixin],
return { data() {
export_id: window.EXPORT_ID, return {
loading: false, export_id: window.EXPORT_ID,
disabled_multiselect: false, loading: false,
disabled_multiselect: false,
recipe_app: 'DEFAULT', recipe_app: "DEFAULT",
recipe_list: [], recipe_list: [],
recipes_loading: false, recipes_loading: false,
recipes: [], recipes: [],
export_all: false, export_all: false,
} filter: undefined,
}, }
mounted() {
if(this.export_id)
this.insertRequested()
else
this.searchRecipes('')
},
methods: {
insertRequested: function(){
let apiFactory = new ApiApiFactory()
this.recipes_loading = true
apiFactory.retrieveRecipe(this.export_id).then((response) => {
this.recipes_loading = false
this.recipe_list.push(response.data)
}).catch((err) => {
console.log(err)
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
}).then(e => this.searchRecipes(''))
}, },
mounted() {
searchRecipes: function (query) { if (this.export_id) this.insertRequested()
else this.searchRecipes("")
let apiFactory = new ApiApiFactory()
this.recipes_loading = true
let maxResultLenght = 1000
apiFactory.listRecipes(query, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, 1, maxResultLenght).then((response) => {
this.recipes = response.data.results;
this.recipes_loading = false
}).catch((err) => {
console.log(err)
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
})
}, },
methods: {
insertRequested: function () {
let apiFactory = new ApiApiFactory()
exportRecipe: function () { this.recipes_loading = true
if (this.recipe_list.length < 1 && this.export_all == false) { apiFactory
makeToast(this.$t("Error"), this.$t("Select at least one recipe"), "danger") .retrieveRecipe(this.export_id)
return; .then((response) => {
} this.recipes_loading = false
this.recipe_list.push(response.data)
})
.catch((err) => {
console.log(err)
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
})
.then((e) => this.searchRecipes(""))
},
this.error = undefined searchRecipes: function (query) {
this.loading = true let apiFactory = new ApiApiFactory()
let formData = new FormData();
formData.append('type', this.recipe_app);
formData.append('all', this.export_all)
for (var i = 0; i < this.recipe_list.length; i++) { this.recipes_loading = true
formData.append('recipes', this.recipe_list[i].id);
}
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'; let maxResultLenght = 1000
axios.post(resolveDjangoUrl('view_export',), formData).then((response) => { apiFactory
if (response.data['error'] !== undefined){ .listRecipes(query, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, 1, maxResultLenght)
makeToast(this.$t("Error"), response.data['error'],"warning") .then((response) => {
}else{ this.recipes = response.data.results
window.location.href = resolveDjangoUrl('view_export_response', response.data['export_id']) this.recipes_loading = false
} })
.catch((err) => {
console.log(err)
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
})
},
}).catch((err) => { exportRecipe: function () {
this.error = err.data if (this.recipe_list.length < 1 && this.export_all == false && this.filter === undefined) {
this.loading = false makeToast(this.$t("Error"), this.$t("Select at least one recipe"), "danger")
console.log(err) return
makeToast(this.$t("Error"), this.$t("There was an error loading a resource!"), "warning") }
})
this.error = undefined
this.loading = true
let formData = new FormData()
formData.append("type", this.recipe_app)
formData.append("all", this.export_all)
formData.append("filter", this.filter?.id)
for (var i = 0; i < this.recipe_list.length; i++) {
formData.append("recipes", this.recipe_list[i].id)
}
axios.defaults.headers.post["Content-Type"] = "application/x-www-form-urlencoded"
axios
.post(resolveDjangoUrl("view_export"), formData)
.then((response) => {
if (response.data["error"] !== undefined) {
makeToast(this.$t("Error"), response.data["error"], "warning")
} else {
window.location.href = resolveDjangoUrl("view_export_response", response.data["export_id"])
}
})
.catch((err) => {
this.error = err.data
this.loading = false
console.log(err)
makeToast(this.$t("Error"), this.$t("There was an error loading a resource!"), "warning")
})
},
}, },
}
} }
</script> </script>
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style> <style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
<style> <style></style>
</style>

View File

@ -1,6 +1,6 @@
<template> <template>
<div id="app" style="margin-bottom: 4vh"> <div id="app" style="margin-bottom: 4vh">
<RecipeSwitcher ref="ref_recipe_switcher"/> <RecipeSwitcher ref="ref_recipe_switcher" />
<div class="row"> <div class="row">
<div class="col-12 col-xl-8 col-lg-10 offset-xl-2 offset-lg-1"> <div class="col-12 col-xl-8 col-lg-10 offset-xl-2 offset-lg-1">
<div class="row"> <div class="row">
@ -8,21 +8,15 @@
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-12 col-lg-10 col-xl-8 mt-3 mb-3"> <div class="col-12 col-lg-10 col-xl-8 mt-3 mb-3">
<b-input-group> <b-input-group>
<b-input <b-input class="form-control form-control-lg form-control-borderless form-control-search" v-model="search.search_input" v-bind:placeholder="$t('Search')"></b-input>
class="form-control form-control-lg form-control-borderless form-control-search"
v-model="search.search_input" v-bind:placeholder="$t('Search')"></b-input>
<b-input-group-append> <b-input-group-append>
<b-button v-b-tooltip.hover :title="$t('show_sql')" @click="showSQL()" <b-button v-b-tooltip.hover :title="$t('show_sql')" @click="showSQL()" v-if="debug && ui.sql_debug">
v-if="debug && ui.sql_debug">
<i class="fas fa-bug" style="font-size: 1.5em"></i> <i class="fas fa-bug" style="font-size: 1.5em"></i>
</b-button> </b-button>
<b-button variant="light" v-b-tooltip.hover :title="$t('Random Recipes')" <b-button variant="light" v-b-tooltip.hover :title="$t('Random Recipes')" @click="openRandom()">
@click="openRandom()">
<i class="fas fa-dice-five" style="font-size: 1.5em"></i> <i class="fas fa-dice-five" style="font-size: 1.5em"></i>
</b-button> </b-button>
<b-button v-b-toggle.collapse_advanced_search v-b-tooltip.hover <b-button v-b-toggle.collapse_advanced_search v-b-tooltip.hover :title="$t('Advanced Settings')" v-bind:variant="searchFiltered(true) ? 'danger' : 'primary'">
:title="$t('Advanced Settings')"
v-bind:variant="searchFiltered(true) ? 'danger' : 'primary'">
<!-- TODO consider changing this icon to a filter --> <!-- TODO consider changing this icon to a filter -->
<i class="fas fa-caret-down" v-if="!search.advanced_search_visible"></i> <i class="fas fa-caret-down" v-if="!search.advanced_search_visible"></i>
<i class="fas fa-caret-up" v-if="search.advanced_search_visible"></i> <i class="fas fa-caret-up" v-if="search.advanced_search_visible"></i>
@ -32,18 +26,15 @@
</div> </div>
</div> </div>
<b-collapse id="collapse_advanced_search" class="mt-2 shadow-sm" <b-collapse id="collapse_advanced_search" class="mt-2 shadow-sm" v-model="search.advanced_search_visible">
v-model="search.advanced_search_visible">
<div class="card"> <div class="card">
<div class="card-body p-4"> <div class="card-body p-4">
<div class="row"> <div class="row">
<div class="col-md-3"> <div class="col-md-3">
<a class="btn btn-primary btn-block text-uppercase" <a class="btn btn-primary btn-block text-uppercase" :href="resolveDjangoUrl('new_recipe')">{{ $t("New_Recipe") }}</a>
:href="resolveDjangoUrl('new_recipe')">{{ $t("New_Recipe") }}</a>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<a class="btn btn-primary btn-block text-uppercase" <a class="btn btn-primary btn-block text-uppercase" :href="resolveDjangoUrl('data_import_url')">{{ $t("Import") }}</a>
:href="resolveDjangoUrl('data_import_url')">{{ $t("Import") }}</a>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<button <button
@ -62,186 +53,99 @@
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<button id="id_settings_button" <button id="id_settings_button" class="btn btn-primary btn-block text-uppercase"><i class="fas fa-cog fa-lg m-1"></i></button>
class="btn btn-primary btn-block text-uppercase"><i
class="fas fa-cog fa-lg m-1"></i></button>
</div> </div>
</div> </div>
<b-popover target="id_settings_button" triggers="click" placement="bottom"> <b-popover target="id_settings_button" triggers="click" placement="bottom">
<b-tabs content-class="mt-1 text-nowrap" small> <b-tabs content-class="mt-1 text-nowrap" small>
<b-tab :title="$t('Settings')" active :title-link-class="['mx-0']"> <b-tab :title="$t('Settings')" active :title-link-class="['mx-0']">
<b-form-group v-bind:label="$t('Recently_Viewed')" <b-form-group v-bind:label="$t('Recently_Viewed')" label-for="popover-input-1" label-cols="6" class="mb-1">
label-for="popover-input-1" label-cols="6" class="mb-1"> <b-form-input type="number" v-model="ui.recently_viewed" id="popover-input-1" size="sm"></b-form-input>
<b-form-input type="number" v-model="ui.recently_viewed"
id="popover-input-1" size="sm"></b-form-input>
</b-form-group> </b-form-group>
<b-form-group v-bind:label="$t('Recipes_per_page')" <b-form-group v-bind:label="$t('Recipes_per_page')" label-for="popover-input-page-count" label-cols="6" class="mb-1">
label-for="popover-input-page-count" label-cols="6" <b-form-input type="number" v-model="ui.page_size" id="popover-input-page-count" size="sm"></b-form-input>
class="mb-1">
<b-form-input type="number" v-model="ui.page_size"
id="popover-input-page-count"
size="sm"></b-form-input>
</b-form-group> </b-form-group>
<b-form-group v-bind:label="$t('Meal_Plan')" label-for="popover-input-2" <b-form-group v-bind:label="$t('Meal_Plan')" label-for="popover-input-2" label-cols="6" class="mb-1">
label-cols="6" class="mb-1"> <b-form-checkbox switch v-model="ui.show_meal_plan" id="popover-input-2" size="sm"></b-form-checkbox>
<b-form-checkbox switch v-model="ui.show_meal_plan"
id="popover-input-2" size="sm"></b-form-checkbox>
</b-form-group> </b-form-group>
<b-form-group v-if="ui.show_meal_plan" <b-form-group v-if="ui.show_meal_plan" v-bind:label="$t('Meal_Plan_Days')" label-for="popover-input-5" label-cols="6" class="mb-1">
v-bind:label="$t('Meal_Plan_Days')" <b-form-input type="number" v-model="ui.meal_plan_days" id="popover-input-5" size="sm"></b-form-input>
label-for="popover-input-5" label-cols="6" class="mb-1">
<b-form-input type="number" v-model="ui.meal_plan_days"
id="popover-input-5" size="sm"></b-form-input>
</b-form-group> </b-form-group>
<b-form-group v-bind:label="$t('Sort_by_new')" <b-form-group v-bind:label="$t('Sort_by_new')" label-for="popover-input-3" label-cols="6" class="mb-1">
label-for="popover-input-3" label-cols="6" class="mb-1"> <b-form-checkbox switch v-model="ui.sort_by_new" id="popover-input-3" size="sm"></b-form-checkbox>
<b-form-checkbox switch v-model="ui.sort_by_new"
id="popover-input-3" size="sm"></b-form-checkbox>
</b-form-group> </b-form-group>
<div class="row" style="margin-top: 1vh"> <div class="row" style="margin-top: 1vh">
<div class="col-12"> <div class="col-12">
<a :href="resolveDjangoUrl('view_settings') + '#search'">{{ <a :href="resolveDjangoUrl('view_settings') + '#search'">{{ $t("Search Settings") }}</a>
$t("Search Settings")
}}</a>
</div> </div>
</div> </div>
</b-tab> </b-tab>
<b-tab :title="$t('fields')" :title-link-class="['mx-0']"> <b-tab :title="$t('fields')" :title-link-class="['mx-0']">
<b-form-group v-bind:label="$t('show_keywords')" <b-form-group v-bind:label="$t('show_keywords')" label-for="popover-show_keywords" label-cols="8" class="mb-1">
label-for="popover-show_keywords" label-cols="8" <b-form-checkbox switch v-model="ui.show_keywords" id="popover-show_keywords" size="sm"></b-form-checkbox>
class="mb-1">
<b-form-checkbox switch v-model="ui.show_keywords"
id="popover-show_keywords"
size="sm"></b-form-checkbox>
</b-form-group> </b-form-group>
<b-form-group v-bind:label="$t('show_foods')" <b-form-group v-bind:label="$t('show_foods')" label-for="popover-show_foods" label-cols="8" class="mb-1">
label-for="popover-show_foods" label-cols="8" <b-form-checkbox switch v-model="ui.show_foods" id="popover-show_foods" size="sm"></b-form-checkbox>
class="mb-1">
<b-form-checkbox switch v-model="ui.show_foods"
id="popover-show_foods"
size="sm"></b-form-checkbox>
</b-form-group> </b-form-group>
<b-form-group v-bind:label="$t('show_books')" <b-form-group v-bind:label="$t('show_books')" label-for="popover-input-show_books" label-cols="8" class="mb-1">
label-for="popover-input-show_books" label-cols="8" <b-form-checkbox switch v-model="ui.show_books" id="popover-input-show_books" size="sm"></b-form-checkbox>
class="mb-1">
<b-form-checkbox switch v-model="ui.show_books"
id="popover-input-show_books"
size="sm"></b-form-checkbox>
</b-form-group> </b-form-group>
<b-form-group v-bind:label="$t('show_rating')" <b-form-group v-bind:label="$t('show_rating')" label-for="popover-show_rating" label-cols="8" class="mb-1">
label-for="popover-show_rating" label-cols="8" <b-form-checkbox switch v-model="ui.show_rating" id="popover-show_rating" size="sm"></b-form-checkbox>
class="mb-1">
<b-form-checkbox switch v-model="ui.show_rating"
id="popover-show_rating"
size="sm"></b-form-checkbox>
</b-form-group> </b-form-group>
<b-form-group v-bind:label="$t('show_units')" <b-form-group v-bind:label="$t('show_units')" label-for="popover-show_units" label-cols="8" class="mb-1">
label-for="popover-show_units" label-cols="8" <b-form-checkbox switch v-model="ui.show_units" id="popover-show_units" size="sm"></b-form-checkbox>
class="mb-1">
<b-form-checkbox switch v-model="ui.show_units"
id="popover-show_units"
size="sm"></b-form-checkbox>
</b-form-group> </b-form-group>
<b-form-group v-bind:label="$t('show_filters')" <b-form-group v-bind:label="$t('show_filters')" label-for="popover-show_filters" label-cols="8" class="mb-1">
label-for="popover-show_filters" label-cols="8" <b-form-checkbox switch v-model="ui.show_filters" id="popover-show_filters" size="sm"></b-form-checkbox>
class="mb-1">
<b-form-checkbox switch v-model="ui.show_filters"
id="popover-show_filters"
size="sm"></b-form-checkbox>
</b-form-group> </b-form-group>
<b-form-group v-bind:label="$t('show_sortby')" <b-form-group v-bind:label="$t('show_sortby')" label-for="popover-show_sortby" label-cols="8" class="mb-1">
label-for="popover-show_sortby" label-cols="8" <b-form-checkbox switch v-model="ui.show_sortby" id="popover-show_sortby" size="sm"></b-form-checkbox>
class="mb-1">
<b-form-checkbox switch v-model="ui.show_sortby"
id="popover-show_sortby"
size="sm"></b-form-checkbox>
</b-form-group> </b-form-group>
<b-form-group v-bind:label="$t('times_cooked')" <b-form-group v-bind:label="$t('times_cooked')" label-for="popover-show_timescooked" label-cols="8" class="mb-1">
label-for="popover-show_timescooked" label-cols="8" <b-form-checkbox switch v-model="ui.show_timescooked" id="popover-show_cooked" size="sm"></b-form-checkbox>
class="mb-1">
<b-form-checkbox switch v-model="ui.show_timescooked"
id="popover-show_cooked"
size="sm"></b-form-checkbox>
</b-form-group> </b-form-group>
<b-form-group v-bind:label="$t('make_now')" <b-form-group v-bind:label="$t('make_now')" label-for="popover-show_makenow" label-cols="8" class="mb-1">
label-for="popover-show_makenow" label-cols="8" <b-form-checkbox switch v-model="ui.show_makenow" id="popover-show_makenow" size="sm"></b-form-checkbox>
class="mb-1">
<b-form-checkbox switch v-model="ui.show_makenow"
id="popover-show_makenow"
size="sm"></b-form-checkbox>
</b-form-group> </b-form-group>
<b-form-group v-bind:label="$t('last_cooked')" <b-form-group v-bind:label="$t('last_cooked')" label-for="popover-show_cookedon" label-cols="8" class="mb-1">
label-for="popover-show_cookedon" label-cols="8" <b-form-checkbox switch v-model="ui.show_cookedon" id="popover-show_cookedon" size="sm"></b-form-checkbox>
class="mb-1">
<b-form-checkbox switch v-model="ui.show_cookedon"
id="popover-show_cookedon"
size="sm"></b-form-checkbox>
</b-form-group> </b-form-group>
<b-form-group v-bind:label="$t('last_viewed')" <b-form-group v-bind:label="$t('last_viewed')" label-for="popover-show_viewedon" label-cols="8" class="mb-1">
label-for="popover-show_viewedon" label-cols="8" <b-form-checkbox switch v-model="ui.show_viewedon" id="popover-show_viewedon" size="sm"></b-form-checkbox>
class="mb-1">
<b-form-checkbox switch v-model="ui.show_viewedon"
id="popover-show_viewedon"
size="sm"></b-form-checkbox>
</b-form-group> </b-form-group>
<b-form-group v-bind:label="$t('created_on')" <b-form-group v-bind:label="$t('created_on')" label-for="popover-show_createdon" label-cols="8" class="mb-1">
label-for="popover-show_createdon" label-cols="8" <b-form-checkbox switch v-model="ui.show_createdon" id="popover-show_createdon" size="sm"></b-form-checkbox>
class="mb-1">
<b-form-checkbox switch v-model="ui.show_createdon"
id="popover-show_createdon"
size="sm"></b-form-checkbox>
</b-form-group> </b-form-group>
<b-form-group v-bind:label="$t('updatedon')" <b-form-group v-bind:label="$t('updatedon')" label-for="popover-show_updatedon" label-cols="8" class="mb-1">
label-for="popover-show_updatedon" label-cols="8" <b-form-checkbox switch v-model="ui.show_updatedon" id="popover-show_updatedon" size="sm"></b-form-checkbox>
class="mb-1">
<b-form-checkbox switch v-model="ui.show_updatedon"
id="popover-show_updatedon"
size="sm"></b-form-checkbox>
</b-form-group> </b-form-group>
</b-tab> </b-tab>
<b-tab :title="$t('advanced')" :title-link-class="['mx-0']"> <b-tab :title="$t('advanced')" :title-link-class="['mx-0']">
<b-form-group v-bind:label="$t('remember_search')" <b-form-group v-bind:label="$t('remember_search')" label-for="popover-rem-search" label-cols="8" class="mb-1">
label-for="popover-rem-search" label-cols="8" <b-form-checkbox switch v-model="ui.remember_search" id="popover-rem-search" size="sm"></b-form-checkbox>
class="mb-1">
<b-form-checkbox switch v-model="ui.remember_search"
id="popover-rem-search"
size="sm"></b-form-checkbox>
</b-form-group> </b-form-group>
<b-form-group v-if="ui.remember_search" <b-form-group v-if="ui.remember_search" v-bind:label="$t('remember_hours')" label-for="popover-input-rem-hours" label-cols="8" class="mb-1">
v-bind:label="$t('remember_hours')" <b-form-input type="number" v-model="ui.remember_hours" id="popover-rem-hours" size="sm"></b-form-input>
label-for="popover-input-rem-hours" label-cols="8"
class="mb-1">
<b-form-input type="number" v-model="ui.remember_hours"
id="popover-rem-hours" size="sm"></b-form-input>
</b-form-group> </b-form-group>
<b-form-group v-bind:label="$t('tree_select')" <b-form-group v-bind:label="$t('tree_select')" label-for="popover-input-treeselect" label-cols="8" class="mb-1">
label-for="popover-input-treeselect" label-cols="8" <b-form-checkbox switch v-model="ui.tree_select" id="popover-input-treeselect" size="sm"></b-form-checkbox>
class="mb-1">
<b-form-checkbox switch v-model="ui.tree_select"
id="popover-input-treeselect"
size="sm"></b-form-checkbox>
</b-form-group> </b-form-group>
<b-form-group v-if="debug" v-bind:label="$t('sql_debug')" <b-form-group v-if="debug" v-bind:label="$t('sql_debug')" label-for="popover-input-sqldebug" label-cols="8" class="mb-1">
label-for="popover-input-sqldebug" label-cols="8" <b-form-checkbox switch v-model="ui.sql_debug" id="popover-input-sqldebug" size="sm"></b-form-checkbox>
class="mb-1">
<b-form-checkbox switch v-model="ui.sql_debug"
id="popover-input-sqldebug"
size="sm"></b-form-checkbox>
</b-form-group> </b-form-group>
</b-tab> </b-tab>
</b-tabs> </b-tabs>
<div class="row" style="margin-top: 1vh"> <div class="row" style="margin-top: 1vh">
<div class="col-12" style="text-align: right"> <div class="col-12" style="text-align: right">
<b-button size="sm" variant="secondary" style="margin-right: 8px" <b-button size="sm" variant="secondary" style="margin-right: 8px" @click="$root.$emit('bv::hide::popover')">{{ $t("Close") }} </b-button>
@click="$root.$emit('bv::hide::popover')">{{ $t("Close") }}
</b-button>
</div> </div>
</div> </div>
</b-popover> </b-popover>
@ -281,23 +185,18 @@
<h6 class="mb-0" v-if="ui.expert_mode && search.keywords_fields > 1"> <h6 class="mb-0" v-if="ui.expert_mode && search.keywords_fields > 1">
{{ $t("Keywords") }} {{ $t("Keywords") }}
</h6> </h6>
<span class="text-sm-left text-warning" <span class="text-sm-left text-warning" v-if="ui.expert_mode && search.keywords_fields > 1 && hasDuplicateFilter(search.search_keywords, search.keywords_fields)">{{
v-if="ui.expert_mode && search.keywords_fields > 1 && hasDuplicateFilter(search.search_keywords, search.keywords_fields)">{{ $t("warning_duplicate_filter")
$t("warning_duplicate_filter") }}</span>
}}</span>
<div class="row" v-if="ui.show_keywords"> <div class="row" v-if="ui.show_keywords">
<div class="col-12"> <div class="col-12">
<b-input-group class="mt-2" v-for="(k, a) in keywordFields" :key="a"> <b-input-group class="mt-2" v-for="(k, a) in keywordFields" :key="a">
<template #prepend v-if="ui.expert_mode"> <template #prepend v-if="ui.expert_mode">
<b-input-group-text style="width: 3em" <b-input-group-text style="width: 3em" @click="addField('keywords', k)">
@click="addField('keywords', k)"> <i class="fas fa-plus-circle text-primary" v-if="k == search.keywords_fields && k < 4" />
<i class="fas fa-plus-circle text-primary"
v-if="k == search.keywords_fields && k < 4"/>
</b-input-group-text> </b-input-group-text>
<b-input-group-text style="width: 3em" <b-input-group-text style="width: 3em" @click="removeField('keywords', k)">
@click="removeField('keywords', k)"> <i class="fas fa-minus-circle text-primary" v-if="k == search.keywords_fields && k > 1" />
<i class="fas fa-minus-circle text-primary"
v-if="k == search.keywords_fields && k > 1"/>
</b-input-group-text> </b-input-group-text>
</template> </template>
<treeselect <treeselect
@ -334,20 +233,14 @@
switch switch
style="width: 4em" style="width: 4em"
> >
<span class="text-uppercase" <span class="text-uppercase" v-if="search.search_keywords[a].operator">{{ $t("or") }}</span>
v-if="search.search_keywords[a].operator">{{
$t("or")
}}</span>
<span class="text-uppercase" v-else>{{ $t("and") }}</span> <span class="text-uppercase" v-else>{{ $t("and") }}</span>
</b-form-checkbox> </b-form-checkbox>
</b-input-group-text> </b-input-group-text>
</b-input-group-append> </b-input-group-append>
<b-input-group-append v-if="ui.expert_mode"> <b-input-group-append v-if="ui.expert_mode">
<b-input-group-text> <b-input-group-text>
<b-form-checkbox v-model="search.search_keywords[a].not" <b-form-checkbox v-model="search.search_keywords[a].not" name="check-button" @change="refreshData(false)" class="shadow-none">
name="check-button"
@change="refreshData(false)"
class="shadow-none">
<span class="text-uppercase">{{ $t("not") }}</span> <span class="text-uppercase">{{ $t("not") }}</span>
</b-form-checkbox> </b-form-checkbox>
</b-input-group-text> </b-input-group-text>
@ -360,23 +253,18 @@
<h6 class="mt-2 mb-0" v-if="ui.expert_mode && search.foods_fields > 1"> <h6 class="mt-2 mb-0" v-if="ui.expert_mode && search.foods_fields > 1">
{{ $t("Foods") }} {{ $t("Foods") }}
</h6> </h6>
<span class="text-sm-left text-warning" <span class="text-sm-left text-warning" v-if="ui.expert_mode && search.foods_fields > 1 && hasDuplicateFilter(search.search_foods, search.foods_fields)">{{
v-if="ui.expert_mode && search.foods_fields > 1 && hasDuplicateFilter(search.search_foods, search.foods_fields)">{{ $t("warning_duplicate_filter")
$t("warning_duplicate_filter") }}</span>
}}</span>
<div class="row" v-if="ui.show_foods"> <div class="row" v-if="ui.show_foods">
<div class="col-12"> <div class="col-12">
<b-input-group class="mt-2" v-for="(f, i) in foodFields" :key="i"> <b-input-group class="mt-2" v-for="(f, i) in foodFields" :key="i">
<template #prepend v-if="ui.expert_mode"> <template #prepend v-if="ui.expert_mode">
<b-input-group-text style="width: 3em" <b-input-group-text style="width: 3em" @click="addField('foods', f)">
@click="addField('foods', f)"> <i class="fas fa-plus-circle text-primary" v-if="f == search.foods_fields && f < 4" />
<i class="fas fa-plus-circle text-primary"
v-if="f == search.foods_fields && f < 4"/>
</b-input-group-text> </b-input-group-text>
<b-input-group-text style="width: 3em" <b-input-group-text style="width: 3em" @click="removeField('foods', f)">
@click="removeField('foods', f)"> <i class="fas fa-minus-circle text-primary" v-if="f == search.foods_fields && f > 1" />
<i class="fas fa-minus-circle text-primary"
v-if="f == search.foods_fields && f > 1"/>
</b-input-group-text> </b-input-group-text>
</template> </template>
<treeselect <treeselect
@ -405,24 +293,15 @@
/> />
<b-input-group-append> <b-input-group-append>
<b-input-group-text> <b-input-group-text>
<b-form-checkbox v-model="search.search_foods[i].operator" <b-form-checkbox v-model="search.search_foods[i].operator" name="check-button" @change="refreshData(false)" class="shadow-none" switch style="width: 4em">
name="check-button" <span class="text-uppercase" v-if="search.search_foods[i].operator">{{ $t("or") }}</span>
@change="refreshData(false)"
class="shadow-none" switch style="width: 4em">
<span class="text-uppercase"
v-if="search.search_foods[i].operator">{{
$t("or")
}}</span>
<span class="text-uppercase" v-else>{{ $t("and") }}</span> <span class="text-uppercase" v-else>{{ $t("and") }}</span>
</b-form-checkbox> </b-form-checkbox>
</b-input-group-text> </b-input-group-text>
</b-input-group-append> </b-input-group-append>
<b-input-group-append v-if="ui.expert_mode"> <b-input-group-append v-if="ui.expert_mode">
<b-input-group-text> <b-input-group-text>
<b-form-checkbox v-model="search.search_foods[i].not" <b-form-checkbox v-model="search.search_foods[i].not" name="check-button" @change="refreshData(false)" class="shadow-none">
name="check-button"
@change="refreshData(false)"
class="shadow-none">
<span class="text-uppercase">{{ $t("not") }}</span> <span class="text-uppercase">{{ $t("not") }}</span>
</b-form-checkbox> </b-form-checkbox>
</b-input-group-text> </b-input-group-text>
@ -435,23 +314,18 @@
<h6 class="mt-2 mb-0" v-if="ui.expert_mode && search.books_fields > 1"> <h6 class="mt-2 mb-0" v-if="ui.expert_mode && search.books_fields > 1">
{{ $t("Books") }} {{ $t("Books") }}
</h6> </h6>
<span class="text-sm-left text-warning" <span class="text-sm-left text-warning" v-if="ui.expert_mode && search.books_fields > 1 && hasDuplicateFilter(search.search_books, search.books_fields)">{{
v-if="ui.expert_mode && search.books_fields > 1 && hasDuplicateFilter(search.search_books, search.books_fields)">{{ $t("warning_duplicate_filter")
$t("warning_duplicate_filter") }}</span>
}}</span>
<div class="row" v-if="ui.show_books"> <div class="row" v-if="ui.show_books">
<div class="col-12"> <div class="col-12">
<b-input-group class="mt-2" v-for="(b, i) in bookFields" :key="i"> <b-input-group class="mt-2" v-for="(b, i) in bookFields" :key="i">
<template #prepend v-if="ui.expert_mode"> <template #prepend v-if="ui.expert_mode">
<b-input-group-text style="width: 3em" <b-input-group-text style="width: 3em" @click="addField('books', b)">
@click="addField('books', b)"> <i class="fas fa-plus-circle text-primary" v-if="b == search.books_fields && b < 4" />
<i class="fas fa-plus-circle text-primary"
v-if="b == search.books_fields && b < 4"/>
</b-input-group-text> </b-input-group-text>
<b-input-group-text style="width: 3em" <b-input-group-text style="width: 3em" @click="removeField('books', b)">
@click="removeField('books', b)"> <i class="fas fa-minus-circle text-primary" v-if="b == search.books_fields && b > 1" />
<i class="fas fa-minus-circle text-primary"
v-if="b == search.books_fields && b > 1"/>
</b-input-group-text> </b-input-group-text>
</template> </template>
<generic-multiselect <generic-multiselect
@ -465,24 +339,15 @@
></generic-multiselect> ></generic-multiselect>
<b-input-group-append> <b-input-group-append>
<b-input-group-text> <b-input-group-text>
<b-form-checkbox v-model="search.search_books[i].operator" <b-form-checkbox v-model="search.search_books[i].operator" name="check-button" @change="refreshData(false)" class="shadow-none" style="width: 4em" switch>
name="check-button" <span class="text-uppercase" v-if="search.search_books[i].operator">{{ $t("or") }}</span>
@change="refreshData(false)"
class="shadow-none" style="width: 4em" switch>
<span class="text-uppercase"
v-if="search.search_books[i].operator">{{
$t("or")
}}</span>
<span class="text-uppercase" v-else>{{ $t("and") }}</span> <span class="text-uppercase" v-else>{{ $t("and") }}</span>
</b-form-checkbox> </b-form-checkbox>
</b-input-group-text> </b-input-group-text>
</b-input-group-append> </b-input-group-append>
<b-input-group-append v-if="ui.expert_mode"> <b-input-group-append v-if="ui.expert_mode">
<b-input-group-text> <b-input-group-text>
<b-form-checkbox v-model="search.search_books[i].not" <b-form-checkbox v-model="search.search_books[i].not" name="check-button" @change="refreshData(false)" class="shadow-none">
name="check-button"
@change="refreshData(false)"
class="shadow-none">
<span class="text-uppercase">{{ $t("not") }}</span> <span class="text-uppercase">{{ $t("not") }}</span>
</b-form-checkbox> </b-form-checkbox>
</b-input-group-text> </b-input-group-text>
@ -506,12 +371,8 @@
/> />
<b-input-group-append> <b-input-group-append>
<b-input-group-text> <b-input-group-text>
<b-form-checkbox v-model="search.search_rating_gte" <b-form-checkbox v-model="search.search_rating_gte" name="check-button" @change="refreshData(false)" class="shadow-none" switch style="width: 4em">
name="check-button" <span class="text-uppercase" v-if="search.search_rating_gte">&gt;=</span>
@change="refreshData(false)"
class="shadow-none" switch style="width: 4em">
<span class="text-uppercase"
v-if="search.search_rating_gte">&gt;=</span>
<span class="text-uppercase" v-else>&lt;=</span> <span class="text-uppercase" v-else>&lt;=</span>
</b-form-checkbox> </b-form-checkbox>
</b-input-group-text> </b-input-group-text>
@ -534,13 +395,8 @@
></generic-multiselect> ></generic-multiselect>
<b-input-group-append> <b-input-group-append>
<b-input-group-text> <b-input-group-text>
<b-form-checkbox v-model="search.search_units_or" <b-form-checkbox v-model="search.search_units_or" name="check-button" @change="refreshData(false)" class="shadow-none" style="width: 4em" switch>
name="check-button" <span class="text-uppercase" v-if="search.search_units_or">{{ $t("or") }}</span>
@change="refreshData(false)"
class="shadow-none" style="width: 4em" switch>
<span class="text-uppercase" v-if="search.search_units_or">{{
$t("or")
}}</span>
<span class="text-uppercase" v-else>{{ $t("and") }}</span> <span class="text-uppercase" v-else>{{ $t("and") }}</span>
</b-form-checkbox> </b-form-checkbox>
</b-input-group-text> </b-input-group-text>
@ -550,23 +406,17 @@
</div> </div>
<!-- special switches --> <!-- special switches -->
<div class="row g-0" <div class="row g-0" v-if="ui.show_timescooked || ui.show_makenow || ui.show_cookedon">
v-if="ui.show_timescooked || ui.show_makenow || ui.show_cookedon">
<div class="col-12"> <div class="col-12">
<b-input-group class="mt-2"> <b-input-group class="mt-2">
<!-- times cooked --> <!-- times cooked -->
<b-input-group-prepend is-text v-if="ui.show_timescooked"> <b-input-group-prepend is-text v-if="ui.show_timescooked">
{{ $t("times_cooked") }} {{ $t("times_cooked") }}
</b-input-group-prepend> </b-input-group-prepend>
<b-form-input id="timescooked" type="number" min="0" <b-form-input id="timescooked" type="number" min="0" v-model="search.timescooked" v-if="ui.show_timescooked"></b-form-input>
v-model="search.timescooked"
v-if="ui.show_timescooked"></b-form-input>
<b-input-group-append v-if="ui.show_timescooked"> <b-input-group-append v-if="ui.show_timescooked">
<b-input-group-text> <b-input-group-text>
<b-form-checkbox v-model="search.timescooked_gte" <b-form-checkbox v-model="search.timescooked_gte" name="check-button" @change="refreshData(false)" class="shadow-none" switch style="width: 4em">
name="check-button"
@change="refreshData(false)"
class="shadow-none" switch style="width: 4em">
<span class="text-uppercase" v-if="search.timescooked_gte">&gt;=</span> <span class="text-uppercase" v-if="search.timescooked_gte">&gt;=</span>
<span class="text-uppercase" v-else>&lt;=</span> <span class="text-uppercase" v-else>&lt;=</span>
</b-form-checkbox> </b-form-checkbox>
@ -585,10 +435,7 @@
@input="refreshData(false)" @input="refreshData(false)"
/> />
<b-input-group-text> <b-input-group-text>
<b-form-checkbox v-model="search.cookedon_gte" <b-form-checkbox v-model="search.cookedon_gte" name="check-button" @change="refreshData(false)" class="shadow-none" switch style="width: 4em">
name="check-button"
@change="refreshData(false)"
class="shadow-none" switch style="width: 4em">
<span class="text-uppercase" v-if="search.cookedon_gte">&gt;=</span> <span class="text-uppercase" v-if="search.cookedon_gte">&gt;=</span>
<span class="text-uppercase" v-else>&lt;=</span> <span class="text-uppercase" v-else>&lt;=</span>
</b-form-checkbox> </b-form-checkbox>
@ -608,10 +455,7 @@
@input="refreshData(false)" @input="refreshData(false)"
/> />
<b-input-group-text> <b-input-group-text>
<b-form-checkbox v-model="search.createdon_gte" <b-form-checkbox v-model="search.createdon_gte" name="check-button" @change="refreshData(false)" class="shadow-none" switch style="width: 4em">
name="check-button"
@change="refreshData(false)"
class="shadow-none" switch style="width: 4em">
<span class="text-uppercase" v-if="search.createdon_gte">&gt;=</span> <span class="text-uppercase" v-if="search.createdon_gte">&gt;=</span>
<span class="text-uppercase" v-else>&lt;=</span> <span class="text-uppercase" v-else>&lt;=</span>
</b-form-checkbox> </b-form-checkbox>
@ -629,10 +473,7 @@
@input="refreshData(false)" @input="refreshData(false)"
/> />
<b-input-group-text> <b-input-group-text>
<b-form-checkbox v-model="search.viewedon_gte" <b-form-checkbox v-model="search.viewedon_gte" name="check-button" @change="refreshData(false)" class="shadow-none" switch style="width: 4em">
name="check-button"
@change="refreshData(false)"
class="shadow-none" switch style="width: 4em">
<span class="text-uppercase" v-if="search.viewedon_gte">&gt;=</span> <span class="text-uppercase" v-if="search.viewedon_gte">&gt;=</span>
<span class="text-uppercase" v-else>&lt;=</span> <span class="text-uppercase" v-else>&lt;=</span>
</b-form-checkbox> </b-form-checkbox>
@ -650,10 +491,7 @@
@input="refreshData(false)" @input="refreshData(false)"
/> />
<b-input-group-text> <b-input-group-text>
<b-form-checkbox v-model="search.updatedon_gte" <b-form-checkbox v-model="search.updatedon_gte" name="check-button" @change="refreshData(false)" class="shadow-none" switch style="width: 4em">
name="check-button"
@change="refreshData(false)"
class="shadow-none" switch style="width: 4em">
<span class="text-uppercase" v-if="search.updatedon_gte">&gt;=</span> <span class="text-uppercase" v-if="search.updatedon_gte">&gt;=</span>
<span class="text-uppercase" v-else>&lt;=</span> <span class="text-uppercase" v-else>&lt;=</span>
</b-form-checkbox> </b-form-checkbox>
@ -662,9 +500,7 @@
<b-input-group-append v-if="ui.show_makenow"> <b-input-group-append v-if="ui.show_makenow">
<b-input-group-text> <b-input-group-text>
{{ $t("make_now") }} {{ $t("make_now") }}
<b-form-checkbox v-model="search.makenow" name="check-button" <b-form-checkbox v-model="search.makenow" name="check-button" @change="refreshData(false)" class="shadow-none" switch style="width: 4em" />
@change="refreshData(false)"
class="shadow-none" switch style="width: 4em"/>
</b-input-group-text> </b-input-group-text>
</b-input-group-append> </b-input-group-append>
</b-input-group> </b-input-group>
@ -674,16 +510,14 @@
<!-- Buttons --> <!-- Buttons -->
<div class="row justify-content-end small"> <div class="row justify-content-end small">
<div class="col-auto"> <div class="col-auto">
<b-button class="my-0" variant="link" size="sm" <b-button class="my-0" variant="link" size="sm" @click="search.explain_visible = !search.explain_visible">
@click="search.explain_visible = !search.explain_visible">
<div v-if="!search.explain_visible"> <div v-if="!search.explain_visible">
<i class="far fa-eye"></i> <i class="far fa-eye"></i>
{{ $t("explain") }} {{ $t("explain") }}
</div> </div>
<div v-else><i class="far fa-eye-slash"></i> {{ $t("explain") }}</div> <div v-else><i class="far fa-eye-slash"></i> {{ $t("explain") }}</div>
</b-button> </b-button>
<b-button class="my-0" variant="link" size="sm" <b-button class="my-0" variant="link" size="sm" @click="ui.expert_mode = !ui.expert_mode">
@click="ui.expert_mode = !ui.expert_mode">
<div v-if="!ui.expert_mode"> <div v-if="!ui.expert_mode">
<i class="fas fa-circle"></i> <i class="fas fa-circle"></i>
{{ $t("expert_mode") }} {{ $t("expert_mode") }}
@ -706,21 +540,20 @@
<!-- TODO find a way to localize this that works without explaining localization to each language translator --> <!-- TODO find a way to localize this that works without explaining localization to each language translator -->
Show all recipes that are matched Show all recipes that are matched
<span v-if="search.search_input"> <span v-if="search.search_input">
by <i>{{ search.search_input }}</i> <br/> by <i>{{ search.search_input }}</i> <br />
</span> </span>
<span v-else> without any search term <br/> </span> <span v-else> without any search term <br /> </span>
<span v-if="search.search_internal"> and are <span class="text-success">internal</span> <br/></span> <span v-if="search.search_internal"> and are <span class="text-success">internal</span> <br /></span>
<span v-for="k in search.search_keywords" v-bind:key="k.id"> <span v-for="k in search.search_keywords" v-bind:key="k.id">
<template v-if="k.items.length > 0"> <template v-if="k.items.length > 0">
and and
<b v-if="k.not">don't</b> <b v-if="k.not">don't</b>
contain contain
<b v-if="k.operator">any</b><b <b v-if="k.operator">any</b><b v-else>all</b> of the following <span class="text-success">keywords</span>:
v-else>all</b> of the following <span class="text-success">keywords</span>:
<i>{{ k.items.flatMap((x) => x.name).join(", ") }}</i> <i>{{ k.items.flatMap((x) => x.name).join(", ") }}</i>
<br/> <br />
</template> </template>
</span> </span>
@ -729,11 +562,9 @@
and and
<b v-if="k.not">don't</b> <b v-if="k.not">don't</b>
contain contain
<b v-if="k.operator">any</b><b <b v-if="k.operator">any</b><b v-else>all</b> of the following <span class="text-success">foods</span>:
v-else>all</b> of the following <span
class="text-success">foods</span>:
<i>{{ k.items.flatMap((x) => x.name).join(", ") }}</i> <i>{{ k.items.flatMap((x) => x.name).join(", ") }}</i>
<br/> <br />
</template> </template>
</span> </span>
@ -742,48 +573,40 @@
and and
<b v-if="k.not">don't</b> <b v-if="k.not">don't</b>
contain contain
<b v-if="k.operator">any</b><b <b v-if="k.operator">any</b><b v-else>all</b> of the following <span class="text-success">books</span>:
v-else>all</b> of the following <span
class="text-success">books</span>:
<i>{{ k.items.flatMap((x) => x.name).join(", ") }}</i> <i>{{ k.items.flatMap((x) => x.name).join(", ") }}</i>
<br/> <br />
</template> </template>
</span> </span>
<span v-if="search.makenow"> and you can <span class="text-success">make right now</span> (based on the on hand flag) <br/></span> <span v-if="search.makenow"> and you can <span class="text-success">make right now</span> (based on the on hand flag) <br /></span>
<span v-if="search.search_units.length > 0"> <span v-if="search.search_units.length > 0">
and contain <b v-if="search.search_units_or">any</b><b and contain <b v-if="search.search_units_or">any</b><b v-else>all</b> of the following <span class="text-success">units</span>:
v-else>all</b> of the following <span
class="text-success">units</span>:
<i>{{ search.search_units.flatMap((x) => x.name).join(", ") }}</i <i>{{ search.search_units.flatMap((x) => x.name).join(", ") }}</i
><br/> ><br />
</span> </span>
<span v-if="search.search_rating !== undefined"> <span v-if="search.search_rating !== undefined">
and have a <span class="text-success">rating</span> <template and have a <span class="text-success">rating</span> <template v-if="search.search_rating_gte">greater than</template><template v-else> less than</template> or
v-if="search.search_rating_gte">greater than</template><template equal to {{ search.search_rating }}<br />
v-else> less than</template> or
equal to {{ search.search_rating }}<br/>
</span> </span>
<span v-if="search.lastcooked !== undefined"> <span v-if="search.lastcooked !== undefined">
and have been <span class="text-success">last cooked</span> <template and have been <span class="text-success">last cooked</span> <template v-if="search.lastcooked_gte"> after</template><template v-else> before</template>
v-if="search.lastcooked_gte"> after</template><template v-else> before</template>
<i>{{ search.lastcooked }}</i <i>{{ search.lastcooked }}</i
><br/> ><br />
</span> </span>
<span v-if="search.timescooked !== undefined"> <span v-if="search.timescooked !== undefined">
and have <span class="text-success">been cooked</span> <template and have <span class="text-success">been cooked</span> <template v-if="search.timescooked_gte"> at least</template><template v-else> less than</template> or
v-if="search.timescooked_gte"> at least</template><template v-else> less than</template> or equal to<i>{{ search.timescooked }}</i> times <br />
equal to<i>{{ search.timescooked }}</i> times <br/>
</span> </span>
<span v-if="search.sort_order.length > 0"> <span v-if="search.sort_order.length > 0">
<span class="text-success">order</span> by <span class="text-success">order</span> by
<i>{{ search.sort_order.flatMap((x) => x.text).join(", ") }}</i> <i>{{ search.sort_order.flatMap((x) => x.text).join(", ") }}</i>
<br/> <br />
</span> </span>
</div> </div>
</div> </div>
@ -797,14 +620,13 @@
<div v-if="recipes.length > 0"> <div v-if="recipes.length > 0">
<div class="row align-content-center"> <div class="row align-content-center">
<div class="col col-md-6" style="margin-top: 2vh"> <div class="col col-md-6" style="margin-top: 2vh">
<b-dropdown id="sortby" :text="sortByLabel" variant="link" <b-dropdown id="sortby" :text="sortByLabel" variant="link" toggle-class="text-decoration-none " class="m-0 p-0">
toggle-class="text-decoration-none " class="m-0 p-0">
<div v-for="o in sortOptions" :key="o.id"> <div v-for="o in sortOptions" :key="o.id">
<b-dropdown-item <b-dropdown-item
v-on:click=" v-on:click="
search.sort_order = [o] search.sort_order = [o]
refreshData(false) refreshData(false)
" "
> >
<span>{{ o.text }}</span> <span>{{ o.text }}</span>
</b-dropdown-item> </b-dropdown-item>
@ -812,35 +634,34 @@
</b-dropdown> </b-dropdown>
</div> </div>
<div class="col col-md-6 text-right" style="margin-top: 2vh"> <div class="col col-md-6 text-right" style="margin-top: 2vh">
<span class="text-muted"> <span class="text-muted">
{{ $t("Page") }} {{ search.pagination_page }}/{{ {{ $t("Page") }} {{ search.pagination_page }}/{{ Math.ceil(pagination_count / ui.page_size) }}
Math.ceil(pagination_count / ui.page_size) <a href="#" @click="resetSearch()"><i class="fas fa-times-circle"></i> {{ $t("Reset") }}</a>
}} </span>
<a href="#" @click="resetSearch()"><i class="fas fa-times-circle"></i> {{ $t("Reset") }}</a>
</span>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col col-md-12"> <div class="col col-md-12">
<div <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-gap: 0.8rem">
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-gap: 0.8rem">
<template v-if="!searchFiltered()"> <template v-if="!searchFiltered()">
<recipe-card v-bind:key="`mp_${m.id}`" v-for="m in meal_plans" :recipe="m.recipe" <recipe-card
:meal_plan="m" :footer_text="m.meal_type_name" v-bind:key="`mp_${m.id}`"
footer_icon="far fa-calendar-alt"></recipe-card> v-for="m in meal_plans"
:recipe="m.recipe"
:meal_plan="m"
:footer_text="m.meal_type_name"
footer_icon="far fa-calendar-alt"
></recipe-card>
</template> </template>
<recipe-card v-for="r in recipes" v-bind:key="r.id" :recipe="r" <recipe-card v-for="r in recipes" v-bind:key="r.id" :recipe="r" :footer_text="isRecentOrNew(r)[0]" :footer_icon="isRecentOrNew(r)[1]"></recipe-card>
:footer_text="isRecentOrNew(r)[0]"
:footer_icon="isRecentOrNew(r)[1]"></recipe-card>
</div> </div>
</div> </div>
</div> </div>
<div class="row" style="margin-top: 2vh" v-if="!random_search"> <div class="row" style="margin-top: 2vh" v-if="!random_search">
<div class="col col-md-12"> <div class="col col-md-12">
<b-pagination pills v-model="search.pagination_page" :total-rows="pagination_count" <b-pagination pills v-model="search.pagination_page" :total-rows="pagination_count" :per-page="ui.page_size" @change="pageChange" align="center"></b-pagination>
:per-page="ui.page_size" @change="pageChange" align="center"></b-pagination>
</div> </div>
</div> </div>
<div class="col-md-2 d-none d-md-block"></div> <div class="col-md-2 d-none d-md-block"></div>
@ -848,9 +669,8 @@
<div v-if="recipes.length < 1 && !recipes_loading"> <div v-if="recipes.length < 1 && !recipes_loading">
<div class="row mt-5"> <div class="row mt-5">
<div class="col col-md-12 text-center"> <div class="col col-md-12 text-center">
<h4 class="text-muted"><i class="far fa-eye"></i> {{ $t('search_no_recipes') }}</h4> <h4 class="text-muted"><i class="far fa-eye"></i> {{ $t("search_no_recipes") }}</h4>
</div> </div>
</div> </div>
<div class="row mt-2"> <div class="row mt-2">
@ -858,47 +678,41 @@
<b-card-group deck> <b-card-group deck>
<b-card v-bind:title="$t('Import')" class="text-center"> <b-card v-bind:title="$t('Import')" class="text-center">
<b-card-text> <b-card-text>
{{ $t('search_import_help_text') }} {{ $t("search_import_help_text") }}
</b-card-text> </b-card-text>
<b-button variant="primary" :href="resolveDjangoUrl('data_import_url')"><i <b-button variant="primary" :href="resolveDjangoUrl('data_import_url')"><i class="fas fa-file-import"></i> {{ $t("Import") }} </b-button>
class="fas fa-file-import"></i> {{ $t('Import') }}
</b-button>
</b-card> </b-card>
<b-card v-bind:title="$t('Create')" class="text-center"> <b-card v-bind:title="$t('Create')" class="text-center">
<b-card-text> <b-card-text>
{{ $t('search_create_help_text') }} {{ $t("search_create_help_text") }}
</b-card-text> </b-card-text>
<b-button variant="primary" :href="resolveDjangoUrl('new_recipe')"><i <b-button variant="primary" :href="resolveDjangoUrl('new_recipe')"><i class="fas fa-plus"></i> {{ $t("Create") }} </b-button>
class="fas fa-plus"></i> {{ $t('Create') }}
</b-button>
</b-card> </b-card>
</b-card-group> </b-card-group>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import Vue from "vue" import Vue from "vue"
import {BootstrapVue} from "bootstrap-vue" import { BootstrapVue } from "bootstrap-vue"
import VueCookies from "vue-cookies" import VueCookies from "vue-cookies"
import "bootstrap-vue/dist/bootstrap-vue.css" import "bootstrap-vue/dist/bootstrap-vue.css"
import moment from "moment" import moment from "moment"
import _debounce from "lodash/debounce" import _debounce from "lodash/debounce"
import Multiselect from "vue-multiselect" import Multiselect from "vue-multiselect"
import {Treeselect, LOAD_CHILDREN_OPTIONS} from "@riophae/vue-treeselect" import { Treeselect, LOAD_CHILDREN_OPTIONS } from "@riophae/vue-treeselect"
import "@riophae/vue-treeselect/dist/vue-treeselect.css" import "@riophae/vue-treeselect/dist/vue-treeselect.css"
import {ApiMixin, ResolveUrlMixin, StandardToasts, ToastMixin} from "@/utils/utils" import { ApiMixin, ResolveUrlMixin, StandardToasts, ToastMixin } from "@/utils/utils"
import LoadingSpinner from "@/components/LoadingSpinner" // TODO: is this deprecated? import LoadingSpinner from "@/components/LoadingSpinner" // TODO: is this deprecated?
import RecipeCard from "@/components/RecipeCard" import RecipeCard from "@/components/RecipeCard"
import GenericMultiselect from "@/components/GenericMultiselect" import GenericMultiselect from "@/components/GenericMultiselect"
@ -913,13 +727,13 @@ let UI_COOKIE_NAME = "ui_search_settings"
export default { export default {
name: "RecipeSearchView", name: "RecipeSearchView",
mixins: [ResolveUrlMixin, ApiMixin, ToastMixin], mixins: [ResolveUrlMixin, ApiMixin, ToastMixin],
components: {GenericMultiselect, RecipeCard, Treeselect, RecipeSwitcher, Multiselect}, components: { GenericMultiselect, RecipeCard, Treeselect, RecipeSwitcher, Multiselect },
data() { data() {
return { return {
// this.Models and this.Actions inherited from ApiMixin // this.Models and this.Actions inherited from ApiMixin
recipes: [], recipes: [],
recipes_loading: true, recipes_loading: true,
facets: {Books: [], Foods: [], Keywords: []}, facets: { Books: [], Foods: [], Keywords: [] },
meal_plans: [], meal_plans: [],
last_viewed_recipes: [], last_viewed_recipes: [],
sortMenu: false, sortMenu: false,
@ -930,22 +744,22 @@ export default {
search_input: "", search_input: "",
search_internal: false, search_internal: false,
search_keywords: [ search_keywords: [
{items: [], operator: true, not: false}, { items: [], operator: true, not: false },
{items: [], operator: false, not: false}, { items: [], operator: false, not: false },
{items: [], operator: true, not: true}, { items: [], operator: true, not: true },
{items: [], operator: false, not: true}, { items: [], operator: false, not: true },
], ],
search_foods: [ search_foods: [
{items: [], operator: true, not: false}, { items: [], operator: true, not: false },
{items: [], operator: false, not: false}, { items: [], operator: false, not: false },
{items: [], operator: true, not: true}, { items: [], operator: true, not: true },
{items: [], operator: false, not: true}, { items: [], operator: false, not: true },
], ],
search_books: [ search_books: [
{items: [], operator: true, not: false}, { items: [], operator: true, not: false },
{items: [], operator: false, not: false}, { items: [], operator: false, not: false },
{items: [], operator: true, not: true}, { items: [], operator: true, not: true },
{items: [], operator: false, not: true}, { items: [], operator: false, not: true },
], ],
search_units: [], search_units: [],
search_units_or: true, search_units_or: true,
@ -1054,12 +868,12 @@ export default {
} }
return [ return [
{id: 5, label: "⭐⭐⭐⭐⭐" + ratingCount(this.facets.Ratings?.["5.0"] ?? 0) + label(5)}, { id: 5, label: "⭐⭐⭐⭐⭐" + ratingCount(this.facets.Ratings?.["5.0"] ?? 0) + label(5) },
{id: 4, label: "⭐⭐⭐⭐ " + ratingCount(this.facets.Ratings?.["4.0"] ?? 0) + label()}, { id: 4, label: "⭐⭐⭐⭐ " + ratingCount(this.facets.Ratings?.["4.0"] ?? 0) + label() },
{id: 3, label: "⭐⭐⭐ " + ratingCount(this.facets.Ratings?.["3.0"] ?? 0) + label()}, { id: 3, label: "⭐⭐⭐ " + ratingCount(this.facets.Ratings?.["3.0"] ?? 0) + label() },
{id: 2, label: "⭐⭐ " + ratingCount(this.facets.Ratings?.["2.0"] ?? 0) + label()}, { id: 2, label: "⭐⭐ " + ratingCount(this.facets.Ratings?.["2.0"] ?? 0) + label() },
{id: 1, label: "⭐ " + ratingCount(this.facets.Ratings?.["1.0"] ?? 0) + label(1)}, { id: 1, label: "⭐ " + ratingCount(this.facets.Ratings?.["1.0"] ?? 0) + label(1) },
{id: 0, label: this.$t("Unrated") + ratingCount(this.facets.Ratings?.["0.0"] ?? 0)}, { id: 0, label: this.$t("Unrated") + ratingCount(this.facets.Ratings?.["0.0"] ?? 0) },
] ]
}, },
keywordFields: function () { keywordFields: function () {
@ -1122,22 +936,22 @@ export default {
this.facets.Keywords = [] this.facets.Keywords = []
for (let x of urlParams.getAll("keyword")) { for (let x of urlParams.getAll("keyword")) {
this.search.search_keywords[0].items.push(Number.parseInt(x)) this.search.search_keywords[0].items.push(Number.parseInt(x))
this.facets.Keywords.push({id: x, name: "loading..."}) this.facets.Keywords.push({ id: x, name: "loading..." })
} }
} }
// TODO: figure out how to find nested items and load keyword/food children for that branch // TODO: figure out how to find nested items and load keyword/food children for that branch
// probably a backend change in facets to pre-load children of nested items // probably a backend change in facets to pre-load children of nested items
for (let x of this.search.search_foods.map((x) => x.items).flat()) { for (let x of this.search.search_foods.map((x) => x.items).flat()) {
this.facets.Foods.push({id: x, name: "loading..."}) this.facets.Foods.push({ id: x, name: "loading..." })
} }
for (let x of this.search.search_keywords.map((x) => x.items).flat()) { for (let x of this.search.search_keywords.map((x) => x.items).flat()) {
this.facets.Keywords.push({id: x, name: "loading..."}) this.facets.Keywords.push({ id: x, name: "loading..." })
} }
for (let x of this.search.search_books.map((x) => x.items).flat()) { for (let x of this.search.search_books.map((x) => x.items).flat()) {
this.facets.Books.push({id: x, name: "loading..."}) this.facets.Books.push({ id: x, name: "loading..." })
} }
this.loadMealPlan() this.loadMealPlan()
@ -1187,13 +1001,13 @@ export default {
"ui.expert_mode": function (newVal, oldVal) { "ui.expert_mode": function (newVal, oldVal) {
if (!newVal) { if (!newVal) {
this.search.search_keywords = this.search.search_keywords.map((x) => { this.search.search_keywords = this.search.search_keywords.map((x) => {
return {...x, not: false} return { ...x, not: false }
}) })
this.search.search_foods = this.search.search_foods.map((x) => { this.search.search_foods = this.search.search_foods.map((x) => {
return {...x, not: false} return { ...x, not: false }
}) })
this.search.search_books = this.search.search_books.map((x) => { this.search.search_books = this.search.search_books.map((x) => {
return {...x, not: false} return { ...x, not: false }
}) })
} }
}, },
@ -1263,13 +1077,13 @@ export default {
}, },
resetSearch: function (filter = undefined) { resetSearch: function (filter = undefined) {
this.search.search_keywords = this.search.search_keywords.map((x) => { this.search.search_keywords = this.search.search_keywords.map((x) => {
return {...x, items: []} return { ...x, items: [] }
}) })
this.search.search_foods = this.search.search_foods.map((x) => { this.search.search_foods = this.search.search_foods.map((x) => {
return {...x, items: []} return { ...x, items: [] }
}) })
this.search.search_books = this.search.search_books.map((x) => { this.search.search_books = this.search.search_books.map((x) => {
return {...x, items: []} return { ...x, items: [] }
}) })
this.search.search_input = filter?.query ?? "" this.search.search_input = filter?.query ?? ""
this.search.search_internal = filter?.internal ?? false this.search.search_internal = filter?.internal ?? false
@ -1324,28 +1138,27 @@ export default {
if (!this.ui.tree_select) { if (!this.ui.tree_select) {
return return
} }
let params = {hash: hash} let params = { hash: hash }
if (facet) { if (facet) {
params[facet] = id params[facet] = id
} }
return this.genericGetAPI("api_get_facets", params).then((response) => { return this.genericGetAPI("api_get_facets", params).then((response) => {
this.facets = {...this.facets, ...response.data.facets} this.facets = { ...this.facets, ...response.data.facets }
}) })
}, },
showSQL: function () { showSQL: function () {
let params = this.buildParams() let params = this.buildParams()
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params).then((result) => { this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params).then((result) => {})
})
}, },
// TODO refactor to combine with load KeywordChildren // TODO refactor to combine with load KeywordChildren
loadFoodChildren({action, parentNode, callback}) { loadFoodChildren({ action, parentNode, callback }) {
if (action === LOAD_CHILDREN_OPTIONS) { if (action === LOAD_CHILDREN_OPTIONS) {
if (this.facets?.cache_key) { if (this.facets?.cache_key) {
this.getFacets(this.facets.cache_key, "food", parentNode.id).then(callback()) this.getFacets(this.facets.cache_key, "food", parentNode.id).then(callback())
} }
} }
}, },
loadKeywordChildren({action, parentNode, callback}) { loadKeywordChildren({ action, parentNode, callback }) {
if (action === LOAD_CHILDREN_OPTIONS) { if (action === LOAD_CHILDREN_OPTIONS) {
if (this.facets?.cache_key) { if (this.facets?.cache_key) {
this.getFacets(this.facets.cache_key, "keyword", parentNode.id).then(callback()) this.getFacets(this.facets.cache_key, "keyword", parentNode.id).then(callback())
@ -1356,7 +1169,7 @@ export default {
return return
}, },
buildParams: function (random) { buildParams: function (random) {
let params = {options: {query: {}}, page: this.search.pagination_page, pageSize: this.ui.page_size} let params = { options: { query: {} }, page: this.search.pagination_page, pageSize: this.ui.page_size }
if (this.search.search_filter) { if (this.search.search_filter) {
params.options.query.filter = this.search.search_filter.id params.options.query.filter = this.search.search_filter.id
return params return params
@ -1473,10 +1286,12 @@ export default {
} }
let search = this.buildParams(false) let search = this.buildParams(false)
console.log("after build", search)
;["page", "pageSize"].forEach((key) => { ;["page", "pageSize"].forEach((key) => {
delete search[key] delete search[key]
}) })
search = {...search, ...search.options.query} search = { ...search, ...search.options.query }
console.log("after concat", search)
let params = { let params = {
name: filtername, name: filtername,
search: JSON.stringify(search), search: JSON.stringify(search),