various fixes

This commit is contained in:
smilerz 2022-02-08 08:13:16 -06:00
parent f1bbe16606
commit 88b3ba1427
No known key found for this signature in database
GPG Key ID: 39444C7606D47126
9 changed files with 201 additions and 248 deletions

View File

@ -4,7 +4,7 @@ from datetime import timedelta
from django.contrib.postgres.search import SearchQuery, SearchRank, TrigramSimilarity from django.contrib.postgres.search import SearchQuery, SearchRank, TrigramSimilarity
from django.core.cache import caches from django.core.cache import caches
from django.db.models import Avg, Case, Count, F, Func, Max, OuterRef, Q, Subquery, Sum, Value, When from django.db.models import Avg, Case, Count, F, Func, Max, OuterRef, Q, Subquery, Sum, Value, When
from django.db.models.functions import Coalesce, Substr from django.db.models.functions import Coalesce, Lower, Substr
from django.utils import timezone, translation from django.utils import timezone, translation
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
@ -666,9 +666,9 @@ class RecipeFacet():
if not self._request.space.demo and self._request.space.show_facet_count: if not self._request.space.demo and self._request.space.show_facet_count:
return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('keywords', depth, steplen)), 0) return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('keywords', depth, steplen)), 0)
).filter(depth=depth, count__gt=0 ).filter(depth=depth, count__gt=0
).values('id', 'name', 'count', 'numchild').order_by('name')[:200] ).values('id', 'name', 'count', 'numchild').order_by(Lower('name').asc())[:200]
else: else:
return queryset.filter(depth=depth).values('id', 'name', 'numchild').order_by('name') return queryset.filter(depth=depth).values('id', 'name', 'numchild').order_by(Lower('name').asc())
def _food_queryset(self, queryset, food=None): def _food_queryset(self, queryset, food=None):
depth = getattr(food, 'depth', 0) + 1 depth = getattr(food, 'depth', 0) + 1
@ -677,9 +677,9 @@ class RecipeFacet():
if not self._request.space.demo and self._request.space.show_facet_count: if not self._request.space.demo and self._request.space.show_facet_count:
return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('steps__ingredients__food', depth, steplen)), 0) return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('steps__ingredients__food', depth, steplen)), 0)
).filter(depth__lte=depth, count__gt=0 ).filter(depth__lte=depth, count__gt=0
).values('id', 'name', 'count', 'numchild').order_by('name')[:200] ).values('id', 'name', 'count', 'numchild').order_by(Lower('name').asc())[:200]
else: else:
return queryset.filter(depth__lte=depth).values('id', 'name', 'numchild').order_by('name') return queryset.filter(depth__lte=depth).values('id', 'name', 'numchild').order_by(Lower('name').asc())
# # TODO: This might be faster https://github.com/django-treebeard/django-treebeard/issues/115 # # TODO: This might be faster https://github.com/django-treebeard/django-treebeard/issues/115
@ -909,7 +909,7 @@ def old_search(request):
params = dict(request.GET) params = dict(request.GET)
params['internal'] = None params['internal'] = None
f = RecipeFilter(params, f = RecipeFilter(params,
queryset=Recipe.objects.filter(space=request.user.userpreference.space).all().order_by('name'), queryset=Recipe.objects.filter(space=request.user.userpreference.space).all().order_by(Lower('name').asc()),
space=request.space) space=request.space)
return f.qs return f.qs

View File

@ -65,9 +65,13 @@ class RecipeShoppingEditor():
except (ValueError, TypeError): except (ValueError, TypeError):
self.servings = getattr(self._shopping_list_recipe, 'servings', None) or getattr(self.mealplan, 'servings', None) or getattr(self.recipe, 'servings', None) self.servings = getattr(self._shopping_list_recipe, 'servings', None) or getattr(self.mealplan, 'servings', None) or getattr(self.recipe, 'servings', None)
@property
def _recipe_servings(self):
return getattr(self.recipe, 'servings', None) or getattr(getattr(self.mealplan, 'recipe', None), 'servings', None) or getattr(getattr(self._shopping_list_recipe, 'recipe', None), 'servings', None)
@property @property
def _servings_factor(self): def _servings_factor(self):
return self.servings / self.recipe.servings return Decimal(self.servings)/Decimal(self._recipe_servings)
@property @property
def _shared_users(self): def _shared_users(self):

View File

@ -14,8 +14,8 @@ from rest_framework.fields import empty
from cookbook.helper.HelperFunctions import str2bool from cookbook.helper.HelperFunctions import str2bool
from cookbook.helper.shopping_helper import RecipeShoppingEditor from cookbook.helper.shopping_helper import RecipeShoppingEditor
from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Food, from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, ExportLog, Food,
FoodInheritField, ImportLog, ExportLog, Ingredient, Keyword, MealPlan, MealType, FoodInheritField, ImportLog, Ingredient, Keyword, MealPlan, MealType,
NutritionInformation, Recipe, RecipeBook, RecipeBookEntry, NutritionInformation, Recipe, RecipeBook, RecipeBookEntry,
RecipeImport, ShareLink, ShoppingList, ShoppingListEntry, RecipeImport, ShareLink, ShoppingList, ShoppingListEntry,
ShoppingListRecipe, Step, Storage, Supermarket, SupermarketCategory, ShoppingListRecipe, Step, Storage, Supermarket, SupermarketCategory,
@ -677,7 +677,6 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
read_only_fields = ('created_by',) read_only_fields = ('created_by',)
# TODO deprecate
class ShoppingListRecipeSerializer(serializers.ModelSerializer): class ShoppingListRecipeSerializer(serializers.ModelSerializer):
name = serializers.SerializerMethodField('get_name') # should this be done at the front end? name = serializers.SerializerMethodField('get_name') # should this be done at the front end?
recipe_name = serializers.ReadOnlyField(source='recipe.name') recipe_name = serializers.ReadOnlyField(source='recipe.name')
@ -689,11 +688,11 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
value = Decimal(value) value = Decimal(value)
value = value.quantize(Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero value = value.quantize(Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
return ( return (
obj.name obj.name
or getattr(obj.mealplan, 'title', None) or getattr(obj.mealplan, 'title', None)
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)]) or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
or obj.recipe.name or obj.recipe.name
) + f' ({value:.2g})' ) + f' ({value:.2g})'
def update(self, instance, validated_data): def update(self, instance, validated_data):
# TODO remove once old shopping list # TODO remove once old shopping list

View File

@ -75,7 +75,7 @@
<ul class="navbar-nav mr-auto"> <ul class="navbar-nav mr-auto">
<li class="nav-item {% if request.resolver_match.url_name in 'view_search' %}active{% endif %}"> <li class="nav-item {% if request.resolver_match.url_name in 'view_search' %}active{% endif %}">
<a class="nav-link" href="{% url 'view_search' %}"><i <a class="nav-link" href="{% url 'view_search' %}"><i
class="fas fa-book"></i> {% trans 'Cookbook' %}</a> class="fas fa-book"></i> {% trans 'Recipes' %}</a>
</li> </li>
<li class="nav-item {% if request.resolver_match.url_name in 'view_plan' %}active{% endif %}"> <li class="nav-item {% if request.resolver_match.url_name in 'view_plan' %}active{% endif %}">
<a class="nav-link" href="{% url 'view_plan' %}"><i <a class="nav-link" href="{% url 'view_plan' %}"><i

View File

@ -15,7 +15,7 @@ from django.core.files import File
from django.db.models import (Case, Count, Exists, F, IntegerField, OuterRef, ProtectedError, Q, from django.db.models import (Case, Count, Exists, F, IntegerField, OuterRef, ProtectedError, Q,
Subquery, Value, When) Subquery, Value, When)
from django.db.models.fields.related import ForeignObjectRel from django.db.models.fields.related import ForeignObjectRel
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce, Lower
from django.http import FileResponse, HttpResponse, JsonResponse from django.http import FileResponse, HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse from django.urls import reverse
@ -42,19 +42,19 @@ from cookbook.helper.recipe_html_import import get_recipe_from_source
from cookbook.helper.recipe_search import RecipeFacet, RecipeSearch, old_search from cookbook.helper.recipe_search import RecipeFacet, RecipeSearch, old_search
from cookbook.helper.recipe_url_import import get_from_scraper from cookbook.helper.recipe_url_import import get_from_scraper
from cookbook.helper.shopping_helper import RecipeShoppingEditor, shopping_helper from cookbook.helper.shopping_helper import RecipeShoppingEditor, shopping_helper
from cookbook.models import (Automation, BookmarkletImport, CookLog, Food, FoodInheritField, from cookbook.models import (Automation, BookmarkletImport, CookLog, ExportLog, Food,
ImportLog, ExportLog, Ingredient, Keyword, MealPlan, MealType, Recipe, RecipeBook, FoodInheritField, ImportLog, Ingredient, Keyword, MealPlan, MealType,
RecipeBookEntry, ShareLink, ShoppingList, ShoppingListEntry, Recipe, RecipeBook, RecipeBookEntry, ShareLink, ShoppingList,
ShoppingListRecipe, Step, Storage, Supermarket, SupermarketCategory, ShoppingListEntry, ShoppingListRecipe, Step, Storage, Supermarket,
SupermarketCategoryRelation, Sync, SyncLog, Unit, UserFile, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, Unit,
UserPreference, ViewLog) UserFile, UserPreference, ViewLog)
from cookbook.provider.dropbox import Dropbox from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud from cookbook.provider.nextcloud import Nextcloud
from cookbook.schemas import FilterSchema, QueryParam, QueryParamAutoSchema, TreeSchema from cookbook.schemas import FilterSchema, QueryParam, QueryParamAutoSchema, TreeSchema
from cookbook.serializer import (AutomationSerializer, BookmarkletImportSerializer, from cookbook.serializer import (AutomationSerializer, BookmarkletImportSerializer,
CookLogSerializer, FoodInheritFieldSerializer, FoodSerializer, CookLogSerializer, ExportLogSerializer, FoodInheritFieldSerializer,
FoodShoppingUpdateSerializer, ImportLogSerializer, ExportLogSerializer, FoodSerializer, FoodShoppingUpdateSerializer, ImportLogSerializer,
IngredientSerializer, KeywordSerializer, MealPlanSerializer, IngredientSerializer, KeywordSerializer, MealPlanSerializer,
MealTypeSerializer, RecipeBookEntrySerializer, MealTypeSerializer, RecipeBookEntrySerializer,
RecipeBookSerializer, RecipeImageSerializer, RecipeBookSerializer, RecipeImageSerializer,
@ -138,7 +138,7 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
schema = FilterSchema() schema = FilterSchema()
def get_queryset(self): def get_queryset(self):
self.queryset = self.queryset.filter(space=self.request.space).order_by('name') self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc())
query = self.request.query_params.get('query', None) query = self.request.query_params.get('query', None)
fuzzy = self.request.user.searchpreference.lookup fuzzy = self.request.user.searchpreference.lookup
@ -161,7 +161,7 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
self.queryset self.queryset
.annotate(starts=Case(When(name__istartswith=query, then=(Value(100))), .annotate(starts=Case(When(name__istartswith=query, then=(Value(100))),
default=Value(0))) # put exact matches at the top of the result set default=Value(0))) # put exact matches at the top of the result set
.filter(filter).order_by('-starts', 'name') .filter(filter).order_by('-starts', Lower('name').asc())
) )
updated_at = self.request.query_params.get('updated_at', None) updated_at = self.request.query_params.get('updated_at', None)
@ -175,9 +175,9 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
limit = self.request.query_params.get('limit', None) limit = self.request.query_params.get('limit', None)
random = self.request.query_params.get('random', False) random = self.request.query_params.get('random', False)
if random:
self.queryset = self.queryset.order_by("?")
if limit is not None: if limit is not None:
if random:
self.queryset = self.queryset.order_by("?")
self.queryset = self.queryset[:int(limit)] self.queryset = self.queryset[:int(limit)]
return self.annotate_recipe(queryset=self.queryset, request=self.request, serializer=self.serializer_class) return self.annotate_recipe(queryset=self.queryset, request=self.request, serializer=self.serializer_class)
@ -276,7 +276,7 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin, ExtendedRecipeMixin):
self.queryset = self.model.objects.none() self.queryset = self.model.objects.none()
else: else:
return self.annotate_recipe(queryset=super().get_queryset(), request=self.request, serializer=self.serializer_class, tree=True) return self.annotate_recipe(queryset=super().get_queryset(), request=self.request, serializer=self.serializer_class, tree=True)
self.queryset = self.queryset.filter(space=self.request.space).order_by('name') self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc())
return self.annotate_recipe(queryset=self.queryset, request=self.request, serializer=self.serializer_class, tree=True) return self.annotate_recipe(queryset=self.queryset, request=self.request, serializer=self.serializer_class, tree=True)
@ -404,7 +404,7 @@ class SupermarketCategoryViewSet(viewsets.ModelViewSet, StandardFilterMixin):
permission_classes = [CustomIsUser] permission_classes = [CustomIsUser]
def get_queryset(self): def get_queryset(self):
self.queryset = self.queryset.filter(space=self.request.space).order_by('name') self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc())
return super().get_queryset() return super().get_queryset()
@ -873,7 +873,6 @@ class ExportLogViewSet(viewsets.ModelViewSet):
return self.queryset.filter(space=self.request.space) return self.queryset.filter(space=self.request.space)
class BookmarkletImportViewSet(viewsets.ModelViewSet): class BookmarkletImportViewSet(viewsets.ModelViewSet):
queryset = BookmarkletImport.objects queryset = BookmarkletImport.objects
serializer_class = BookmarkletImportSerializer serializer_class = BookmarkletImportSerializer

View File

@ -60,7 +60,7 @@ def search(request):
if request.user.userpreference.search_style == UserPreference.NEW: if request.user.userpreference.search_style == UserPreference.NEW:
return search_v2(request) return search_v2(request)
f = RecipeFilter(request.GET, f = RecipeFilter(request.GET,
queryset=Recipe.objects.filter(space=request.user.userpreference.space).all().order_by('name'), queryset=Recipe.objects.filter(space=request.user.userpreference.space).all().order_by(Lower('name').asc()),
space=request.space) space=request.space)
if request.user.userpreference.search_style == UserPreference.LARGE: if request.user.userpreference.search_style == UserPreference.LARGE:
table = RecipeTable(f.qs) table = RecipeTable(f.qs)
@ -448,7 +448,7 @@ def history(request):
def system(request): def system(request):
if not request.user.is_superuser: if not request.user.is_superuser:
return HttpResponseRedirect(reverse('index')) return HttpResponseRedirect(reverse('index'))
postgres = False if ( postgres = False if (
settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql_psycopg2' # noqa: E501 settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql_psycopg2' # noqa: E501
or settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql' # noqa: E501 or settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql' # noqa: E501

View File

@ -1,16 +1,17 @@
<template> <template>
<div> <div>
<h3><i class="fas fa-edit"></i> <span v-if="recipe !== undefined">{{ recipe.name }}</span></h3> <h3>
<i class="fas fa-edit"></i> <span v-if="recipe !== undefined">{{ recipe.name }}</span>
</h3>
<loading-spinner :size="25" v-if="!recipe"></loading-spinner> <loading-spinner :size="25" v-if="!recipe"></loading-spinner>
<div v-if="recipe !== undefined"> <div v-if="recipe !== undefined">
<!-- Title and description --> <!-- Title and description -->
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<label for="id_name"> {{ $t("Name") }}</label> <label for="id_name"> {{ $t("Name") }}</label>
<input class="form-control" id="id_name" v-model="recipe.name"/> <input class="form-control" id="id_name" v-model="recipe.name" />
</div> </div>
</div> </div>
<div class="row pt-2"> <div class="row pt-2">
@ -18,16 +19,14 @@
<label for="id_description"> <label for="id_description">
{{ $t("Description") }} {{ $t("Description") }}
</label> </label>
<textarea id="id_description" class="form-control" v-model="recipe.description" <textarea id="id_description" class="form-control" v-model="recipe.description" maxlength="512"></textarea>
maxlength="512"></textarea>
</div> </div>
</div> </div>
<!-- Image and misc properties --> <!-- Image and misc properties -->
<div class="row pt-2"> <div class="row pt-2">
<div class="col-md-6" style="max-height: 50vh; min-height: 30vh"> <div class="col-md-6" style="max-height: 50vh; min-height: 30vh">
<input id="id_file_upload" ref="file_upload" type="file" hidden <input id="id_file_upload" ref="file_upload" type="file" hidden @change="uploadImage($event.target.files[0])" />
@change="uploadImage($event.target.files[0])"/>
<div <div
class="h-100 w-100 border border-primary rounded" class="h-100 w-100 border border-primary rounded"
@ -36,31 +35,26 @@
@dragover.prevent @dragover.prevent
@click="$refs.file_upload.click()" @click="$refs.file_upload.click()"
> >
<i class="far fa-image fa-10x text-primary" <i class="far fa-image fa-10x text-primary" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%)" v-if="!recipe.image"></i>
style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%)"
v-if="!recipe.image"></i>
<img :src="recipe.image" id="id_image" class="img img-fluid img-responsive" <img :src="recipe.image" id="id_image" class="img img-fluid img-responsive" style="object-fit: cover; height: 100%" v-if="recipe.image" />
style="object-fit: cover; height: 100%" v-if="recipe.image"/>
</div> </div>
<button style="bottom: 10px; left: 30px; position: absolute" class="btn btn-danger" <button style="bottom: 10px; left: 30px; position: absolute" class="btn btn-danger" @click="deleteImage" v-if="recipe.image">{{ $t("Delete") }}</button>
@click="deleteImage" v-if="recipe.image">{{ $t("Delete") }}
</button>
</div> </div>
<div class="col-md-6 mt-1"> <div class="col-md-6 mt-1">
<label for="id_name"> {{ $t("Preparation") }} {{ $t("Time") }} ({{ $t("min") }})</label> <label for="id_name"> {{ $t("Preparation") }} {{ $t("Time") }} ({{ $t("min") }})</label>
<input class="form-control" id="id_prep_time" v-model="recipe.working_time" type="number"/> <input class="form-control" id="id_prep_time" v-model="recipe.working_time" type="number" />
<br/> <br />
<label for="id_name"> {{ $t("Waiting") }} {{ $t("Time") }} ({{ $t("min") }})</label> <label for="id_name"> {{ $t("Waiting") }} {{ $t("Time") }} ({{ $t("min") }})</label>
<input class="form-control" id="id_wait_time" v-model="recipe.waiting_time" type="number"/> <input class="form-control" id="id_wait_time" v-model="recipe.waiting_time" type="number" />
<br/> <br />
<label for="id_name"> {{ $t("Servings") }}</label> <label for="id_name"> {{ $t("Servings") }}</label>
<input class="form-control" id="id_servings" v-model="recipe.servings" type="number"/> <input class="form-control" id="id_servings" v-model="recipe.servings" type="number" />
<br/> <br />
<label for="id_name"> {{ $t("Servings") }} {{ $t("Text") }}</label> <label for="id_name"> {{ $t("Servings") }} {{ $t("Text") }}</label>
<input class="form-control" id="id_servings_text" v-model="recipe.servings_text" maxlength="32"/> <input class="form-control" id="id_servings_text" v-model="recipe.servings_text" maxlength="32" />
<br/> <br />
<label for="id_name"> {{ $t("Keywords") }}</label> <label for="id_name"> {{ $t("Keywords") }}</label>
<multiselect <multiselect
v-model="recipe.keywords" v-model="recipe.keywords"
@ -123,26 +117,22 @@
<b-collapse id="id_nutrition_collapse" class="mt-2" v-model="nutrition_visible"> <b-collapse id="id_nutrition_collapse" class="mt-2" v-model="nutrition_visible">
<div class="card-body" v-if="recipe.nutrition !== null"> <div class="card-body" v-if="recipe.nutrition !== null">
<b-alert show> <b-alert show>
There is currently only very basic support for tracking nutritional information. There is currently only very basic support for tracking nutritional information. A
A <a href="https://github.com/vabene1111/recipes/issues/896" target="_blank" rel="noreferrer nofollow">big update</a> is planned to improve on this in many different areas.
<a href="https://github.com/vabene1111/recipes/issues/896" target="_blank"
rel="noreferrer nofollow">big update</a> is planned to improve on this in
many different areas.
</b-alert> </b-alert>
<label for="id_name"> {{ $t(energy()) }}</label> <label for="id_name"> {{ $t(energy()) }}</label>
<input class="form-control" id="id_calories" v-model="recipe.nutrition.calories"/> <input class="form-control" id="id_calories" v-model="recipe.nutrition.calories" />
<label for="id_name"> {{ $t("Carbohydrates") }}</label> <label for="id_name"> {{ $t("Carbohydrates") }}</label>
<input class="form-control" id="id_carbohydrates" <input class="form-control" id="id_carbohydrates" v-model="recipe.nutrition.carbohydrates" />
v-model="recipe.nutrition.carbohydrates"/>
<label for="id_name"> {{ $t("Fats") }}</label> <label for="id_name"> {{ $t("Fats") }}</label>
<input class="form-control" id="id_fats" v-model="recipe.nutrition.fats"/> <input class="form-control" id="id_fats" v-model="recipe.nutrition.fats" />
<label for="id_name"> {{ $t("Proteins") }}</label> <label for="id_name"> {{ $t("Proteins") }}</label>
<input class="form-control" id="id_proteins" v-model="recipe.nutrition.proteins"/> <input class="form-control" id="id_proteins" v-model="recipe.nutrition.proteins" />
</div> </div>
</b-collapse> </b-collapse>
</div> </div>
@ -150,12 +140,10 @@
</div> </div>
<!-- Steps --> <!-- Steps -->
<draggable :list="recipe.steps" group="steps" :empty-insert-threshold="10" handle=".handle" <draggable :list="recipe.steps" group="steps" :empty-insert-threshold="10" handle=".handle" @sort="sortSteps()">
@sort="sortSteps()">
<div v-for="(step, step_index) in recipe.steps" v-bind:key="step_index"> <div v-for="(step, step_index) in recipe.steps" v-bind:key="step_index">
<div class="card mt-2 mb-2"> <div class="card mt-2 mb-2">
<div class="card-body pr-2 pl-2 pr-md-5 pl-md-5" :id="`id_card_step_${step_index}`"> <div class="card-body pr-2 pl-2 pr-md-5 pl-md-5" :id="`id_card_step_${step_index}`">
<!-- step card header --> <!-- step card header -->
<div class="row"> <div class="row">
<div class="col-11"> <div class="col-11">
@ -166,32 +154,26 @@
</h4> </h4>
</div> </div>
<div class="col-1" style="text-align: right"> <div class="col-1" style="text-align: right">
<a class="btn shadow-none btn-lg" href="#" role="button" id="dropdownMenuLink" <a class="btn shadow-none btn-lg" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-ellipsis-v text-muted"></i> <i class="fas fa-ellipsis-v text-muted"></i>
</a> </a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuLink"> <div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuLink">
<button class="dropdown-item" @click="removeStep(step)"><i <button class="dropdown-item" @click="removeStep(step)"><i class="fa fa-trash fa-fw"></i> {{ $t("Delete") }}</button>
class="fa fa-trash fa-fw"></i> {{ $t("Delete") }}
</button>
<button type="button" class="dropdown-item" v-if="!step.show_as_header" <button type="button" class="dropdown-item" v-if="!step.show_as_header" @click="step.show_as_header = true">
@click="step.show_as_header = true">
<i class="fas fa-eye fa-fw"></i> {{ $t("Show_as_header") }} <i class="fas fa-eye fa-fw"></i> {{ $t("Show_as_header") }}
</button> </button>
<button type="button" class="dropdown-item" v-if="step.show_as_header" <button type="button" class="dropdown-item" v-if="step.show_as_header" @click="step.show_as_header = false">
@click="step.show_as_header = false">
<i class="fas fa-eye-slash fa-fw"></i> {{ $t("Hide_as_header") }} <i class="fas fa-eye-slash fa-fw"></i> {{ $t("Hide_as_header") }}
</button> </button>
<button class="dropdown-item" @click="moveStep(step, step_index - 1)" <button class="dropdown-item" @click="moveStep(step, step_index - 1)" v-if="step_index > 0">
v-if="step_index > 0"><i class="fa fa-arrow-up fa-fw"></i> <i class="fa fa-arrow-up fa-fw"></i>
{{ $t("Move_Up") }} {{ $t("Move_Up") }}
</button> </button>
<button class="dropdown-item" @click="moveStep(step, step_index + 1)" <button class="dropdown-item" @click="moveStep(step, step_index + 1)" v-if="step_index !== recipe.steps.length - 1">
v-if="step_index !== recipe.steps.length - 1">
<i class="fa fa-arrow-down fa-fw"></i> {{ $t("Move_Down") }} <i class="fa fa-arrow-down fa-fw"></i> {{ $t("Move_Down") }}
</button> </button>
</div> </div>
@ -202,36 +184,30 @@
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<label :for="'id_step_' + step.id + 'name'">{{ $t("Step_Name") }}</label> <label :for="'id_step_' + step.id + 'name'">{{ $t("Step_Name") }}</label>
<input class="form-control" v-model="step.name" <input class="form-control" v-model="step.name" :id="'id_step_' + step.id + 'name'" />
:id="'id_step_' + step.id + 'name'"/>
</div> </div>
</div> </div>
<!-- step data visibility controller --> <!-- step data visibility controller -->
<div class="row pt-2"> <div class="row pt-2">
<div class="col col-md-12"> <div class="col col-md-12">
<b-button pill variant="primary" size="sm" class="ml-1" <b-button pill variant="primary" size="sm" class="ml-1" @click="step.time_visible = true" v-if="!step.time_visible">
@click="step.time_visible = true" v-if="!step.time_visible">
<i class="fas fa-plus-circle"></i> {{ $t("Time") }} <i class="fas fa-plus-circle"></i> {{ $t("Time") }}
</b-button> </b-button>
<b-button pill variant="primary" size="sm" class="ml-1" <b-button pill variant="primary" size="sm" class="ml-1" @click="step.ingredients_visible = true" v-if="!step.ingredients_visible">
@click="step.ingredients_visible = true" v-if="!step.ingredients_visible">
<i class="fas fa-plus-circle"></i> {{ $t("Ingredients") }} <i class="fas fa-plus-circle"></i> {{ $t("Ingredients") }}
</b-button> </b-button>
<b-button pill variant="primary" size="sm" class="ml-1" <b-button pill variant="primary" size="sm" class="ml-1" @click="step.instruction_visible = true" v-if="!step.instruction_visible">
@click="step.instruction_visible = true" v-if="!step.instruction_visible">
<i class="fas fa-plus-circle"></i> {{ $t("Instructions") }} <i class="fas fa-plus-circle"></i> {{ $t("Instructions") }}
</b-button> </b-button>
<b-button pill variant="primary" size="sm" class="ml-1" <b-button pill variant="primary" size="sm" class="ml-1" @click="step.step_recipe_visible = true" v-if="!step.step_recipe_visible">
@click="step.step_recipe_visible = true" v-if="!step.step_recipe_visible">
<i class="fas fa-plus-circle"></i> {{ $t("Recipe") }} <i class="fas fa-plus-circle"></i> {{ $t("Recipe") }}
</b-button> </b-button>
<b-button pill variant="primary" size="sm" class="ml-1" <b-button pill variant="primary" size="sm" class="ml-1" @click="step.file_visible = true" v-if="!step.file_visible">
@click="step.file_visible = true" v-if="!step.file_visible">
<i class="fas fa-plus-circle"></i> {{ $t("File") }} <i class="fas fa-plus-circle"></i> {{ $t("File") }}
</b-button> </b-button>
</div> </div>
@ -240,8 +216,7 @@
<div class="row pt-2" v-if="step.time_visible"> <div class="row pt-2" v-if="step.time_visible">
<div class="col-md-12"> <div class="col-md-12">
<label :for="'id_step_' + step.id + '_time'">{{ $t("step_time_minutes") }}</label> <label :for="'id_step_' + step.id + '_time'">{{ $t("step_time_minutes") }}</label>
<input class="form-control" v-model="step.time" <input class="form-control" v-model="step.time" :id="'id_step_' + step.id + '_time'" />
:id="'id_step_' + step.id + '_time'"/>
</div> </div>
</div> </div>
@ -265,10 +240,19 @@
:multiple="false" :multiple="false"
:loading="files_loading" :loading="files_loading"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0" style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
@search-change="searchFiles" > @search-change="searchFiles"
>
</multiselect> </multiselect>
<b-input-group-append> <b-input-group-append>
<b-button variant="primary" @click="step_for_file_create = step;show_file_create = true"> + </b-button> <b-button
variant="primary"
@click="
step_for_file_create = step
show_file_create = true
"
>
+
</b-button>
</b-input-group-append> </b-input-group-append>
</b-input-group> </b-input-group>
</div> </div>
@ -309,23 +293,16 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-12 pr-0 pl-0 pr-md-2 pl-md-2 mt-2"> <div class="col-md-12 pr-0 pl-0 pr-md-2 pl-md-2 mt-2">
<draggable :list="step.ingredients" group="ingredients" <draggable :list="step.ingredients" group="ingredients" :empty-insert-threshold="10" handle=".handle" @sort="sortIngredients(step)">
:empty-insert-threshold="10" handle=".handle" <div v-for="(ingredient, index) in step.ingredients" :key="ingredient.id">
@sort="sortIngredients(step)"> <hr class="d-md-none" />
<div v-for="(ingredient, index) in step.ingredients"
:key="ingredient.id">
<hr class="d-md-none"/>
<div class="d-flex"> <div class="d-flex">
<div class="flex-grow-0 handle align-self-start"> <div class="flex-grow-0 handle align-self-start">
<button type="button" <button type="button" class="btn btn-lg shadow-none pr-0 pl-1 pr-md-2 pl-md-2"><i class="fas fa-arrows-alt-v"></i></button>
class="btn btn-lg shadow-none pr-0 pl-1 pr-md-2 pl-md-2"><i
class="fas fa-arrows-alt-v"></i></button>
</div> </div>
<div class="flex-fill row" <div class="flex-fill row" style="margin-left: 4px; margin-right: 4px">
style="margin-left: 4px; margin-right: 4px"> <div class="col-lg-2 col-md-6 small-padding" v-if="!ingredient.is_header">
<div class="col-lg-2 col-md-6 small-padding"
v-if="!ingredient.is_header">
<input <input
class="form-control" class="form-control"
v-model="ingredient.amount" v-model="ingredient.amount"
@ -336,8 +313,7 @@
/> />
</div> </div>
<div class="col-lg-2 col-md-6 small-padding" <div class="col-lg-2 col-md-6 small-padding" v-if="!ingredient.is_header">
v-if="!ingredient.is_header">
<!-- search set to false to allow API to drive results & order --> <!-- search set to false to allow API to drive results & order -->
<multiselect <multiselect
v-if="!ingredient.no_amount" v-if="!ingredient.no_amount"
@ -364,9 +340,9 @@
> >
</multiselect> </multiselect>
</div> </div>
<div class="col-lg-4 col-md-6 small-padding" <div class="col-lg-4 col-md-6 small-padding" v-if="!ingredient.is_header">
v-if="!ingredient.is_header">
<!-- search set to false to allow API to drive results & order --> <!-- search set to false to allow API to drive results & order -->
<multiselect <multiselect
ref="food" ref="food"
v-model="ingredient.food" v-model="ingredient.food"
@ -391,8 +367,7 @@
> >
</multiselect> </multiselect>
</div> </div>
<div class="small-padding" <div class="small-padding" v-bind:class="{ 'col-lg-4 col-md-6': !ingredient.is_header, 'col-lg-12 col-md-12': ingredient.is_header }">
v-bind:class="{ 'col-lg-4 col-md-6': !ingredient.is_header, 'col-lg-12 col-md-12': ingredient.is_header }">
<input <input
class="form-control" class="form-control"
maxlength="256" maxlength="256"
@ -411,50 +386,44 @@
</div> </div>
<div class="flex-grow-0 small-padding"> <div class="flex-grow-0 small-padding">
<a class="btn shadow-none btn-lg pr-1 pl-0 pr-md-2 pl-md-2" href="#" <a
role="button" id="dropdownMenuLink2" class="btn shadow-none btn-lg pr-1 pl-0 pr-md-2 pl-md-2"
data-toggle="dropdown" aria-haspopup="true" href="#"
aria-expanded="false"> role="button"
id="dropdownMenuLink2"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
<i class="fas fa-ellipsis-v text-muted"></i> <i class="fas fa-ellipsis-v text-muted"></i>
</a> </a>
<div class="dropdown-menu dropdown-menu-right" <div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuLink2">
aria-labelledby="dropdownMenuLink2"> <button type="button" class="dropdown-item" @click="removeIngredient(step, ingredient)">
<button type="button" class="dropdown-item"
@click="removeIngredient(step, ingredient)">
<i class="fa fa-trash fa-fw"></i> <i class="fa fa-trash fa-fw"></i>
{{ $t("Delete") }} {{ $t("Delete") }}
</button> </button>
<button type="button" class="dropdown-item" <button type="button" class="dropdown-item" v-if="!ingredient.is_header" @click="ingredient.is_header = true">
v-if="!ingredient.is_header"
@click="ingredient.is_header = true">
<i class="fas fa-heading fa-fw"></i> <i class="fas fa-heading fa-fw"></i>
{{ $t("Make_Header") }} {{ $t("Make_Header") }}
</button> </button>
<button type="button" class="dropdown-item" <button type="button" class="dropdown-item" v-if="ingredient.is_header" @click="ingredient.is_header = false">
v-if="ingredient.is_header"
@click="ingredient.is_header = false">
<i class="fas fa-leaf fa-fw"></i> <i class="fas fa-leaf fa-fw"></i>
{{ $t("Make_Ingredient") }} {{ $t("Make_Ingredient") }}
</button> </button>
<button type="button" class="dropdown-item" <button type="button" class="dropdown-item" v-if="!ingredient.no_amount" @click="ingredient.no_amount = true">
v-if="!ingredient.no_amount"
@click="ingredient.no_amount = true">
<i class="fas fa-balance-scale-right fa-fw"></i> <i class="fas fa-balance-scale-right fa-fw"></i>
{{ $t("Disable_Amount") }} {{ $t("Disable_Amount") }}
</button> </button>
<button type="button" class="dropdown-item" <button type="button" class="dropdown-item" v-if="ingredient.no_amount" @click="ingredient.no_amount = false">
v-if="ingredient.no_amount"
@click="ingredient.no_amount = false">
<i class="fas fa-balance-scale-right fa-fw"></i> <i class="fas fa-balance-scale-right fa-fw"></i>
{{ $t("Enable_Amount") }} {{ $t("Enable_Amount") }}
</button> </button>
<button type="button" class="dropdown-item" <button type="button" class="dropdown-item" @click="copyTemplateReference(index, ingredient)">
@click="copyTemplateReference(index, ingredient)">
<i class="fas fa-code"></i> <i class="fas fa-code"></i>
{{ $t("Copy_template_reference") }} {{ $t("Copy_template_reference") }}
</button> </button>
@ -466,11 +435,8 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-2 offset-md-5" <div class="col-md-2 offset-md-5" style="text-align: center; margin-top: 8px">
style="text-align: center; margin-top: 8px"> <button class="btn btn-success btn-block" @click="addIngredient(step)"><i class="fa fa-plus"></i></button>
<button class="btn btn-success btn-block"
@click="addIngredient(step)"><i class="fa fa-plus"></i>
</button>
</div> </div>
</div> </div>
</div> </div>
@ -503,29 +469,24 @@
{{ $t("Add_Step") }} {{ $t("Add_Step") }}
</button> </button>
<button type="button" v-b-modal:id_modal_sort class="btn btn-warning shadow-none "><i <button type="button" v-b-modal:id_modal_sort class="btn btn-warning shadow-none"><i class="fas fa-sort-amount-down-alt fa-lg"></i></button>
class="fas fa-sort-amount-down-alt fa-lg"></i></button>
</b-button-group> </b-button-group>
</div> </div>
</div> </div>
</div> </div>
</draggable> </draggable>
<br/> <br />
<br/> <br />
<br/> <br />
<br/> <br />
<br/> <br />
<br/> <br />
<!-- bottom buttons save/close/view --> <!-- bottom buttons save/close/view -->
<div class="row fixed-bottom p-2 b-2 border-top text-center" style="background: white" <div class="row fixed-bottom p-2 b-2 border-top text-center" style="background: white" v-if="recipe !== undefined">
v-if="recipe !== undefined">
<div class="col-md-3 col-6"> <div class="col-md-3 col-6">
<a :href="resolveDjangoUrl('delete_recipe', recipe.id)" <a :href="resolveDjangoUrl('delete_recipe', recipe.id)" class="btn btn-block btn-danger shadow-none">{{ $t("Delete") }}</a>
class="btn btn-block btn-danger shadow-none">{{ $t("Delete") }}</a>
</div> </div>
<div class="col-md-3 col-6"> <div class="col-md-3 col-6">
<a :href="resolveDjangoUrl('view_recipe', recipe.id)"> <a :href="resolveDjangoUrl('view_recipe', recipe.id)">
@ -533,15 +494,12 @@
</a> </a>
</div> </div>
<div class="col-md-3 col-6"> <div class="col-md-3 col-6">
<button type="button" @click="updateRecipe(false)" v-b-tooltip.hover <button type="button" @click="updateRecipe(false)" v-b-tooltip.hover :title="`${$t('Key_Ctrl')} + S`" class="btn btn-sm btn-block btn-info shadow-none">
:title="`${$t('Key_Ctrl')} + S`" class="btn btn-sm btn-block btn-info shadow-none">
{{ $t("Save") }} {{ $t("Save") }}
</button> </button>
</div> </div>
<div class="col-md-3 col-6"> <div class="col-md-3 col-6">
<button type="button" @click="updateRecipe(true)" v-b-tooltip.hover <button type="button" @click="updateRecipe(true)" v-b-tooltip.hover :title="`${$t('Key_Ctrl')} + ${$t('Key_Shift')} + S`" class="btn btn-sm btn-block btn-success shadow-none">
:title="`${$t('Key_Ctrl')} + ${$t('Key_Shift')} + S`"
class="btn btn-sm btn-block btn-success shadow-none">
{{ $t("Save_and_View") }} {{ $t("Save_and_View") }}
</button> </button>
</div> </div>
@ -549,11 +507,9 @@
<!-- modal for sorting steps --> <!-- modal for sorting steps -->
<b-modal id="id_modal_sort" v-bind:title="$t('Sort')" ok-only> <b-modal id="id_modal_sort" v-bind:title="$t('Sort')" ok-only>
<draggable :list="recipe.steps" group="step_sorter" :empty-insert-threshold="10" handle=".handle" <draggable :list="recipe.steps" group="step_sorter" :empty-insert-threshold="10" handle=".handle" @sort="sortSteps()" class="list-group" tag="ul">
@sort="sortSteps()" class="list-group" tag="ul">
<li class="list-group-item" v-for="(step, step_index) in recipe.steps" v-bind:key="step_index"> <li class="list-group-item" v-for="(step, step_index) in recipe.steps" v-bind:key="step_index">
<button type="button" class="btn btn-lg shadow-none handle"><i class="fas fa-arrows-alt-v"></i> <button type="button" class="btn btn-lg shadow-none handle"><i class="fas fa-arrows-alt-v"></i></button>
</button>
<template v-if="step.name !== ''">{{ step.name }}</template> <template v-if="step.name !== ''">{{ step.name }}</template>
<template v-else>{{ $t("Step") }} {{ step_index + 1 }}</template> <template v-else>{{ $t("Step") }} {{ step_index + 1 }}</template>
</li> </li>
@ -561,30 +517,21 @@
</b-modal> </b-modal>
<!-- form to create files on the fly --> <!-- form to create files on the fly -->
<generic-modal-form :model="Models.USERFILE" :action="Actions.CREATE" :show="show_file_create" <generic-modal-form :model="Models.USERFILE" :action="Actions.CREATE" :show="show_file_create" @finish-action="fileCreated" />
@finish-action="fileCreated"/>
</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 draggable from "vuedraggable" import draggable from "vuedraggable"
import { import { ApiMixin, resolveDjangoUrl, ResolveUrlMixin, StandardToasts, convertEnergyToCalories, energyHeading } from "@/utils/utils"
ApiMixin,
resolveDjangoUrl,
ResolveUrlMixin,
StandardToasts,
convertEnergyToCalories,
energyHeading
} from "@/utils/utils"
import Multiselect from "vue-multiselect" import Multiselect from "vue-multiselect"
import {ApiApiFactory} from "@/utils/openapi/api" import { ApiApiFactory } from "@/utils/openapi/api"
import LoadingSpinner from "@/components/LoadingSpinner" import LoadingSpinner from "@/components/LoadingSpinner"
import VueMarkdownEditor from "@kangc/v-md-editor" import VueMarkdownEditor from "@kangc/v-md-editor"
@ -598,7 +545,7 @@ VueMarkdownEditor.use(vuepressTheme, {
}) })
import enUS from "@kangc/v-md-editor/lib/lang/en-US" import enUS from "@kangc/v-md-editor/lib/lang/en-US"
import GenericModalForm from "@/components/Modals/GenericModalForm"; import GenericModalForm from "@/components/Modals/GenericModalForm"
VueMarkdownEditor.lang.use("en-US", enUS) VueMarkdownEditor.lang.use("en-US", enUS)
@ -609,7 +556,7 @@ Vue.use(BootstrapVue)
export default { export default {
name: "RecipeEditView", name: "RecipeEditView",
mixins: [ResolveUrlMixin, ApiMixin], mixins: [ResolveUrlMixin, ApiMixin],
components: {Multiselect, LoadingSpinner, draggable, GenericModalForm}, components: { Multiselect, LoadingSpinner, draggable, GenericModalForm },
data() { data() {
return { return {
recipe_id: window.RECIPE_ID, recipe_id: window.RECIPE_ID,
@ -639,11 +586,11 @@ export default {
}, },
mounted() { mounted() {
this.loadRecipe() this.loadRecipe()
// this.searchUnits("") this.searchUnits("")
// this.searchFoods("") this.searchFoods("")
// this.searchKeywords("") this.searchKeywords("")
this.searchFiles("") this.searchFiles("")
// this.searchRecipes("") this.searchRecipes("")
this.$i18n.locale = window.CUSTOM_LOCALE this.$i18n.locale = window.CUSTOM_LOCALE
}, },
@ -692,26 +639,28 @@ export default {
loadRecipe: function () { loadRecipe: function () {
let apiFactory = new ApiApiFactory() let apiFactory = new ApiApiFactory()
apiFactory.retrieveRecipe(this.recipe_id).then((response) => { apiFactory
this.recipe = response.data .retrieveRecipe(this.recipe_id)
this.loading = false .then((response) => {
this.recipe = response.data
this.loading = false
// set default visibility style for each component of the step // set default visibility style for each component of the step
this.recipe.steps.forEach((s) => { this.recipe.steps.forEach((s) => {
this.$set(s, 'time_visible', (s.time !== 0)) this.$set(s, "time_visible", s.time !== 0)
this.$set(s, 'ingredients_visible', (s.ingredients.length > 0)) this.$set(s, "ingredients_visible", s.ingredients.length > 0)
this.$set(s, 'instruction_visible', (s.instruction !== '')) this.$set(s, "instruction_visible", s.instruction !== "")
this.$set(s, 'step_recipe_visible', (s.step_recipe !== null)) this.$set(s, "step_recipe_visible", s.step_recipe !== null)
this.$set(s, 'file_visible', (s.file !== null)) this.$set(s, "file_visible", s.file !== null)
}) })
//TODO workaround function until view is properly refactored, loads name of selected sub recipe so the input can find its label //TODO workaround function until view is properly refactored, loads name of selected sub recipe so the input can find its label
this.recipe.steps.forEach((s) => { this.recipe.steps.forEach((s) => {
if (s.step_recipe != null) { if (s.step_recipe != null) {
this.recipes.push(s.step_recipe_data) this.recipes.push(s.step_recipe_data)
} }
})
}) })
})
.catch((err) => { .catch((err) => {
this.loading = false this.loading = false
console.log(err) console.log(err)
@ -736,7 +685,7 @@ export default {
} }
this.recipe.servings = Math.floor(this.recipe.servings) // temporary fix until a proper framework for frontend input validation is established this.recipe.servings = Math.floor(this.recipe.servings) // temporary fix until a proper framework for frontend input validation is established
if (this.recipe.servings === "" || isNaN(this.recipe.servings) || this.recipe.servings===0 ) { if (this.recipe.servings === "" || isNaN(this.recipe.servings) || this.recipe.servings === 0) {
this.recipe.servings = 1 this.recipe.servings = 1
} }
@ -796,10 +745,10 @@ export default {
ingredients_visible: true, ingredients_visible: true,
instruction_visible: true, instruction_visible: true,
step_recipe_visible: false, step_recipe_visible: false,
file_visible: false file_visible: false,
} }
if (step_index !== undefined) { if (step_index !== undefined) {
console.log('adding at index', step_index) console.log("adding at index", step_index)
this.recipe.steps.splice(step_index + 1, 0, empty_step) this.recipe.steps.splice(step_index + 1, 0, empty_step)
} else { } else {
this.recipe.steps.push(empty_step) this.recipe.steps.push(empty_step)
@ -832,12 +781,12 @@ export default {
this.$nextTick(() => document.getElementById(`amount_${this.recipe.steps.indexOf(step)}_${step.ingredients.length - 1}`).focus()) this.$nextTick(() => document.getElementById(`amount_${this.recipe.steps.indexOf(step)}_${step.ingredients.length - 1}`).focus())
}, },
removeIngredient: function (step, ingredient) { removeIngredient: function (step, ingredient) {
if (confirm(this.$t("confirm_delete", {object: this.$t("Ingredient")}))) { if (confirm(this.$t("confirm_delete", { object: this.$t("Ingredient") }))) {
step.ingredients = step.ingredients.filter((item) => item !== ingredient) step.ingredients = step.ingredients.filter((item) => item !== ingredient)
} }
}, },
removeStep: function (step) { removeStep: function (step) {
if (confirm(this.$t("confirm_delete", {object: this.$t("Step")}))) { if (confirm(this.$t("confirm_delete", { object: this.$t("Step") }))) {
this.recipe.steps = this.recipe.steps.filter((item) => item !== step) this.recipe.steps = this.recipe.steps.filter((item) => item !== step)
} }
}, },
@ -850,7 +799,7 @@ export default {
let [tmp, step, id] = index.split("_") let [tmp, step, id] = index.split("_")
let new_food = this.recipe.steps[step].ingredients[id] let new_food = this.recipe.steps[step].ingredients[id]
new_food.food = {name: tag} new_food.food = { name: tag }
this.foods.push(new_food.food) this.foods.push(new_food.food)
this.recipe.steps[step].ingredients[id] = new_food this.recipe.steps[step].ingredients[id] = new_food
}, },
@ -858,12 +807,12 @@ export default {
let [tmp, step, id] = index.split("_") let [tmp, step, id] = index.split("_")
let new_unit = this.recipe.steps[step].ingredients[id] let new_unit = this.recipe.steps[step].ingredients[id]
new_unit.unit = {name: tag} new_unit.unit = { name: tag }
this.units.push(new_unit.unit) this.units.push(new_unit.unit)
this.recipe.steps[step].ingredients[id] = new_unit this.recipe.steps[step].ingredients[id] = new_unit
}, },
addKeyword: function (tag) { addKeyword: function (tag) {
let new_keyword = {label: tag, name: tag} let new_keyword = { label: tag, name: tag }
this.recipe.keywords.push(new_keyword) this.recipe.keywords.push(new_keyword)
}, },
searchKeywords: function (query) { searchKeywords: function (query) {
@ -886,7 +835,7 @@ export default {
this.files_loading = true this.files_loading = true
apiFactory apiFactory
.listUserFiles({query: {query: query}}) .listUserFiles({ query: { query: query } })
.then((response) => { .then((response) => {
this.files = response.data this.files = response.data
this.files_loading = false this.files_loading = false
@ -898,7 +847,7 @@ export default {
}, },
searchRecipes: function (query) { searchRecipes: function (query) {
this.recipes_loading = true this.recipes_loading = true
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, {query: query}) this.genericAPI(this.Models.RECIPE, this.Actions.LIST, { query: query })
.then((result) => { .then((result) => {
this.recipes = result.data.results this.recipes = result.data.results
this.recipes_loading = false this.recipes_loading = false
@ -958,13 +907,13 @@ export default {
}) })
}, },
fileCreated: function (data) { fileCreated: function (data) {
if (data !== 'cancel') { if (data !== "cancel") {
this.step_for_file_create.file = data.item this.step_for_file_create.file = data.item
} }
this.show_file_create = false this.show_file_create = false
}, },
scrollToStep: function (step_index) { scrollToStep: function (step_index) {
document.getElementById("id_step_" + step_index).scrollIntoView({behavior: "smooth"}) document.getElementById("id_step_" + step_index).scrollIntoView({ behavior: "smooth" })
}, },
addNutrition: function () { addNutrition: function () {
this.recipe.nutrition = {} this.recipe.nutrition = {}

View File

@ -36,7 +36,7 @@
<!-- add to shopping form --> <!-- add to shopping form -->
<b-row class="justify-content-md-center align-items-center pl-1 pr-1" v-if="entrymode"> <b-row class="justify-content-md-center align-items-center pl-1 pr-1" v-if="entrymode">
<b-col cols="12" md="3" v-if="!entry_mode_simple" class="d-none d-md-block mt-1"> <b-col cols="12" md="3" v-if="!ui.entry_mode_simple" class="d-none d-md-block mt-1">
<b-form-input <b-form-input
size="lg" size="lg"
min="1" min="1"
@ -46,13 +46,13 @@
style="font-size: 16px; border-radius: 5px !important; border: 1px solid #e8e8e8 !important" style="font-size: 16px; border-radius: 5px !important; border: 1px solid #e8e8e8 !important"
></b-form-input> ></b-form-input>
</b-col> </b-col>
<b-col cols="12" md="4" v-if="!entry_mode_simple" class="mt-1"> <b-col cols="12" md="4" v-if="!ui.entry_mode_simple" class="mt-1">
<lookup-input :class_list="'mb-0'" :form="formUnit" :model="Models.UNIT" @change="new_item.unit = $event" :show_label="false" :clear="clear" /> <lookup-input :class_list="'mb-0'" :form="formUnit" :model="Models.UNIT" @change="new_item.unit = $event" :show_label="false" :clear="clear" />
</b-col> </b-col>
<b-col cols="12" md="4" v-if="!entry_mode_simple" class="mt-1"> <b-col cols="12" md="4" v-if="!ui.entry_mode_simple" class="mt-1">
<lookup-input :class_list="'mb-0'" :form="formFood" :model="Models.FOOD" @change="new_item.food = $event" :show_label="false" :clear="clear" /> <lookup-input :class_list="'mb-0'" :form="formFood" :model="Models.FOOD" @change="new_item.food = $event" :show_label="false" :clear="clear" />
</b-col> </b-col>
<b-col cols="12" md="11" v-if="entry_mode_simple" class="mt-1"> <b-col cols="12" md="11" v-if="ui.entry_mode_simple" class="mt-1">
<b-form-input size="lg" type="text" :placeholder="$t('QuickEntry')" v-model="new_item.ingredient" @keyup.enter="addItem"></b-form-input> <b-form-input size="lg" type="text" :placeholder="$t('QuickEntry')" v-model="new_item.ingredient" @keyup.enter="addItem"></b-form-input>
</b-col> </b-col>
<b-col cols="12" md="1" class="d-none d-md-block mt-1"> <b-col cols="12" md="1" class="d-none d-md-block mt-1">
@ -60,7 +60,7 @@
<i class="btn fas fa-cart-plus fa-lg px-0 text-success" @click="addItem" /> <i class="btn fas fa-cart-plus fa-lg px-0 text-success" @click="addItem" />
</b-button> </b-button>
</b-col> </b-col>
<b-col cols="12" md="3" v-if="!entry_mode_simple" class="d-block d-md-none mt-1"> <b-col cols="12" md="3" v-if="!ui.entry_mode_simple" class="d-block d-md-none mt-1">
<b-row> <b-row>
<b-col cols="9"> <b-col cols="9">
<b-form-input <b-form-input
@ -82,10 +82,10 @@
</b-row> </b-row>
<b-row class="row justify-content-around mt-2" v-if="entrymode"> <b-row class="row justify-content-around mt-2" v-if="entrymode">
<b-form-checkbox switch v-model="entry_mode_simple"> <b-form-checkbox switch v-model="ui.entry_mode_simple">
{{ $t("QuickEntry") }} {{ $t("QuickEntry") }}
</b-form-checkbox> </b-form-checkbox>
<b-button variant="success" size="sm" class="d-flex d-md-none p-0" v-if="entry_mode_simple"> <b-button variant="success" size="sm" class="d-flex d-md-none p-0" v-if="ui.entry_mode_simple">
<i class="btn fas fa-cart-plus" @click="addItem" /> <i class="btn fas fa-cart-plus" @click="addItem" />
</b-button> </b-button>
</b-row> </b-row>
@ -156,9 +156,6 @@
<b-input-group-prepend is-text> <b-input-group-prepend is-text>
<input type="number" :min="1" v-model="add_recipe_servings" style="width: 3em" /> <input type="number" :min="1" v-model="add_recipe_servings" style="width: 3em" />
</b-input-group-prepend> </b-input-group-prepend>
<!-- <b-input-group-prepend is-text>
<b>{{ $t("Recipe") }}</b>
</b-input-group-prepend> -->
<generic-multiselect <generic-multiselect
class="input-group-text m-0 p-0" class="input-group-text m-0 p-0"
@change="new_recipe = $event.val" @change="new_recipe = $event.val"
@ -589,16 +586,16 @@
<b-form-select v-model="group_by" :options="group_by_choices" size="sm"></b-form-select> <b-form-select v-model="group_by" :options="group_by_choices" size="sm"></b-form-select>
</b-form-group> </b-form-group>
<b-form-group v-bind:label="$t('Supermarket')" label-for="popover-input-2" label-cols="6" class="mb-1"> <b-form-group v-bind:label="$t('Supermarket')" label-for="popover-input-2" label-cols="6" class="mb-1">
<b-form-select v-model="selected_supermarket" :options="supermarkets" text-field="name" value-field="id" size="sm"></b-form-select> <b-form-select v-model="ui.selected_supermarket" :options="supermarkets" text-field="name" value-field="id" size="sm"></b-form-select>
</b-form-group> </b-form-group>
<!-- TODO: shade filters red when they are actually filtering content --> <!-- TODO: shade filters red when they are actually filtering content -->
<b-form-group v-bind:label="$t('ShowDelayed')" label-for="popover-input-3" content-cols="1" class="mb-1"> <b-form-group v-bind:label="$t('ShowDelayed')" label-for="popover-input-3" content-cols="1" class="mb-1">
<b-form-checkbox v-model="show_delay"></b-form-checkbox> <b-form-checkbox v-model="show_delay"></b-form-checkbox>
</b-form-group> </b-form-group>
<b-form-group v-bind:label="$t('ShowUncategorizedFood')" label-for="popover-input-4" content-cols="1" class="mb-1" v-if="!selected_supermarket"> <b-form-group v-bind:label="$t('ShowUncategorizedFood')" label-for="popover-input-4" content-cols="1" class="mb-1" v-if="!ui.selected_supermarket">
<b-form-checkbox v-model="show_undefined_categories"></b-form-checkbox> <b-form-checkbox v-model="show_undefined_categories"></b-form-checkbox>
</b-form-group> </b-form-group>
<b-form-group v-bind:label="$t('SupermarketCategoriesOnly')" label-for="popover-input-5" content-cols="1" class="mb-1" v-if="selected_supermarket"> <b-form-group v-bind:label="$t('SupermarketCategoriesOnly')" label-for="popover-input-5" content-cols="1" class="mb-1" v-if="ui.selected_supermarket">
<b-form-checkbox v-model="supermarket_categories_only"></b-form-checkbox> <b-form-checkbox v-model="supermarket_categories_only"></b-form-checkbox>
</b-form-group> </b-form-group>
</div> </div>
@ -718,13 +715,15 @@ export default {
group_by_choices: ["created_by", "category", "recipe"], group_by_choices: ["created_by", "category", "recipe"],
supermarkets: [], supermarkets: [],
shopping_categories: [], shopping_categories: [],
selected_supermarket: undefined,
show_undefined_categories: true, show_undefined_categories: true,
supermarket_categories_only: false, supermarket_categories_only: false,
shopcat: null, shopcat: null,
delay: 0, delay: 0,
clear: Math.random(), clear: Math.random(),
entry_mode_simple: false, ui: {
entry_mode_simple: false,
selected_supermarket: undefined,
},
settings: { settings: {
shopping_auto_sync: 0, shopping_auto_sync: 0,
default_delay: 4, default_delay: 4,
@ -773,14 +772,15 @@ export default {
let shopping_list = this.items let shopping_list = this.items
// filter out list items that are delayed // filter out list items that are delayed
if (!this.show_delay && shopping_list) { if (!this.show_delay && shopping_list) {
shopping_list = shopping_list.filter((x) => !x.delay_until || !Date.parse(x?.delay_until) < new Date(Date.now())) shopping_list = shopping_list.filter((x) => !x.delay_until || Date.parse(x?.delay_until) < new Date(Date.now()))
} }
// if a supermarket is selected and filtered to only supermarket categories filter out everything else // if a supermarket is selected and filtered to only supermarket categories filter out everything else
if (this.selected_supermarket && this.supermarket_categories_only) { if (this.ui.selected_supermarket && this.supermarket_categories_only) {
let shopping_categories = this.supermarkets // category IDs configured on supermarket let shopping_categories = this.supermarkets // category IDs configured on supermarket
.filter((x) => x.id === this.selected_supermarket) .filter((x) => x.id === this.ui.selected_supermarket)
.map((x) => x.category_to_supermarket) .map((x) => x.category_to_supermarket)
.flat() .flat()
.map((x) => x.category.id) .map((x) => x.category.id)
@ -791,12 +791,12 @@ export default {
} }
var groups = { false: {}, true: {} } // force unchecked to always be first var groups = { false: {}, true: {} } // force unchecked to always be first
if (this.selected_supermarket) { if (this.ui.selected_supermarket) {
// TODO: make nulls_first a user setting // TODO: make nulls_first a user setting
groups.false[this.$t("Undefined")] = {} groups.false[this.$t("Undefined")] = {}
groups.true[this.$t("Undefined")] = {} groups.true[this.$t("Undefined")] = {}
let super_cats = this.supermarkets let super_cats = this.supermarkets
.filter((x) => x.id === this.selected_supermarket) .filter((x) => x.id === this.ui.selected_supermarket)
.map((x) => x.category_to_supermarket) .map((x) => x.category_to_supermarket)
.flat() .flat()
.map((x) => x.category.name) .map((x) => x.category.name)
@ -850,7 +850,7 @@ export default {
return this.items.filter((x) => !x.delay_until || !Date.parse(x?.delay_until) > new Date(Date.now())).length < this.items.length return this.items.filter((x) => !x.delay_until || !Date.parse(x?.delay_until) > new Date(Date.now())).length < this.items.length
}, },
filterApplied() { filterApplied() {
return (this.itemsDelayed && !this.show_delay) || !this.show_undefined_categories || (this.supermarket_categories_only && this.selected_supermarket) return (this.itemsDelayed && !this.show_delay) || !this.show_undefined_categories || (this.supermarket_categories_only && this.ui.selected_supermarket)
}, },
Recipes() { Recipes() {
// hiding recipes associated with shopping list items that are complete // hiding recipes associated with shopping list items that are complete
@ -876,10 +876,13 @@ export default {
}, },
}, },
watch: { watch: {
selected_supermarket(newVal, oldVal) { ui: {
this.supermarket_categories_only = this.settings.filter_to_supermarket handler() {
localStorage.setItem("shopping_v2_selected_supermarket", JSON.stringify(this.selected_supermarket)) this.$cookies.set(SETTINGS_COOKIE_NAME, this.ui)
},
deep: true,
}, },
new_recipe: { new_recipe: {
handler() { handler() {
this.add_recipe_servings = this.new_recipe.servings this.add_recipe_servings = this.new_recipe.servings
@ -910,12 +913,11 @@ export default {
"settings.default_delay": function (newVal, oldVal) { "settings.default_delay": function (newVal, oldVal) {
this.delay = Number(newVal) this.delay = Number(newVal)
}, },
entry_mode_simple(newVal) { "ui.selected_supermarket": function (newVal, oldVal) {
this.$cookies.set(SETTINGS_COOKIE_NAME, newVal) this.supermarket_categories_only = this.settings.filter_to_supermarket
}, },
}, },
mounted() { mounted() {
console.log(screen.height)
this.getShoppingList() this.getShoppingList()
this.getSupermarkets() this.getSupermarkets()
this.getShoppingCategories() this.getShoppingCategories()
@ -929,15 +931,14 @@ export default {
} }
this.$nextTick(function () { this.$nextTick(function () {
if (this.$cookies.isKey(SETTINGS_COOKIE_NAME)) { if (this.$cookies.isKey(SETTINGS_COOKIE_NAME)) {
this.entry_mode_simple = this.$cookies.get(SETTINGS_COOKIE_NAME) this.ui = Object.assign({}, this.ui, this.$cookies.get(SETTINGS_COOKIE_NAME))
} }
this.selected_supermarket = localStorage.getItem("shopping_v2_selected_supermarket") || undefined
}) })
}, },
methods: { methods: {
// this.genericAPI inherited from ApiMixin // this.genericAPI inherited from ApiMixin
addItem: function () { addItem: function () {
if (this.entry_mode_simple) { if (this.ui.entry_mode_simple) {
if (this.new_item.ingredient !== "" && this.new_item.ingredient !== undefined) { if (this.new_item.ingredient !== "" && this.new_item.ingredient !== undefined) {
this.genericPostAPI("api_ingredient_from_string", { text: this.new_item.ingredient }).then((result) => { this.genericPostAPI("api_ingredient_from_string", { text: this.new_item.ingredient }).then((result) => {
let unit = null let unit = null
@ -1004,7 +1005,7 @@ export default {
}) })
}, },
resetFilters: function () { resetFilters: function () {
this.selected_supermarket = undefined this.ui.selected_supermarket = undefined
this.supermarket_categories_only = this.settings.filter_to_supermarket this.supermarket_categories_only = this.settings.filter_to_supermarket
this.show_undefined_categories = true this.show_undefined_categories = true
this.group_by = "category" this.group_by = "category"
@ -1091,7 +1092,7 @@ export default {
}, },
getShoppingList: function (autosync = false) { getShoppingList: function (autosync = false) {
let params = {} let params = {}
params.supermarket = this.selected_supermarket params.supermarket = this.ui.selected_supermarket
params.options = { query: { recent: 1 } } params.options = { query: { recent: 1 } }
if (autosync) { if (autosync) {

View File

@ -110,6 +110,7 @@ export default {
methods: { methods: {
// this.genericAPI inherited from ApiMixin // this.genericAPI inherited from ApiMixin
search: function (query) { search: function (query) {
console.log("did the thing")
let options = { let options = {
page: 1, page: 1,
pageSize: this.limit, pageSize: this.limit,