fixes keyword filter on OR search
This commit is contained in:
@ -1,5 +1,7 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.postgres.search import TrigramSimilarity
|
||||||
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
@ -11,6 +11,8 @@ from cookbook.managers import DICTIONARY
|
|||||||
from cookbook.models import Food, Keyword, ViewLog
|
from cookbook.models import Food, Keyword, ViewLog
|
||||||
|
|
||||||
|
|
||||||
|
# TODO create extensive tests to make sure ORs ANDs and various filters, sorting, etc work as expected
|
||||||
|
# TODO consider creating a simpleListRecipe API that only includes minimum of recipe info and minimal filtering
|
||||||
def search_recipes(request, queryset, params):
|
def search_recipes(request, queryset, params):
|
||||||
search_prefs = request.user.searchpreference
|
search_prefs = request.user.searchpreference
|
||||||
search_string = params.get('query', '')
|
search_string = params.get('query', '')
|
||||||
@ -19,6 +21,7 @@ def search_recipes(request, queryset, params):
|
|||||||
search_foods = params.getlist('foods', [])
|
search_foods = params.getlist('foods', [])
|
||||||
search_books = params.getlist('books', [])
|
search_books = params.getlist('books', [])
|
||||||
|
|
||||||
|
# TODO I think default behavior should be 'AND' which is how most sites operate with facet/filters based on results
|
||||||
search_keywords_or = params.get('keywords_or', True)
|
search_keywords_or = params.get('keywords_or', True)
|
||||||
search_foods_or = params.get('foods_or', True)
|
search_foods_or = params.get('foods_or', True)
|
||||||
search_books_or = params.get('books_or', True)
|
search_books_or = params.get('books_or', True)
|
||||||
@ -124,12 +127,12 @@ def search_recipes(request, queryset, params):
|
|||||||
queryset = queryset.filter(query_filter)
|
queryset = queryset.filter(query_filter)
|
||||||
|
|
||||||
if len(search_keywords) > 0:
|
if len(search_keywords) > 0:
|
||||||
# TODO creating setting to include descendants of keywords a setting
|
|
||||||
if search_keywords_or == 'true':
|
if search_keywords_or == 'true':
|
||||||
# when performing an 'or' search all descendants are included in the OR condition
|
# when performing an 'or' search all descendants are included in the OR condition
|
||||||
# so descendants are appended to filter all at once
|
# so descendants are appended to filter all at once
|
||||||
for kw in Keyword.objects.filter(pk__in=search_keywords):
|
# TODO creating setting to include descendants of keywords a setting
|
||||||
search_keywords += list(kw.get_descendants().values_list('pk', flat=True))
|
# for kw in Keyword.objects.filter(pk__in=search_keywords):
|
||||||
|
# search_keywords += list(kw.get_descendants().values_list('pk', flat=True))
|
||||||
queryset = queryset.filter(keywords__id__in=search_keywords)
|
queryset = queryset.filter(keywords__id__in=search_keywords)
|
||||||
else:
|
else:
|
||||||
# when performing an 'and' search returned recipes should include a parent OR any of its descedants
|
# when performing an 'and' search returned recipes should include a parent OR any of its descedants
|
||||||
@ -165,24 +168,31 @@ def search_recipes(request, queryset, params):
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
def get_facet(qs, params):
|
def get_facet(qs, params, space):
|
||||||
|
# NOTE facet counts for tree models include self AND descendants
|
||||||
facets = {}
|
facets = {}
|
||||||
ratings = params.getlist('ratings', [])
|
ratings = params.getlist('ratings', [])
|
||||||
keyword_list = params.getlist('keywords', [])
|
keyword_list = params.getlist('keywords', [])
|
||||||
ingredient_list = params.getlist('ingredient', [])
|
ingredient_list = params.getlist('foods', [])
|
||||||
book_list = params.getlist('book', [])
|
book_list = params.getlist('book', [])
|
||||||
|
search_keywords_or = params.get('keywords_or', True)
|
||||||
|
search_foods_or = params.get('foods_or', True)
|
||||||
|
search_books_or = params.get('books_or', True)
|
||||||
|
|
||||||
# this returns a list of keywords in the queryset and how many times it appears
|
# if using an OR search, will annotate all keywords, otherwise, just those that appear in results
|
||||||
kws = Keyword.objects.filter(recipe__in=qs).annotate(kw_count=Count('recipe'))
|
if search_keywords_or:
|
||||||
|
keywords = Keyword.objects.filter(space=space).annotate(recipe_count=Count('recipe'))
|
||||||
|
else:
|
||||||
|
keywords = Keyword.objects.filter(recipe__in=qs, space=space).annotate(recipe_count=Count('recipe'))
|
||||||
# custom django-tree function annotates a queryset to make building a tree easier.
|
# custom django-tree function annotates a queryset to make building a tree easier.
|
||||||
# see https://django-treebeard.readthedocs.io/en/latest/api.html#treebeard.models.Node.get_annotated_list_qs for details
|
# see https://django-treebeard.readthedocs.io/en/latest/api.html#treebeard.models.Node.get_annotated_list_qs for details
|
||||||
kw_a = annotated_qs(kws, root=True, fill=True)
|
kw_a = annotated_qs(keywords, root=True, fill=True)
|
||||||
|
|
||||||
# TODO add rating facet
|
# TODO add rating facet
|
||||||
facets['Ratings'] = []
|
facets['Ratings'] = []
|
||||||
facets['Keywords'] = fill_annotated_parents(kw_a, keyword_list)
|
facets['Keywords'] = fill_annotated_parents(kw_a, keyword_list)
|
||||||
# TODO add food facet
|
# TODO add food facet
|
||||||
facets['Ingredients'] = []
|
facets['Foods'] = []
|
||||||
# TODO add book facet
|
# TODO add book facet
|
||||||
facets['Books'] = []
|
facets['Books'] = []
|
||||||
|
|
||||||
@ -199,7 +209,7 @@ def fill_annotated_parents(annotation, filters):
|
|||||||
|
|
||||||
annotation[i][1]['id'] = r[0].id
|
annotation[i][1]['id'] = r[0].id
|
||||||
annotation[i][1]['name'] = r[0].name
|
annotation[i][1]['name'] = r[0].name
|
||||||
annotation[i][1]['count'] = getattr(r[0], 'kw_count', 0)
|
annotation[i][1]['count'] = getattr(r[0], 'recipe_count', 0)
|
||||||
annotation[i][1]['isDefaultExpanded'] = False
|
annotation[i][1]['isDefaultExpanded'] = False
|
||||||
|
|
||||||
if str(r[0].id) in filters:
|
if str(r[0].id) in filters:
|
||||||
@ -217,7 +227,7 @@ def fill_annotated_parents(annotation, filters):
|
|||||||
|
|
||||||
while j < level:
|
while j < level:
|
||||||
# this causes some double counting when a recipe has both a child and an ancestor
|
# this causes some double counting when a recipe has both a child and an ancestor
|
||||||
annotation[parent[j]][1]['count'] += getattr(r[0], 'kw_count', 0)
|
annotation[parent[j]][1]['count'] += getattr(r[0], 'recipe_count', 0)
|
||||||
if expand:
|
if expand:
|
||||||
annotation[parent[j]][1]['isDefaultExpanded'] = True
|
annotation[parent[j]][1]['isDefaultExpanded'] = True
|
||||||
j += 1
|
j += 1
|
||||||
|
@ -5,7 +5,6 @@ import uuid
|
|||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
|
|
||||||
from annoying.fields import AutoOneToOneField
|
from annoying.fields import AutoOneToOneField
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib import auth
|
from django.contrib import auth
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth.models import Group, User
|
||||||
from django.contrib.postgres.indexes import GinIndex
|
from django.contrib.postgres.indexes import GinIndex
|
||||||
@ -38,16 +37,14 @@ def get_model_name(model):
|
|||||||
|
|
||||||
|
|
||||||
class TreeManager(MP_NodeManager):
|
class TreeManager(MP_NodeManager):
|
||||||
def get_or_create(self, **kwargs):
|
|
||||||
# model.Manager get_or_create() is not compatible with MP_Tree
|
# model.Manager get_or_create() is not compatible with MP_Tree
|
||||||
|
def get_or_create(self, **kwargs):
|
||||||
kwargs['name'] = kwargs['name'].strip()
|
kwargs['name'] = kwargs['name'].strip()
|
||||||
q = self.filter(name__iexact=kwargs['name'], space=kwargs['space'])
|
try:
|
||||||
if len(q) != 0:
|
return self.get(name__iexact=kwargs['name'], space=kwargs['space']), False
|
||||||
return q[0], False
|
except self.model.DoesNotExist:
|
||||||
else:
|
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
node = self.model.add_root(**kwargs)
|
return self.model.add_root(**kwargs), True
|
||||||
return node, True
|
|
||||||
|
|
||||||
|
|
||||||
class PermissionModelMixin:
|
class PermissionModelMixin:
|
||||||
|
@ -63,7 +63,6 @@ class RecipeSchema(AutoSchema):
|
|||||||
|
|
||||||
|
|
||||||
class TreeSchema(AutoSchema):
|
class TreeSchema(AutoSchema):
|
||||||
|
|
||||||
def get_path_parameters(self, path, method):
|
def get_path_parameters(self, path, method):
|
||||||
if not is_list_view(path, method, self.view):
|
if not is_list_view(path, method, self.view):
|
||||||
return super(TreeSchema, self).get_path_parameters(path, method)
|
return super(TreeSchema, self).get_path_parameters(path, method)
|
||||||
@ -85,5 +84,4 @@ class TreeSchema(AutoSchema):
|
|||||||
"description": 'Return all self and children of {} with ID [int].'.format(api_name),
|
"description": 'Return all self and children of {} with ID [int].'.format(api_name),
|
||||||
'schema': {'type': 'int', },
|
'schema': {'type': 'int', },
|
||||||
})
|
})
|
||||||
|
|
||||||
return parameters
|
return parameters
|
||||||
|
@ -3,12 +3,11 @@ from decimal import Decimal
|
|||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db.models import QuerySet, Sum, Avg
|
from django.db.models import Avg, QuerySet, Sum
|
||||||
from drf_writable_nested import (UniqueFieldsMixin,
|
from drf_writable_nested import (UniqueFieldsMixin,
|
||||||
WritableNestedModelSerializer)
|
WritableNestedModelSerializer)
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError, NotFound
|
from rest_framework.exceptions import ValidationError, NotFound
|
||||||
from treebeard.mp_tree import MP_NodeQuerySet
|
|
||||||
|
|
||||||
from cookbook.models import (Comment, CookLog, Food, Ingredient, Keyword,
|
from cookbook.models import (Comment, CookLog, Food, Ingredient, Keyword,
|
||||||
MealPlan, MealType, NutritionInformation, Recipe,
|
MealPlan, MealType, NutritionInformation, Recipe,
|
||||||
@ -210,8 +209,8 @@ class KeywordSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
|
|||||||
|
|
||||||
def get_image(self, obj):
|
def get_image(self, obj):
|
||||||
recipes = obj.recipe_set.all().filter(space=obj.space).exclude(image__isnull=True).exclude(image__exact='')
|
recipes = obj.recipe_set.all().filter(space=obj.space).exclude(image__isnull=True).exclude(image__exact='')
|
||||||
if len(recipes) == 0:
|
if len(recipes) == 0 and obj.has_children():
|
||||||
recipes = Recipe.objects.filter(keywords__in=obj.get_tree(), space=obj.space).exclude(image__isnull=True).exclude(image__exact='') # if no recipes found - check whole tree
|
recipes = Recipe.objects.filter(keywords__in=obj.get_descendants(), space=obj.space).exclude(image__isnull=True).exclude(image__exact='') # if no recipes found - check whole tree
|
||||||
if len(recipes) != 0:
|
if len(recipes) != 0:
|
||||||
return random.choice(recipes).image.url
|
return random.choice(recipes).image.url
|
||||||
else:
|
else:
|
||||||
@ -229,7 +228,6 @@ class KeywordSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
# list_serializer_class = SpaceFilterSerializer
|
|
||||||
model = Keyword
|
model = Keyword
|
||||||
fields = ('id', 'name', 'icon', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe', 'created_at', 'updated_at')
|
fields = ('id', 'name', 'icon', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe', 'created_at', 'updated_at')
|
||||||
read_only_fields = ('id', 'numchild', 'parent', 'image')
|
read_only_fields = ('id', 'numchild', 'parent', 'image')
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -22,7 +22,6 @@ MOVE_URL = 'api:keyword-move'
|
|||||||
MERGE_URL = 'api:keyword-merge'
|
MERGE_URL = 'api:keyword-merge'
|
||||||
|
|
||||||
|
|
||||||
# TODO are there better ways to manage these fixtures?
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def obj_1(space_1):
|
def obj_1(space_1):
|
||||||
return Keyword.objects.get_or_create(name='test_1', space=space_1)[0]
|
return Keyword.objects.get_or_create(name='test_1', space=space_1)[0]
|
||||||
@ -352,7 +351,6 @@ def test_merge(
|
|||||||
|
|
||||||
|
|
||||||
def test_root_filter(obj_1, obj_1_1, obj_1_1_1, obj_2, obj_3, u1_s1):
|
def test_root_filter(obj_1, obj_1_1, obj_1_1_1, obj_2, obj_3, u1_s1):
|
||||||
|
|
||||||
# should return root objects in the space (obj_1, obj_2), ignoring query filters
|
# should return root objects in the space (obj_1, obj_2), ignoring query filters
|
||||||
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?root=0').content)
|
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?root=0').content)
|
||||||
assert len(response['results']) == 2
|
assert len(response['results']) == 2
|
||||||
@ -367,7 +365,6 @@ def test_root_filter(obj_1, obj_1_1, obj_1_1_1, obj_2, obj_3, u1_s1):
|
|||||||
|
|
||||||
|
|
||||||
def test_tree_filter(obj_1, obj_1_1, obj_1_1_1, obj_2, obj_3, u1_s1):
|
def test_tree_filter(obj_1, obj_1_1, obj_1_1_1, obj_2, obj_3, u1_s1):
|
||||||
|
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
obj_2.move(obj_1, 'sorted-child')
|
obj_2.move(obj_1, 'sorted-child')
|
||||||
# should return full tree starting at obj_1 (obj_1_1_1, obj_2), ignoring query filters
|
# should return full tree starting at obj_1 (obj_1_1_1, obj_2), ignoring query filters
|
||||||
|
@ -109,9 +109,9 @@ urlpatterns = [
|
|||||||
path('api/ingredient-from-string/', api.ingredient_from_string, name='api_ingredient_from_string'),
|
path('api/ingredient-from-string/', api.ingredient_from_string, name='api_ingredient_from_string'),
|
||||||
path('api/share-link/<int:pk>', api.share_link, name='api_share_link'),
|
path('api/share-link/<int:pk>', api.share_link, name='api_share_link'),
|
||||||
|
|
||||||
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'),
|
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'), # TODO is this deprecated?
|
||||||
path('dal/food/', dal.IngredientsAutocomplete.as_view(), name='dal_food'),
|
path('dal/food/', dal.IngredientsAutocomplete.as_view(), name='dal_food'), # TODO is this deprecated?
|
||||||
path('dal/unit/', dal.UnitAutocomplete.as_view(), name='dal_unit'),
|
path('dal/unit/', dal.UnitAutocomplete.as_view(), name='dal_unit'), # TODO is this deprecated?
|
||||||
|
|
||||||
path('telegram/setup/<int:pk>', telegram.setup_bot, name='telegram_setup'),
|
path('telegram/setup/<int:pk>', telegram.setup_bot, name='telegram_setup'),
|
||||||
path('telegram/remove/<int:pk>', telegram.remove_bot, name='telegram_remove'),
|
path('telegram/remove/<int:pk>', telegram.remove_bot, name='telegram_remove'),
|
||||||
@ -137,7 +137,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
generic_models = (
|
generic_models = (
|
||||||
Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync,
|
Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync,
|
||||||
Comment, RecipeBookEntry, Keyword, Food, ShoppingList, InviteLink
|
Comment, RecipeBookEntry, Food, ShoppingList, InviteLink
|
||||||
)
|
)
|
||||||
|
|
||||||
for m in generic_models:
|
for m in generic_models:
|
||||||
|
@ -108,8 +108,9 @@ class FuzzyFilterMixin(ViewSetMixin):
|
|||||||
if fuzzy:
|
if fuzzy:
|
||||||
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name', query)).filter(trigram__gt=0.2).order_by("-trigram")
|
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name', query)).filter(trigram__gt=0.2).order_by("-trigram")
|
||||||
else:
|
else:
|
||||||
# TODO have this check unaccent search settings?
|
# TODO have this check unaccent search settings or other search preferences?
|
||||||
self.queryset = self.queryset.filter(name__icontains=query)
|
# TODO for some querysets exact matches are sorted beyond pagesize, need to find better solution
|
||||||
|
self.queryset = self.queryset.filter(name__istartswith=query) | self.queryset.filter(name__icontains=query)
|
||||||
|
|
||||||
updated_at = self.request.query_params.get('updated_at', None)
|
updated_at = self.request.query_params.get('updated_at', None)
|
||||||
if updated_at is not None:
|
if updated_at is not None:
|
||||||
@ -144,14 +145,14 @@ class TreeMixin(FuzzyFilterMixin):
|
|||||||
except self.model.DoesNotExist:
|
except self.model.DoesNotExist:
|
||||||
self.queryset = self.model.objects.none()
|
self.queryset = self.model.objects.none()
|
||||||
if root == 0:
|
if root == 0:
|
||||||
self.queryset = self.model.get_root_nodes() | self.model.objects.filter(depth=0)
|
self.queryset = self.model.get_root_nodes()
|
||||||
else:
|
else:
|
||||||
self.queryset = self.model.objects.get(id=root).get_children()
|
self.queryset = self.model.objects.get(id=root).get_children()
|
||||||
elif tree:
|
elif tree:
|
||||||
if tree.isnumeric():
|
if tree.isnumeric():
|
||||||
try:
|
try:
|
||||||
self.queryset = self.model.objects.get(id=int(tree)).get_descendants_and_self()
|
self.queryset = self.model.objects.get(id=int(tree)).get_descendants_and_self()
|
||||||
except Keyword.DoesNotExist:
|
except self.model.DoesNotExist:
|
||||||
self.queryset = self.model.objects.none()
|
self.queryset = self.model.objects.none()
|
||||||
else:
|
else:
|
||||||
return super().get_queryset()
|
return super().get_queryset()
|
||||||
@ -466,7 +467,7 @@ class RecipePagination(PageNumberPagination):
|
|||||||
max_page_size = 100
|
max_page_size = 100
|
||||||
|
|
||||||
def paginate_queryset(self, queryset, request, view=None):
|
def paginate_queryset(self, queryset, request, view=None):
|
||||||
self.facets = get_facet(queryset, request.query_params)
|
self.facets = get_facet(queryset, request.query_params, request.space)
|
||||||
return super().paginate_queryset(queryset, request, view)
|
return super().paginate_queryset(queryset, request, view)
|
||||||
|
|
||||||
def get_paginated_response(self, data):
|
def get_paginated_response(self, data):
|
||||||
|
@ -148,7 +148,6 @@ def import_url(request):
|
|||||||
|
|
||||||
recipe.steps.add(step)
|
recipe.steps.add(step)
|
||||||
|
|
||||||
all_keywords = Keyword.get_tree()
|
|
||||||
for kw in data['keywords']:
|
for kw in data['keywords']:
|
||||||
k, created = Keyword.objects.get_or_create(name=kw['text'], space=request.space)
|
k, created = Keyword.objects.get_or_create(name=kw['text'], space=request.space)
|
||||||
recipe.keywords.add(k)
|
recipe.keywords.add(k)
|
||||||
|
@ -56,9 +56,7 @@ def index(request):
|
|||||||
return HttpResponseRedirect(reverse('view_search'))
|
return HttpResponseRedirect(reverse('view_search'))
|
||||||
|
|
||||||
|
|
||||||
# faceting
|
# TODO need to deprecate
|
||||||
# unaccent / likely will perform full table scan
|
|
||||||
# create tests
|
|
||||||
def search(request):
|
def search(request):
|
||||||
if has_group_permission(request.user, ('guest',)):
|
if has_group_permission(request.user, ('guest',)):
|
||||||
if request.user.userpreference.search_style == UserPreference.NEW:
|
if request.user.userpreference.search_style == UserPreference.NEW:
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
<div class="col-xl-8 col-12">
|
<div class="col-xl-8 col-12">
|
||||||
<!-- TODO only show scollbars in split mode, but this doesn't interact well with infinite scroll, maybe a different component? -->
|
<!-- TODO only show scollbars in split mode, but this doesn't interact well with infinite scroll, maybe a different component? -->
|
||||||
<div class="container-fluid d-flex flex-column flex-grow-1" :class="{'vh-100' : show_split}">
|
<div class="container-fluid d-flex flex-column flex-grow-1" :class="{'vh-100' : show_split}">
|
||||||
<!-- <div class="container-fluid d-flex flex-column flex-grow-1 vh-100"> -->
|
|
||||||
<!-- expanded options box -->
|
<!-- expanded options box -->
|
||||||
<div class="row flex-shrink-0">
|
<div class="row flex-shrink-0">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
|
@ -143,12 +143,6 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<b-input-group class="mt-2">
|
<b-input-group class="mt-2">
|
||||||
<!-- <generic-multiselect @change="genericSelectChanged" parent_variable="search_keywords"
|
|
||||||
:initial_selection="settings.search_keywords"
|
|
||||||
search_function="listKeywords" label="label"
|
|
||||||
:tree_api="true"
|
|
||||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
|
||||||
v-bind:placeholder="$t('Keywords')"></generic-multiselect> -->
|
|
||||||
<treeselect v-model="settings.search_keywords" :options="facets.Keywords" :flat="true"
|
<treeselect v-model="settings.search_keywords" :options="facets.Keywords" :flat="true"
|
||||||
searchNested multiple :placeholder="$t('Keywords')" :normalizer="normalizer"
|
searchNested multiple :placeholder="$t('Keywords')" :normalizer="normalizer"
|
||||||
@input="refreshData(false)"
|
@input="refreshData(false)"
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'KeywordContextMenu',
|
name: 'GenericContextMenu',
|
||||||
props: {
|
props: {
|
||||||
show_edit: {type: Boolean, default: true},
|
show_edit: {type: Boolean, default: true},
|
||||||
show_delete: {type: Boolean, default: true},
|
show_delete: {type: Boolean, default: true},
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<b-dropdown variant="link" toggle-class="text-decoration-none" no-caret>
|
|
||||||
<template #button-content>
|
|
||||||
<i class="fas fa-ellipsis-v" ></i>
|
|
||||||
</template>
|
|
||||||
<b-dropdown-item v-on:click="$emit('item-action', 'edit')" v-if="show_edit">
|
|
||||||
<i class="fas fa-pencil-alt fa-fw"></i> {{ $t('Edit') }}
|
|
||||||
</b-dropdown-item>
|
|
||||||
|
|
||||||
<b-dropdown-item v-on:click="$emit('item-action', 'delete')" v-if="show_delete">
|
|
||||||
<i class="fas fa-trash-alt fa-fw"></i> {{ $t('Delete') }}
|
|
||||||
</b-dropdown-item>
|
|
||||||
|
|
||||||
<b-dropdown-item v-on:click="$emit('item-action', 'move')" v-if="show_move">
|
|
||||||
<i class="fas fa-trash-alt fa-fw"></i> {{ $t('Move') }}
|
|
||||||
</b-dropdown-item>
|
|
||||||
|
|
||||||
<b-dropdown-item v-if="show_merge" v-on:click="$emit('item-action', 'merge')">
|
|
||||||
<i class="fas fa-trash-alt fa-fw"></i> {{ $t('Merge') }}
|
|
||||||
</b-dropdown-item>
|
|
||||||
|
|
||||||
</b-dropdown>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'KeywordContextMenu',
|
|
||||||
props: {
|
|
||||||
show_edit: {type: Boolean, default: true},
|
|
||||||
show_delete: {type: Boolean, default: true},
|
|
||||||
show_move: {type: Boolean, default: false},
|
|
||||||
show_merge: {type: Boolean, default: false},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -36,10 +36,7 @@ export default {
|
|||||||
search_function: String,
|
search_function: String,
|
||||||
label: String,
|
label: String,
|
||||||
parent_variable: {type: String, default: undefined},
|
parent_variable: {type: String, default: undefined},
|
||||||
limit: {
|
limit: {type: Number, default: 10,},
|
||||||
type: Number,
|
|
||||||
default: 10,
|
|
||||||
},
|
|
||||||
sticky_options: {type:Array, default(){return []}},
|
sticky_options: {type:Array, default(){return []}},
|
||||||
initial_selection: {type:Array, default(){return []}},
|
initial_selection: {type:Array, default(){return []}},
|
||||||
multiple: {type: Boolean, default: true},
|
multiple: {type: Boolean, default: true},
|
||||||
|
@ -80,7 +80,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// TODO make this conditional on .env DEBUG = TRUE
|
// TODO make this conditional on .env DEBUG = FALSE
|
||||||
config.optimization.minimize(true)
|
config.optimization.minimize(true)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
{"status":"done","chunks":{"recipe_search_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/recipe_search_view.js"],"recipe_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/recipe_view.js"],"offline_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/offline_view.js"],"import_response_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/import_response_view.js"],"supermarket_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/supermarket_view.js"],"user_file_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/user_file_view.js"],"keyword_list_view":["css/chunk-vendors.css","js/chunk-vendors.js","css/keyword_list_view.css","js/keyword_list_view.js"]},"assets":{"../../templates/sw.js":{"name":"../../templates/sw.js","path":"..\\..\\templates\\sw.js"},"css/chunk-vendors.css":{"name":"css/chunk-vendors.css","path":"css\\chunk-vendors.css"},"js/chunk-vendors.js":{"name":"js/chunk-vendors.js","path":"js\\chunk-vendors.js"},"js/import_response_view.js":{"name":"js/import_response_view.js","path":"js\\import_response_view.js"},"css/keyword_list_view.css":{"name":"css/keyword_list_view.css","path":"css\\keyword_list_view.css"},"js/keyword_list_view.js":{"name":"js/keyword_list_view.js","path":"js\\keyword_list_view.js"},"js/offline_view.js":{"name":"js/offline_view.js","path":"js\\offline_view.js"},"js/recipe_search_view.js":{"name":"js/recipe_search_view.js","path":"js\\recipe_search_view.js"},"js/recipe_view.js":{"name":"js/recipe_view.js","path":"js\\recipe_view.js"},"js/supermarket_view.js":{"name":"js/supermarket_view.js","path":"js\\supermarket_view.js"},"js/user_file_view.js":{"name":"js/user_file_view.js","path":"js\\user_file_view.js"},"recipe_search_view.html":{"name":"recipe_search_view.html","path":"recipe_search_view.html"},"recipe_view.html":{"name":"recipe_view.html","path":"recipe_view.html"},"offline_view.html":{"name":"offline_view.html","path":"offline_view.html"},"import_response_view.html":{"name":"import_response_view.html","path":"import_response_view.html"},"supermarket_view.html":{"name":"supermarket_view.html","path":"supermarket_view.html"},"user_file_view.html":{"name":"user_file_view.html","path":"user_file_view.html"},"keyword_list_view.html":{"name":"keyword_list_view.html","path":"keyword_list_view.html"},"manifest.json":{"name":"manifest.json","path":"manifest.json"}}}
|
{"status":"done","chunks":{"recipe_search_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/recipe_search_view.js"],"recipe_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/recipe_view.js"],"offline_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/offline_view.js"],"import_response_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/import_response_view.js"],"supermarket_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/supermarket_view.js"],"user_file_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/user_file_view.js"],"keyword_list_view":["css/chunk-vendors.css","js/chunk-vendors.js","css/keyword_list_view.css","js/keyword_list_view.js"]},"assets":{"../../templates/sw.js":{"name":"../../templates/sw.js","path":"../../templates/sw.js"},"css/chunk-vendors.css":{"name":"css/chunk-vendors.css","path":"css/chunk-vendors.css"},"js/chunk-vendors.js":{"name":"js/chunk-vendors.js","path":"js/chunk-vendors.js"},"js/import_response_view.js":{"name":"js/import_response_view.js","path":"js/import_response_view.js"},"css/keyword_list_view.css":{"name":"css/keyword_list_view.css","path":"css/keyword_list_view.css"},"js/keyword_list_view.js":{"name":"js/keyword_list_view.js","path":"js/keyword_list_view.js"},"js/offline_view.js":{"name":"js/offline_view.js","path":"js/offline_view.js"},"js/recipe_search_view.js":{"name":"js/recipe_search_view.js","path":"js/recipe_search_view.js"},"js/recipe_view.js":{"name":"js/recipe_view.js","path":"js/recipe_view.js"},"js/supermarket_view.js":{"name":"js/supermarket_view.js","path":"js/supermarket_view.js"},"js/user_file_view.js":{"name":"js/user_file_view.js","path":"js/user_file_view.js"},"recipe_search_view.html":{"name":"recipe_search_view.html","path":"recipe_search_view.html"},"recipe_view.html":{"name":"recipe_view.html","path":"recipe_view.html"},"offline_view.html":{"name":"offline_view.html","path":"offline_view.html"},"import_response_view.html":{"name":"import_response_view.html","path":"import_response_view.html"},"supermarket_view.html":{"name":"supermarket_view.html","path":"supermarket_view.html"},"user_file_view.html":{"name":"user_file_view.html","path":"user_file_view.html"},"keyword_list_view.html":{"name":"keyword_list_view.html","path":"keyword_list_view.html"},"manifest.json":{"name":"manifest.json","path":"manifest.json"}}}
|
Reference in New Issue
Block a user