Merge branch 'additional_fixes' into feature/custom_filters

This commit is contained in:
smilerz 2022-02-08 09:05:07 -06:00
commit dc71260baa
No known key found for this signature in database
GPG Key ID: 39444C7606D47126
13 changed files with 229 additions and 267 deletions

29
boot.sh
View File

@ -1,12 +1,35 @@
#!/bin/sh #!/bin/sh
source venv/bin/activate source venv/bin/activate
echo "Updating database" echo "Migrating database"
python manage.py migrate
attempt=0
max_attempts=20
while python manage.py migrate; \
status=$?; \
attempt=$((attempt+1)); \
[ $status -eq 1 ] \
&& [ $attempt -le $max_attempts ]; do
echo -e "\n!!! Migration failed (error ${status}, attempt ${attempt}/${max_attempts})."
echo "!!! Database may not be ready yet or system is misconfigured."
echo -e "!!! Retrying in 5 seconds...\n"
sleep 5
done
if [ $attempt -gt $max_attempts ]; then
echo -e "\n!!! Migration failed. Maximum attempts exceeded."
echo "!!! Please check logs above - misconfiguration is very likely."
echo "!!! Shutting down container."
exit 1 # exit with error to make the container stop
fi
echo "Generating static files"
python manage.py collectstatic_js_reverse python manage.py collectstatic_js_reverse
python manage.py collectstatic --noinput python manage.py collectstatic --noinput
echo "Done" echo "Done"
chmod -R 755 /opt/recipes/mediafiles chmod -R 755 /opt/recipes/mediafiles
exec gunicorn -b :8080 --access-logfile - --error-logfile - --log-level INFO recipes.wsgi exec gunicorn -b :8080 --access-logfile - --error-logfile - --log-level INFO recipes.wsgi

View File

@ -5,7 +5,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 _
@ -159,6 +159,8 @@ class RecipeSearch():
# otherwise sort by the remaining order_by attributes or favorite by default # otherwise sort by the remaining order_by attributes or favorite by default
else: else:
order += default_order order += default_order
order[:] = [Lower('name').asc() if x == 'name' else x for x in order]
order[:] = [Lower('name').desc() if x == '-name' else x for x in order]
self.orderby = order self.orderby = order
def string_filters(self, string=None): def string_filters(self, string=None):
@ -653,9 +655,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
@ -664,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('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())
def old_search(request): def old_search(request):
@ -674,6 +676,6 @@ 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

@ -719,7 +719,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')

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,20 +42,18 @@ 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, CustomFilter, Food, from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilter, ExportLog, Food,
FoodInheritField, ImportLog, Ingredient, Keyword, MealPlan, MealType, FoodInheritField, ImportLog, Ingredient, Keyword, MealPlan, MealType,
Recipe, RecipeBook, RecipeBookEntry, ShareLink, ShoppingList, Recipe, RecipeBook, RecipeBookEntry, ShareLink, ShoppingList,
ShoppingListEntry, ShoppingListRecipe, Step, Storage, Supermarket, ShoppingListEntry, ShoppingListRecipe, Step, Storage, Supermarket,
SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, Unit, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, Unit,
UserFile, UserPreference, ViewLog) UserFile, UserPreference, ViewLog)
from cookbook.models import (ExportLog)
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,
ExportLogSerializer, CookLogSerializer, CustomFilterSerializer, ExportLogSerializer,
CookLogSerializer, CustomFilterSerializer,
FoodInheritFieldSerializer, FoodSerializer, FoodInheritFieldSerializer, FoodSerializer,
FoodShoppingUpdateSerializer, ImportLogSerializer, FoodShoppingUpdateSerializer, ImportLogSerializer,
IngredientSerializer, KeywordSerializer, MealPlanSerializer, IngredientSerializer, KeywordSerializer, MealPlanSerializer,
@ -141,7 +139,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
@ -164,7 +162,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)
@ -178,9 +176,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)
@ -279,7 +277,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)
@ -407,7 +405,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()
@ -886,7 +884,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

@ -9,15 +9,9 @@ services:
- ./.env - ./.env
networks: networks:
- default - default
healthcheck:
test: ["CMD-SHELL", "psql -U $$POSTGRES_USER -d $$POSTGRES_DB --list || exit 1"]
interval: 4s
timeout: 1s
retries: 12
web_recipes: web_recipes:
image: vabene1111/recipes image: vabene1111/recipes
restart: always
env_file: env_file:
- ./.env - ./.env
volumes: volumes:
@ -25,8 +19,7 @@ services:
- nginx_config:/opt/recipes/nginx/conf.d - nginx_config:/opt/recipes/nginx/conf.d
- ./mediafiles:/opt/recipes/mediafiles - ./mediafiles:/opt/recipes/mediafiles
depends_on: depends_on:
db_recipes: - db_recipes
condition: service_healthy
networks: networks:
- default - default

View File

@ -7,15 +7,9 @@ services:
- ./postgresql:/var/lib/postgresql/data - ./postgresql:/var/lib/postgresql/data
env_file: env_file:
- ./.env - ./.env
healthcheck:
test: ["CMD-SHELL", "psql -U $$POSTGRES_USER -d $$POSTGRES_DB --list || exit 1"]
interval: 4s
timeout: 1s
retries: 12
web_recipes: web_recipes:
image: vabene1111/recipes image: vabene1111/recipes
restart: always
env_file: env_file:
- ./.env - ./.env
volumes: volumes:
@ -23,8 +17,7 @@ services:
- nginx_config:/opt/recipes/nginx/conf.d - nginx_config:/opt/recipes/nginx/conf.d
- ./mediafiles:/opt/recipes/mediafiles - ./mediafiles:/opt/recipes/mediafiles
depends_on: depends_on:
db_recipes: - db_recipes
condition: service_healthy
nginx_recipes: nginx_recipes:
image: nginx:mainline-alpine image: nginx:mainline-alpine

View File

@ -9,15 +9,9 @@ services:
- ./.env - ./.env
networks: networks:
- default - default
healthcheck:
test: ["CMD-SHELL", "psql -U $$POSTGRES_USER -d $$POSTGRES_DB --list || exit 1"]
interval: 4s
timeout: 1s
retries: 12
web_recipes: web_recipes:
image: vabene1111/recipes image: vabene1111/recipes
restart: always
env_file: env_file:
- ./.env - ./.env
volumes: volumes:
@ -25,8 +19,7 @@ services:
- nginx_config:/opt/recipes/nginx/conf.d - nginx_config:/opt/recipes/nginx/conf.d
- ./mediafiles:/opt/recipes/mediafiles - ./mediafiles:/opt/recipes/mediafiles
depends_on: depends_on:
db_recipes: - db_recipes
condition: service_healthy
networks: networks:
- default - default

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

@ -474,7 +474,7 @@
<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="$t('sort_by')" variant="link" toggle-class="text-decoration-none " class="m-0 p-0"> <b-dropdown id="sortby" :text="sortByLabel" variant="link" 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="
@ -627,6 +627,13 @@ export default {
locale: function () { locale: function () {
return window.CUSTOM_LOCALE return window.CUSTOM_LOCALE
}, },
sortByLabel: function () {
if (this.search.sort_order.length == 1) {
return this.search.sort_order[0].text
} else {
return this.$t("sort_by")
}
},
yesterday: function () { yesterday: function () {
const now = new Date() const now = new Date()
return new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1) return new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1)
@ -1014,7 +1021,6 @@ export default {
rating: rating, rating: rating,
internal: this.search.search_internal, internal: this.search.search_internal,
random: this.random_search, random: this.random_search,
_new: this.ui.sort_by_new,
timescooked: timescooked, timescooked: timescooked,
makenow: this.search.makenow || undefined, makenow: this.search.makenow || undefined,
lastcooked: lastcooked, lastcooked: lastcooked,
@ -1028,6 +1034,7 @@ export default {
} }
if (!this.searchFiltered()) { if (!this.searchFiltered()) {
params.options.query.last_viewed = this.ui.recently_viewed params.options.query.last_viewed = this.ui.recently_viewed
params._new = this.ui.sort_by_new
} }
return params return params
}, },
@ -1039,7 +1046,7 @@ export default {
this.search?.search_units?.length !== 0 || this.search?.search_units?.length !== 0 ||
this.random_search || this.random_search ||
this.search?.search_filter || this.search?.search_filter ||
this.search.sort_order.length !== 0 || this.search.sort_order.length > 1 ||
this.search?.search_rating !== undefined || this.search?.search_rating !== undefined ||
(this.search.timescooked !== undefined && this.search.timescooked !== "") || (this.search.timescooked !== undefined && this.search.timescooked !== "") ||
this.search.makenow !== false || this.search.makenow !== false ||
@ -1048,7 +1055,7 @@ export default {
if (ignore_string) { if (ignore_string) {
return filtered return filtered
} else { } else {
return filtered || this.search?.search_input != "" return filtered || this.search?.search_input != "" || this.search.sort_order.length <= 1
} }
}, },
addFields(field) { addFields(field) {

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,8 +913,8 @@ 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() {
@ -928,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
@ -1003,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"
@ -1090,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) {