Merge remote-tracking branch 'origin/develop' into HomeAssistantConnector
# Conflicts: # cookbook/views/api.py # recipes/settings.py
This commit is contained in:
commit
beb860acc6
@ -1,6 +1,7 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="vaben">
|
||||
<words>
|
||||
<w>mealplan</w>
|
||||
<w>pinia</w>
|
||||
<w>selfhosted</w>
|
||||
<w>unapplied</w>
|
||||
|
@ -1,5 +1,6 @@
|
||||
from datetime import datetime
|
||||
|
||||
from allauth.account.forms import ResetPasswordForm, SignupForm
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
@ -9,6 +10,8 @@ from django_scopes import scopes_disabled
|
||||
from django_scopes.forms import SafeModelChoiceField, SafeModelMultipleChoiceField
|
||||
from hcaptcha.fields import hCaptchaField
|
||||
|
||||
|
||||
|
||||
from .models import (Comment, Food, InviteLink, Keyword, Recipe, RecipeBook, RecipeBookEntry,
|
||||
SearchPreference, Space, Storage, Sync, User, UserPreference, ConnectorConfig)
|
||||
|
||||
@ -358,12 +361,12 @@ class SpaceJoinForm(forms.Form):
|
||||
token = forms.CharField()
|
||||
|
||||
|
||||
class AllAuthSignupForm(forms.Form):
|
||||
class AllAuthSignupForm(SignupForm):
|
||||
captcha = hCaptchaField()
|
||||
terms = forms.BooleanField(label=_('Accept Terms and Privacy'))
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(AllAuthSignupForm, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
if settings.PRIVACY_URL == '' and settings.TERMS_URL == '':
|
||||
self.fields.pop('terms')
|
||||
if settings.HCAPTCHA_SECRET == '':
|
||||
@ -373,6 +376,15 @@ class AllAuthSignupForm(forms.Form):
|
||||
pass
|
||||
|
||||
|
||||
class CustomPasswordResetForm(ResetPasswordForm):
|
||||
captcha = hCaptchaField()
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(CustomPasswordResetForm, self).__init__(**kwargs)
|
||||
if settings.HCAPTCHA_SECRET == '':
|
||||
self.fields.pop('captcha')
|
||||
|
||||
|
||||
class UserCreateForm(forms.Form):
|
||||
name = forms.CharField(label='Username')
|
||||
password = forms.CharField(
|
||||
|
@ -98,7 +98,7 @@ class AutomationEngine:
|
||||
try:
|
||||
return self.food_aliases[food.lower()]
|
||||
except KeyError:
|
||||
return food
|
||||
return self.apply_regex_replace_automation(food, Automation.FOOD_REPLACE)
|
||||
else:
|
||||
if automation := Automation.objects.filter(space=self.request.space, type=Automation.FOOD_ALIAS, param_1__iexact=food, disabled=False).order_by('order').first():
|
||||
return automation.param_2
|
||||
|
@ -27,9 +27,6 @@ def shopping_helper(qs, request):
|
||||
elif checked in ['true', 1, '1']:
|
||||
qs = qs.filter(checked=True)
|
||||
elif checked in ['recent']:
|
||||
today_start = timezone.now().replace(hour=0, minute=0, second=0)
|
||||
week_ago = today_start - timedelta(days=user.userpreference.shopping_recent_days)
|
||||
qs = qs.filter(Q(checked=False) | Q(completed_at__gte=week_ago))
|
||||
supermarket_order = ['checked'] + supermarket_order
|
||||
|
||||
return qs.distinct().order_by(*supermarket_order).select_related('unit', 'food', 'ingredient', 'created_by', 'list_recipe', 'list_recipe__mealplan', 'list_recipe__recipe')
|
||||
|
@ -15,8 +15,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-05-18 14:28+0200\n"
|
||||
"PO-Revision-Date: 2023-11-22 18:19+0000\n"
|
||||
"Last-Translator: Spreez <tandoor@larsdev.de>\n"
|
||||
"PO-Revision-Date: 2024-02-13 16:19+0000\n"
|
||||
"Last-Translator: Kirstin Seidel-Gebert <kirstin@trebeg.de>\n"
|
||||
"Language-Team: German <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/de/>\n"
|
||||
"Language: de\n"
|
||||
@ -161,7 +161,7 @@ msgstr "Name"
|
||||
|
||||
#: .\cookbook\forms.py:124 .\cookbook\forms.py:315 .\cookbook\views\lists.py:88
|
||||
msgid "Keywords"
|
||||
msgstr "Schlüsselwörter"
|
||||
msgstr "Schlagwörter"
|
||||
|
||||
#: .\cookbook\forms.py:125
|
||||
msgid "Preparation time in minutes"
|
||||
@ -1436,9 +1436,9 @@ msgid ""
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" <b>Passwort und Token</b> werden im <b>Klartext</b> in der Datenbank "
|
||||
" <b>Kennwort und Token</b> werden im <b>Klartext</b> in der Datenbank "
|
||||
"gespeichert.\n"
|
||||
" Dies ist notwendig da Passwort oder Token benötigt werden, um API-"
|
||||
" Dies ist notwendig da Kennwort oder Token benötigt werden, um API-"
|
||||
"Anfragen zu stellen, bringt jedoch auch ein Sicherheitsrisiko mit sich. <br/>"
|
||||
"\n"
|
||||
" Um das Risiko zu minimieren sollten, wenn möglich, Tokens oder "
|
||||
|
@ -13,8 +13,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-05-18 14:28+0200\n"
|
||||
"PO-Revision-Date: 2023-08-15 19:19+0000\n"
|
||||
"Last-Translator: Jochum van der Heide <jochum@famvanderheide.com>\n"
|
||||
"PO-Revision-Date: 2024-02-10 12:20+0000\n"
|
||||
"Last-Translator: Jonan B <jonanb@pm.me>\n"
|
||||
"Language-Team: Dutch <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/nl/>\n"
|
||||
"Language: nl\n"
|
||||
@ -159,7 +159,7 @@ msgstr "Naam"
|
||||
|
||||
#: .\cookbook\forms.py:124 .\cookbook\forms.py:315 .\cookbook\views\lists.py:88
|
||||
msgid "Keywords"
|
||||
msgstr "Etiketten"
|
||||
msgstr "Trefwoorden"
|
||||
|
||||
#: .\cookbook\forms.py:125
|
||||
msgid "Preparation time in minutes"
|
||||
@ -1224,7 +1224,7 @@ msgstr "Markdown gids"
|
||||
|
||||
#: .\cookbook\templates\base.html:329
|
||||
msgid "GitHub"
|
||||
msgstr "GitHub"
|
||||
msgstr "Github"
|
||||
|
||||
#: .\cookbook\templates\base.html:331
|
||||
msgid "Translate Tandoor"
|
||||
|
@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-05-18 14:28+0200\n"
|
||||
"PO-Revision-Date: 2023-02-26 13:15+0000\n"
|
||||
"Last-Translator: 吕楪 <thy@irithys.com>\n"
|
||||
"PO-Revision-Date: 2024-02-15 03:19+0000\n"
|
||||
"Last-Translator: dalan <xzdlj@outlook.com>\n"
|
||||
"Language-Team: Chinese (Simplified) <http://translate.tandoor.dev/projects/"
|
||||
"tandoor/recipes-backend/zh_Hans/>\n"
|
||||
"Language: zh_CN\n"
|
||||
@ -480,34 +480,32 @@ msgid "One of queryset or hash_key must be provided"
|
||||
msgstr "必须提供 queryset 或 hash_key 之一"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:266
|
||||
#, fuzzy
|
||||
#| msgid "Use fractions"
|
||||
msgid "reverse rotation"
|
||||
msgstr "使用分数"
|
||||
msgstr "反向旋转"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:267
|
||||
msgid "careful rotation"
|
||||
msgstr ""
|
||||
msgstr "小心旋转"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:268
|
||||
msgid "knead"
|
||||
msgstr ""
|
||||
msgstr "揉"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:269
|
||||
msgid "thicken"
|
||||
msgstr ""
|
||||
msgstr "增稠"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:270
|
||||
msgid "warm up"
|
||||
msgstr ""
|
||||
msgstr "预热"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:271
|
||||
msgid "ferment"
|
||||
msgstr ""
|
||||
msgstr "发酵"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:272
|
||||
msgid "sous-vide"
|
||||
msgstr ""
|
||||
msgstr "真空烹调法"
|
||||
|
||||
#: .\cookbook\helper\shopping_helper.py:157
|
||||
msgid "You must supply a servings size"
|
||||
@ -549,10 +547,8 @@ msgid "Imported %s recipes."
|
||||
msgstr "导入了%s菜谱。"
|
||||
|
||||
#: .\cookbook\integration\openeats.py:26
|
||||
#, fuzzy
|
||||
#| msgid "Recipe Home"
|
||||
msgid "Recipe source:"
|
||||
msgstr "菜谱主页"
|
||||
msgstr "菜谱来源:"
|
||||
|
||||
#: .\cookbook\integration\paprika.py:49
|
||||
msgid "Notes"
|
||||
|
@ -6,11 +6,12 @@ from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from cookbook.models import PermissionModelMixin, ShoppingListEntry
|
||||
from cookbook.models import PermissionModelMixin
|
||||
|
||||
|
||||
def copy_values_to_sle(apps, schema_editor):
|
||||
with scopes_disabled():
|
||||
ShoppingListEntry = apps.get_model('cookbook', 'ShoppingListEntry')
|
||||
entries = ShoppingListEntry.objects.all()
|
||||
for entry in entries:
|
||||
if entry.shoppinglist_set.first():
|
||||
|
@ -1,25 +1,22 @@
|
||||
# Generated by Django 3.2.7 on 2021-10-01 22:34
|
||||
|
||||
import datetime
|
||||
from datetime import timedelta
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
from django.db import migrations
|
||||
from django.utils import timezone
|
||||
from django.utils.timezone import utc
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from cookbook.models import FoodInheritField, ShoppingListEntry
|
||||
|
||||
|
||||
def delete_orphaned_sle(apps, schema_editor):
|
||||
ShoppingListEntry = apps.get_model('cookbook', 'ShoppingListEntry')
|
||||
with scopes_disabled():
|
||||
# shopping list entry is orphaned - delete it
|
||||
ShoppingListEntry.objects.filter(shoppinglist=None).delete()
|
||||
|
||||
|
||||
def create_inheritfields(apps, schema_editor):
|
||||
FoodInheritField = apps.get_model('cookbook', 'FoodInheritField')
|
||||
FoodInheritField.objects.create(name='Supermarket Category', field='supermarket_category')
|
||||
FoodInheritField.objects.create(name='On Hand', field='food_onhand')
|
||||
FoodInheritField.objects.create(name='Diet', field='diet')
|
||||
@ -29,6 +26,7 @@ def create_inheritfields(apps, schema_editor):
|
||||
|
||||
|
||||
def set_completed_at(apps, schema_editor):
|
||||
ShoppingListEntry = apps.get_model('cookbook', 'ShoppingListEntry')
|
||||
today_start = timezone.now().replace(hour=0, minute=0, second=0)
|
||||
# arbitrary - keeping all of the closed shopping list items out of the 'recent' view
|
||||
month_ago = today_start - timedelta(days=30)
|
||||
|
18
cookbook/migrations/0210_shoppinglistentry_updated_at.py
Normal file
18
cookbook/migrations/0210_shoppinglistentry_updated_at.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.7 on 2024-01-28 10:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0209_remove_space_use_plural'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='shoppinglistentry',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
]
|
@ -789,7 +789,7 @@ class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, Permiss
|
||||
objects = ScopedManager(space='space')
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.pk}: {self.amount} {self.food.name} {self.unit.name}'
|
||||
return f'{self.pk}: {self.amount} ' + (self.food.name if self.food else ' ') + (self.unit.name if self.unit else '')
|
||||
|
||||
class Meta:
|
||||
ordering = ['order', 'pk']
|
||||
@ -1009,6 +1009,8 @@ class RecipeBook(ExportModelOperationsMixin('book'), models.Model, PermissionMod
|
||||
shared = models.ManyToManyField(User, blank=True, related_name='shared_with')
|
||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
filter = models.ForeignKey('cookbook.CustomFilter', null=True, blank=True, on_delete=models.SET_NULL)
|
||||
order = models.IntegerField(default=0)
|
||||
|
||||
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||
objects = ScopedManager(space='space')
|
||||
@ -1125,6 +1127,8 @@ class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'), model
|
||||
checked = models.BooleanField(default=False)
|
||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
completed_at = models.DateTimeField(null=True, blank=True)
|
||||
delay_until = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
|
@ -57,7 +57,8 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
|
||||
api_serializer = None
|
||||
# extended values are computationally expensive and not needed in normal circumstances
|
||||
try:
|
||||
if str2bool(self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer:
|
||||
if str2bool(
|
||||
self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer:
|
||||
return fields
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
@ -81,12 +82,14 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
|
||||
class OpenDataModelMixin(serializers.ModelSerializer):
|
||||
|
||||
def create(self, validated_data):
|
||||
if 'open_data_slug' in validated_data and validated_data['open_data_slug'] is not None and validated_data['open_data_slug'].strip() == '':
|
||||
if 'open_data_slug' in validated_data and validated_data['open_data_slug'] is not None and validated_data[
|
||||
'open_data_slug'].strip() == '':
|
||||
validated_data['open_data_slug'] = None
|
||||
return super().create(validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
if 'open_data_slug' in validated_data and validated_data['open_data_slug'] is not None and validated_data['open_data_slug'].strip() == '':
|
||||
if 'open_data_slug' in validated_data and validated_data['open_data_slug'] is not None and validated_data[
|
||||
'open_data_slug'].strip() == '':
|
||||
validated_data['open_data_slug'] = None
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
@ -122,7 +125,8 @@ class CustomOnHandField(serializers.Field):
|
||||
if not self.context["request"].user.is_authenticated:
|
||||
return []
|
||||
shared_users = []
|
||||
if c := caches['default'].get(f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}', None):
|
||||
if c := caches['default'].get(
|
||||
f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}', None):
|
||||
shared_users = c
|
||||
else:
|
||||
try:
|
||||
@ -332,7 +336,8 @@ class UserSpaceSerializer(WritableNestedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = UserSpace
|
||||
fields = ('id', 'user', 'space', 'groups', 'active', 'internal_note', 'invite_link', 'created_at', 'updated_at',)
|
||||
fields = (
|
||||
'id', 'user', 'space', 'groups', 'active', 'internal_note', 'invite_link', 'created_at', 'updated_at',)
|
||||
read_only_fields = ('id', 'invite_link', 'created_at', 'updated_at', 'space')
|
||||
|
||||
|
||||
@ -382,13 +387,15 @@ class UserPreferenceSerializer(WritableNestedModelSerializer):
|
||||
class Meta:
|
||||
model = UserPreference
|
||||
fields = (
|
||||
'user', 'image', 'theme', 'nav_bg_color', 'nav_text_color', 'nav_show_logo', 'default_unit', 'default_page', 'use_fractions', 'use_kj',
|
||||
'user', 'image', 'theme', 'nav_bg_color', 'nav_text_color', 'nav_show_logo', 'default_unit', 'default_page',
|
||||
'use_fractions', 'use_kj',
|
||||
'plan_share', 'nav_sticky',
|
||||
'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping',
|
||||
'food_inherit_default', 'default_delay',
|
||||
'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days',
|
||||
'csv_delim', 'csv_prefix',
|
||||
'filter_to_supermarket', 'shopping_add_onhand', 'left_handed', 'show_step_ingredients', 'food_children_exist'
|
||||
'filter_to_supermarket', 'shopping_add_onhand', 'left_handed', 'show_step_ingredients',
|
||||
'food_children_exist'
|
||||
)
|
||||
|
||||
|
||||
@ -498,10 +505,13 @@ class UnitSerializer(UniqueFieldsMixin, ExtendedRecipeMixin, OpenDataModelMixin)
|
||||
if x := validated_data.get('name', None):
|
||||
validated_data['plural_name'] = x.strip()
|
||||
|
||||
if unit := Unit.objects.filter(Q(name__iexact=validated_data['name']) | Q(plural_name__iexact=validated_data['name']), space=space).first():
|
||||
if unit := Unit.objects.filter(
|
||||
Q(name__iexact=validated_data['name']) | Q(plural_name__iexact=validated_data['name']),
|
||||
space=space).first():
|
||||
return unit
|
||||
|
||||
obj, created = Unit.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
|
||||
obj, created = Unit.objects.get_or_create(name__iexact=validated_data['name'], space=space,
|
||||
defaults=validated_data)
|
||||
return obj
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
@ -521,7 +531,8 @@ class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerial
|
||||
def create(self, validated_data):
|
||||
validated_data['name'] = validated_data['name'].strip()
|
||||
space = validated_data.pop('space', self.context['request'].space)
|
||||
obj, created = SupermarketCategory.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
|
||||
obj, created = SupermarketCategory.objects.get_or_create(name__iexact=validated_data['name'], space=space,
|
||||
defaults=validated_data)
|
||||
return obj
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
@ -546,7 +557,8 @@ class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer, OpenDataMo
|
||||
def create(self, validated_data):
|
||||
validated_data['name'] = validated_data['name'].strip()
|
||||
space = validated_data.pop('space', self.context['request'].space)
|
||||
obj, created = Supermarket.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
|
||||
obj, created = Supermarket.objects.get_or_create(name__iexact=validated_data['name'], space=space,
|
||||
defaults=validated_data)
|
||||
return obj
|
||||
|
||||
class Meta:
|
||||
@ -561,7 +573,8 @@ class PropertyTypeSerializer(OpenDataModelMixin, WritableNestedModelSerializer,
|
||||
def create(self, validated_data):
|
||||
validated_data['name'] = validated_data['name'].strip()
|
||||
space = validated_data.pop('space', self.context['request'].space)
|
||||
obj, created = PropertyType.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
|
||||
obj, created = PropertyType.objects.get_or_create(name__iexact=validated_data['name'], space=space,
|
||||
defaults=validated_data)
|
||||
return obj
|
||||
|
||||
class Meta:
|
||||
@ -686,12 +699,14 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
||||
|
||||
properties = validated_data.pop('properties', None)
|
||||
|
||||
obj, created = Food.objects.get_or_create(name=name, plural_name=plural_name, space=space, properties_food_unit=properties_food_unit,
|
||||
obj, created = Food.objects.get_or_create(name=name, plural_name=plural_name, space=space,
|
||||
properties_food_unit=properties_food_unit,
|
||||
defaults=validated_data)
|
||||
|
||||
if properties and len(properties) > 0:
|
||||
for p in properties:
|
||||
obj.properties.add(Property.objects.create(property_type_id=p['property_type']['id'], property_amount=p['property_amount'], space=space))
|
||||
obj.properties.add(Property.objects.create(property_type_id=p['property_type']['id'],
|
||||
property_amount=p['property_amount'], space=space))
|
||||
|
||||
return obj
|
||||
|
||||
@ -723,7 +738,8 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
||||
'properties', 'properties_food_amount', 'properties_food_unit', 'fdc_id',
|
||||
'food_onhand', 'supermarket_category',
|
||||
'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name', 'ignore_shopping',
|
||||
'substitute', 'substitute_siblings', 'substitute_children', 'substitute_onhand', 'child_inherit_fields', 'open_data_slug',
|
||||
'substitute', 'substitute_siblings', 'substitute_children', 'substitute_onhand', 'child_inherit_fields',
|
||||
'open_data_slug',
|
||||
)
|
||||
read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe')
|
||||
|
||||
@ -747,7 +763,8 @@ class IngredientSimpleSerializer(WritableNestedModelSerializer):
|
||||
uch = UnitConversionHelper(self.context['request'].space)
|
||||
conversions = []
|
||||
for c in uch.get_conversions(obj):
|
||||
conversions.append({'food': c.food.name, 'unit': c.unit.name, 'amount': c.amount}) # TODO do formatting in helper
|
||||
conversions.append(
|
||||
{'food': c.food.name, 'unit': c.unit.name, 'amount': c.amount}) # TODO do formatting in helper
|
||||
return conversions
|
||||
else:
|
||||
return []
|
||||
@ -849,7 +866,8 @@ class UnitConversionSerializer(WritableNestedModelSerializer, OpenDataModelMixin
|
||||
|
||||
class Meta:
|
||||
model = UnitConversion
|
||||
fields = ('id', 'name', 'base_amount', 'base_unit', 'converted_amount', 'converted_unit', 'food', 'open_data_slug')
|
||||
fields = (
|
||||
'id', 'name', 'base_amount', 'base_unit', 'converted_amount', 'converted_unit', 'food', 'open_data_slug')
|
||||
|
||||
|
||||
class NutritionInformationSerializer(serializers.ModelSerializer):
|
||||
@ -919,7 +937,8 @@ class RecipeSerializer(RecipeBaseSerializer):
|
||||
fields = (
|
||||
'id', 'name', 'description', 'image', 'keywords', 'steps', 'working_time',
|
||||
'waiting_time', 'created_by', 'created_at', 'updated_at', 'source_url',
|
||||
'internal', 'show_ingredient_overview', 'nutrition', 'properties', 'food_properties', 'servings', 'file_path', 'servings_text', 'rating',
|
||||
'internal', 'show_ingredient_overview', 'nutrition', 'properties', 'food_properties', 'servings',
|
||||
'file_path', 'servings_text', 'rating',
|
||||
'last_cooked',
|
||||
'private', 'shared',
|
||||
)
|
||||
@ -981,7 +1000,7 @@ class RecipeBookSerializer(SpacedModelSerializer, WritableNestedModelSerializer)
|
||||
|
||||
class Meta:
|
||||
model = RecipeBook
|
||||
fields = ('id', 'name', 'description', 'shared', 'created_by', 'filter')
|
||||
fields = ('id', 'name', 'description', 'shared', 'created_by', 'filter', 'order')
|
||||
read_only_fields = ('created_by',)
|
||||
|
||||
|
||||
@ -998,7 +1017,8 @@ class RecipeBookEntrySerializer(serializers.ModelSerializer):
|
||||
def create(self, validated_data):
|
||||
book = validated_data['book']
|
||||
recipe = validated_data['recipe']
|
||||
if not book.get_owner() == self.context['request'].user and not self.context['request'].user in book.get_shared():
|
||||
if not book.get_owner() == self.context['request'].user and not self.context[
|
||||
'request'].user in book.get_shared():
|
||||
raise NotFound(detail=None, code=None)
|
||||
obj, created = RecipeBookEntry.objects.get_or_create(book=book, recipe=recipe)
|
||||
return obj
|
||||
@ -1062,6 +1082,8 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
|
||||
name = serializers.SerializerMethodField('get_name') # should this be done at the front end?
|
||||
recipe_name = serializers.ReadOnlyField(source='recipe.name')
|
||||
mealplan_note = serializers.ReadOnlyField(source='mealplan.note')
|
||||
mealplan_from_date = serializers.ReadOnlyField(source='mealplan.from_date')
|
||||
mealplan_type = serializers.ReadOnlyField(source='mealplan.meal_type.name')
|
||||
servings = CustomDecimalField()
|
||||
|
||||
def get_name(self, obj):
|
||||
@ -1085,14 +1107,14 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ShoppingListRecipe
|
||||
fields = ('id', 'recipe_name', 'name', 'recipe', 'mealplan', 'servings', 'mealplan_note')
|
||||
fields = ('id', 'recipe_name', 'name', 'recipe', 'mealplan', 'servings', 'mealplan_note', 'mealplan_from_date',
|
||||
'mealplan_type')
|
||||
read_only_fields = ('id',)
|
||||
|
||||
|
||||
class ShoppingListEntrySerializer(WritableNestedModelSerializer):
|
||||
food = FoodSerializer(allow_null=True)
|
||||
unit = UnitSerializer(allow_null=True, required=False)
|
||||
ingredient_note = serializers.ReadOnlyField(source='ingredient.note')
|
||||
recipe_mealplan = ShoppingListRecipeSerializer(source='list_recipe', read_only=True)
|
||||
amount = CustomDecimalField()
|
||||
created_by = UserSerializer(read_only=True)
|
||||
@ -1103,7 +1125,7 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
|
||||
|
||||
# autosync values are only needed for frequent 'checked' value updating
|
||||
if self.context['request'] and bool(int(self.context['request'].query_params.get('autosync', False))):
|
||||
for f in list(set(fields) - set(['id', 'checked'])):
|
||||
for f in list(set(fields) - set(['id', 'checked', 'updated_at', ])):
|
||||
del fields[f]
|
||||
return fields
|
||||
|
||||
@ -1145,11 +1167,16 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
|
||||
class Meta:
|
||||
model = ShoppingListEntry
|
||||
fields = (
|
||||
'id', 'list_recipe', 'food', 'unit', 'ingredient', 'ingredient_note', 'amount', 'order', 'checked',
|
||||
'id', 'list_recipe', 'food', 'unit', 'amount', 'order', 'checked',
|
||||
'recipe_mealplan',
|
||||
'created_by', 'created_at', 'completed_at', 'delay_until'
|
||||
'created_by', 'created_at', 'updated_at', 'completed_at', 'delay_until'
|
||||
)
|
||||
read_only_fields = ('id', 'created_by', 'created_at',)
|
||||
read_only_fields = ('id', 'created_by', 'created_at','updated_at',)
|
||||
|
||||
|
||||
class ShoppingListEntryBulkSerializer(serializers.Serializer):
|
||||
ids = serializers.ListField()
|
||||
checked = serializers.BooleanField()
|
||||
|
||||
|
||||
# TODO deprecate
|
||||
@ -1304,7 +1331,8 @@ class InviteLinkSerializer(WritableNestedModelSerializer):
|
||||
class Meta:
|
||||
model = InviteLink
|
||||
fields = (
|
||||
'id', 'uuid', 'email', 'group', 'valid_until', 'used_by', 'reusable', 'internal_note', 'created_by', 'created_at',)
|
||||
'id', 'uuid', 'email', 'group', 'valid_until', 'used_by', 'reusable', 'internal_note', 'created_by',
|
||||
'created_at',)
|
||||
read_only_fields = ('id', 'uuid', 'created_by', 'created_at',)
|
||||
|
||||
|
||||
|
2
cookbook/static/themes/tandoor.min.css
vendored
2
cookbook/static/themes/tandoor.min.css
vendored
@ -480,7 +480,7 @@ hr {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, .1)
|
||||
border-top: 1px solid #ced4da;
|
||||
}
|
||||
|
||||
.small, small {
|
||||
|
50
cookbook/static/themes/tandoor_dark.min.css
vendored
50
cookbook/static/themes/tandoor_dark.min.css
vendored
@ -2850,89 +2850,41 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: transparent;
|
||||
color: #b98766;
|
||||
border: 1px solid #b98766
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
transition: all .5s ease-in-out;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: transparent;
|
||||
color: #b55e4f;
|
||||
border: 1px solid #b55e4f
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
transition: all .5s ease-in-out;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: transparent;
|
||||
color: #82aa8b;
|
||||
border: 1px solid #82aa8b
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
transition: all .5s ease-in-out;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.btn-info:hover {
|
||||
background: transparent;
|
||||
color: #385f84;
|
||||
border: 1px solid #385f84
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
transition: all .5s ease-in-out;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background: transparent;
|
||||
color: #eaaa21;
|
||||
border: 1px solid #eaaa21
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
transition: all .5s ease-in-out;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: transparent;
|
||||
color: #a7240e;
|
||||
border: 1px solid #a7240e
|
||||
}
|
||||
|
||||
.btn-light {
|
||||
transition: all .5s ease-in-out;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.btn-light:hover {
|
||||
background-color: hsla(0, 0%, 18%, .5);
|
||||
color: #cfd5cd;
|
||||
border: 1px solid hsla(0, 0%, 18%, .5)
|
||||
}
|
||||
|
||||
.btn-dark {
|
||||
transition: all .5s ease-in-out;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.btn-dark:hover {
|
||||
background: transparent;
|
||||
color: #221e1e;
|
||||
border: 1px solid #221e1e
|
||||
}
|
||||
|
||||
.btn-opacity-primary {
|
||||
color: #b98766;
|
||||
background-color: #0012a7;
|
||||
@ -6155,7 +6107,7 @@ a.close.disabled {
|
||||
padding: .5rem .75rem;
|
||||
margin-bottom: 0;
|
||||
font-size: 1rem;
|
||||
background-color: #f7f7f7;
|
||||
background-color: #242424;
|
||||
border-bottom: 1px solid #ebebeb;
|
||||
border-top-left-radius: calc(.3rem - 1px);
|
||||
border-top-right-radius: calc(.3rem - 1px)
|
||||
|
@ -34,5 +34,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-sm-12 col-lg-6 col-md-6 offset-lg-3 offset-md-3 text-center">
|
||||
<a href="{% url 'account_login' %}">{% trans "Sign In" %}</a>
|
||||
{% if SIGNUP_ENABLED %}
|
||||
- <a href="{% url 'account_signup' %}">{% trans "Sign Up" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
@ -7,11 +7,32 @@
|
||||
{% block title %}{% trans "Password Reset" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h3>{% trans "Password Reset" %}</h3>
|
||||
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
{% include "account/snippets/already_logged_in.html" %}
|
||||
{% include "account/snippets/already_logged_in.html" %}
|
||||
{% endif %}
|
||||
|
||||
<p>{% blocktrans %}We have sent you an e-mail. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}</p>
|
||||
<div class="row">
|
||||
<div class="col-12" style="text-align: center">
|
||||
<h3>{% trans "Password Reset" %}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-lg-6 col-md-6 offset-lg-3 offset-md-3">
|
||||
<hr>
|
||||
<p>{% blocktrans %}We have sent you an e-mail. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-sm-12 col-lg-6 col-md-6 offset-lg-3 offset-md-3 text-center">
|
||||
<a href="{% url 'account_login' %}">{% trans "Sign In" %}</a>
|
||||
{% if SIGNUP_ENABLED %}
|
||||
- <a href="{% url 'account_signup' %}">{% trans "Sign Up" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@ -408,7 +408,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="container-fluid mt-2 mt-md-5 mt-xl-5 mt-lg-5{% if request.user.userpreference.left_handed %} left-handed {% endif %}"
|
||||
<div class="container-fluid mt-2 mt-md-3 mt-xl-3 mt-lg-3{% if request.user.userpreference.left_handed %} left-handed {% endif %}"
|
||||
id="id_base_container">
|
||||
<div class="row">
|
||||
<div class="col-xl-2 d-none d-xl-block">
|
||||
|
@ -79,6 +79,7 @@
|
||||
|
||||
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
|
||||
window.RECIPE_ID = {{recipe.pk}};
|
||||
window.RECIPE_SERVINGS = '{{ servings }}'
|
||||
window.SHARE_UID = '{{ share }}';
|
||||
window.USER_PREF = {
|
||||
'use_fractions': {% if request.user.userpreference.use_fractions %} true {% else %} false {% endif %},
|
||||
|
@ -2,6 +2,7 @@
|
||||
{% load render_bundle from webpack_loader %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %} {{ title }} {% endblock %}
|
||||
|
||||
{% block content_fluid %}
|
||||
@ -10,16 +11,21 @@
|
||||
<shopping-list-view></shopping-list-view>
|
||||
</div>
|
||||
|
||||
{% endblock %} {% block script %} {% if debug %}
|
||||
<script src="{% url 'js_reverse' %}"></script>
|
||||
{% else %}
|
||||
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
<script type="application/javascript">
|
||||
window.IMAGE_PLACEHOLDER = "{% static 'assets/recipe_no_image.svg' %}"
|
||||
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
|
||||
window.SHOPPING_MIN_AUTOSYNC_INTERVAL = {{ SHOPPING_MIN_AUTOSYNC_INTERVAL }}
|
||||
</script>
|
||||
{% block script %}
|
||||
{% if debug %}
|
||||
<script src="{% url 'js_reverse' %}"></script>
|
||||
{% else %}
|
||||
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
|
||||
{% endif %}
|
||||
|
||||
{% render_bundle 'shopping_list_view' %} {% endblock %}
|
||||
<script type="application/javascript">
|
||||
window.IMAGE_PLACEHOLDER = "{% static 'assets/recipe_no_image.svg' %}"
|
||||
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
|
||||
window.SHOPPING_MIN_AUTOSYNC_INTERVAL = {{ SHOPPING_MIN_AUTOSYNC_INTERVAL }}
|
||||
</script>
|
||||
|
||||
|
||||
{% render_bundle 'shopping_list_view' %}
|
||||
{% endblock %}
|
||||
|
@ -13,7 +13,9 @@ DETAIL_URL = 'api:shoppinglistentry-detail'
|
||||
|
||||
@pytest.fixture()
|
||||
def obj_1(space_1, u1_s1):
|
||||
e = ShoppingListEntry.objects.create(created_by=auth.get_user(u1_s1), food=Food.objects.get_or_create(name='test 1', space=space_1)[0], space=space_1)
|
||||
e = ShoppingListEntry.objects.create(created_by=auth.get_user(u1_s1),
|
||||
food=Food.objects.get_or_create(name='test 1', space=space_1)[0],
|
||||
space=space_1)
|
||||
s = ShoppingList.objects.create(created_by=auth.get_user(u1_s1), space=space_1, )
|
||||
s.entries.add(e)
|
||||
return e
|
||||
@ -21,7 +23,9 @@ def obj_1(space_1, u1_s1):
|
||||
|
||||
@pytest.fixture
|
||||
def obj_2(space_1, u1_s1):
|
||||
e = ShoppingListEntry.objects.create(created_by=auth.get_user(u1_s1), food=Food.objects.get_or_create(name='test 2', space=space_1)[0], space=space_1)
|
||||
e = ShoppingListEntry.objects.create(created_by=auth.get_user(u1_s1),
|
||||
food=Food.objects.get_or_create(name='test 2', space=space_1)[0],
|
||||
space=space_1)
|
||||
s = ShoppingList.objects.create(created_by=auth.get_user(u1_s1), space=space_1, )
|
||||
s.entries.add(e)
|
||||
return e
|
||||
@ -79,6 +83,29 @@ def test_update(arg, request, obj_1):
|
||||
assert response['amount'] == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
['a_u', 403],
|
||||
['g1_s1', 403],
|
||||
['u1_s1', 200],
|
||||
['a1_s1', 200],
|
||||
['g1_s2', 403],
|
||||
['u1_s2', 200],
|
||||
['a1_s2', 200],
|
||||
])
|
||||
def test_bulk_update(arg, request, obj_1, obj_2):
|
||||
c = request.getfixturevalue(arg[0])
|
||||
r = c.post(
|
||||
reverse(LIST_URL, ) + 'bulk/',
|
||||
{'ids': [obj_1.id, obj_2.id], 'checked': True},
|
||||
content_type='application/json'
|
||||
)
|
||||
assert r.status_code == arg[1]
|
||||
assert r
|
||||
if r.status_code == 200:
|
||||
obj_1.refresh_from_db()
|
||||
assert obj_1.checked == (arg[0] == 'u1_s1')
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
['a_u', 403],
|
||||
['g1_s1', 201],
|
||||
|
@ -214,6 +214,9 @@ def test_completed(sle, u1_s1):
|
||||
|
||||
def test_recent(sle, u1_s1):
|
||||
user = auth.get_user(u1_s1)
|
||||
user.userpreference.shopping_recent_days = 7 # hardcoded API limit 14 days
|
||||
user.userpreference.save()
|
||||
|
||||
today_start = timezone.now().replace(hour=0, minute=0, second=0)
|
||||
|
||||
# past_date within recent_days threshold
|
||||
|
@ -7,7 +7,7 @@ from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from cookbook.models import Food, Ingredient
|
||||
from cookbook.models import Food, Ingredient, ShoppingListRecipe, ShoppingListEntry
|
||||
from cookbook.tests.factories import MealPlanFactory, RecipeFactory, StepFactory, UserFactory
|
||||
|
||||
if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql':
|
||||
@ -126,7 +126,7 @@ def test_shopping_recipe_edit(request, recipe, sle_count, use_mealplan, u1_s1, u
|
||||
u1_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}))
|
||||
r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)
|
||||
assert [x['created_by']['id'] for x in r].count(user.id) == sle_count
|
||||
all_ing = [x['ingredient'] for x in r]
|
||||
all_ing = list(ShoppingListEntry.objects.filter(list_recipe__recipe=recipe).all().values_list('ingredient', flat=True))
|
||||
keep_ing = all_ing[1:-1] # remove first and last element
|
||||
del keep_ing[int(len(keep_ing) / 2)] # remove a middle element
|
||||
list_recipe = r[0]['list_recipe']
|
||||
|
@ -30,6 +30,7 @@ from django.http import FileResponse, HttpResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.timezone import make_aware
|
||||
from django.utils.translation import gettext as _
|
||||
from django_scopes import scopes_disabled
|
||||
from icalendar import Calendar, Event
|
||||
@ -102,7 +103,8 @@ from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer,
|
||||
SupermarketCategorySerializer, SupermarketSerializer,
|
||||
SyncLogSerializer, SyncSerializer, UnitConversionSerializer,
|
||||
UnitSerializer, UserFileSerializer, UserPreferenceSerializer,
|
||||
UserSerializer, UserSpaceSerializer, ViewLogSerializer, ConnectorConfigConfigSerializer)
|
||||
UserSerializer, UserSpaceSerializer, ViewLogSerializer,
|
||||
ShoppingListEntryBulkSerializer, ConnectorConfigConfigSerializer)
|
||||
from cookbook.views.import_export import get_integration
|
||||
from recipes import settings
|
||||
from recipes.settings import FDC_API_KEY, DRF_THROTTLE_RECIPE_URL_IMPORT
|
||||
@ -489,6 +491,7 @@ class SyncLogViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
|
||||
|
||||
class SupermarketViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
||||
schema = FilterSchema()
|
||||
queryset = Supermarket.objects
|
||||
serializer_class = SupermarketSerializer
|
||||
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
|
||||
@ -498,7 +501,7 @@ class SupermarketViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
||||
return super().get_queryset()
|
||||
|
||||
|
||||
class SupermarketCategoryViewSet(viewsets.ModelViewSet, FuzzyFilterMixin):
|
||||
class SupermarketCategoryViewSet(viewsets.ModelViewSet, FuzzyFilterMixin, MergeMixin):
|
||||
queryset = SupermarketCategory.objects
|
||||
model = SupermarketCategory
|
||||
serializer_class = SupermarketCategorySerializer
|
||||
@ -668,8 +671,16 @@ class RecipeBookViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
||||
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
|
||||
|
||||
def get_queryset(self):
|
||||
order_field = self.request.GET.get('order_field')
|
||||
order_direction = self.request.GET.get('order_direction')
|
||||
|
||||
if not order_field:
|
||||
order_field = 'id'
|
||||
|
||||
ordering = f"{'' if order_direction == 'asc' else '-'}{order_field}"
|
||||
|
||||
self.queryset = self.queryset.filter(Q(created_by=self.request.user) | Q(shared=self.request.user)).filter(
|
||||
space=self.request.space).distinct()
|
||||
space=self.request.space).distinct().order_by(ordering)
|
||||
return super().get_queryset()
|
||||
|
||||
|
||||
@ -1169,11 +1180,47 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
|
||||
if pk := self.request.query_params.getlist('id', []):
|
||||
self.queryset = self.queryset.filter(food__id__in=[int(i) for i in pk])
|
||||
|
||||
if 'checked' in self.request.query_params or 'recent' in self.request.query_params:
|
||||
if 'checked' in self.request.query_params:
|
||||
return shopping_helper(self.queryset, self.request)
|
||||
elif not self.detail:
|
||||
today_start = timezone.now().replace(hour=0, minute=0, second=0)
|
||||
week_ago = today_start - datetime.timedelta(days=min(self.request.user.userpreference.shopping_recent_days, 14))
|
||||
self.queryset = self.queryset.filter(Q(checked=False) | Q(completed_at__gte=week_ago))
|
||||
|
||||
try:
|
||||
last_autosync = self.request.query_params.get('last_autosync', None)
|
||||
if last_autosync:
|
||||
last_autosync = datetime.datetime.fromtimestamp(int(last_autosync) / 1000, datetime.timezone.utc)
|
||||
self.queryset = self.queryset.filter(updated_at__gte=last_autosync)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
# TODO once old shopping list is removed this needs updated to sharing users in preferences
|
||||
return self.queryset
|
||||
if self.detail:
|
||||
return self.queryset
|
||||
else:
|
||||
return self.queryset[:1000]
|
||||
|
||||
@decorators.action(
|
||||
detail=False,
|
||||
methods=['POST'],
|
||||
serializer_class=ShoppingListEntryBulkSerializer,
|
||||
permission_classes=[CustomIsUser]
|
||||
)
|
||||
def bulk(self, request):
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
if serializer.is_valid():
|
||||
ShoppingListEntry.objects.filter(
|
||||
Q(created_by=self.request.user)
|
||||
| Q(shoppinglist__shared=self.request.user)
|
||||
| Q(created_by__in=list(self.request.user.get_shopping_share()))
|
||||
).filter(space=request.space, id__in=serializer.validated_data['ids']).update(
|
||||
checked=serializer.validated_data['checked'],
|
||||
updated_at=timezone.now(),
|
||||
)
|
||||
return Response(serializer.data)
|
||||
else:
|
||||
return Response(serializer.errors, 400)
|
||||
|
||||
|
||||
# TODO deprecate
|
||||
@ -1183,11 +1230,13 @@ class ShoppingListViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.queryset.filter(
|
||||
self.queryset = self.queryset.filter(
|
||||
Q(created_by=self.request.user)
|
||||
| Q(shared=self.request.user)
|
||||
| Q(created_by__in=list(self.request.user.get_shopping_share()))
|
||||
).filter(space=self.request.space).distinct()
|
||||
).filter(space=self.request.space)
|
||||
|
||||
return self.queryset.distinct()
|
||||
|
||||
def get_serializer_class(self):
|
||||
try:
|
||||
@ -1256,6 +1305,7 @@ class BookmarkletImportViewSet(viewsets.ModelViewSet):
|
||||
|
||||
|
||||
class UserFileViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
||||
schema = FilterSchema()
|
||||
queryset = UserFile.objects
|
||||
serializer_class = UserFileSerializer
|
||||
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
|
||||
|
@ -171,9 +171,10 @@ def recipe_view(request, pk, share=None):
|
||||
created_at__gt=(timezone.now() - timezone.timedelta(minutes=5)),
|
||||
space=request.space).exists():
|
||||
ViewLog.objects.create(recipe=recipe, created_by=request.user, space=request.space)
|
||||
|
||||
if request.method == "GET":
|
||||
servings = request.GET.get("servings")
|
||||
return render(request, 'recipe_view.html',
|
||||
{'recipe': recipe, 'comments': comments, 'comment_form': comment_form, 'share': share, })
|
||||
{'recipe': recipe, 'comments': comments, 'comment_form': comment_form, 'share': share, 'servings': servings })
|
||||
|
||||
|
||||
@group_required('user')
|
||||
|
@ -65,8 +65,9 @@ At Keycloak, create a new client and assign a `Client-ID`, this client comes wit
|
||||
|
||||
To enable Keycloak as a sign in option, set those variables to define the social provider and specify its configuration:
|
||||
```ini
|
||||
SOCIAL_PROVIDERS=allauth.socialaccount.providers.keycloak
|
||||
SOCIALACCOUNT_PROVIDERS='{ "keycloak": { "KEYCLOAK_URL": "https://auth.example.com/", "KEYCLOAK_REALM": "master" } }'
|
||||
SOCIAL_PROVIDERS=allauth.socialaccount.providers.openid_connect
|
||||
SOCIALACCOUNT_PROVIDERS='{"openid_connect":{"APPS":[{"provider_id":"keycloak","name":"Keycloak","client_id":"KEYCLOAK_CLIENT_ID","secret":"KEYCLOAK_CLIENT_SECRET","settings":{"server_url":"https://auth.example.org/realms/KEYCLOAK_REALM/.well-known/openid-configuration"}}]}}
|
||||
'
|
||||
```
|
||||
|
||||
1. Restart the service, login as superuser and open the `Admin` page.
|
||||
|
@ -53,7 +53,7 @@ docker stop {{database_container}} {{tandoor_container}}
|
||||
4. Rename the tandoor volume
|
||||
|
||||
``` bash
|
||||
sudo mv -R ~/.docker/compose/postgres ~/.docker/compose/postgres.old
|
||||
sudo mv ~/.docker/compose/postgres ~/.docker/compose/postgres.old
|
||||
```
|
||||
|
||||
5. Update image tag on postgres container.
|
||||
|
@ -98,8 +98,6 @@ FDC_API_KEY = os.getenv('FDC_API_KEY', 'DEMO_KEY')
|
||||
SHARING_ABUSE = bool(int(os.getenv('SHARING_ABUSE', False)))
|
||||
SHARING_LIMIT = int(os.getenv('SHARING_LIMIT', 0))
|
||||
|
||||
ACCOUNT_SIGNUP_FORM_CLASS = 'cookbook.forms.AllAuthSignupForm'
|
||||
|
||||
DRF_THROTTLE_RECIPE_URL_IMPORT = os.getenv('DRF_THROTTLE_RECIPE_URL_IMPORT', '60/hour')
|
||||
|
||||
TERMS_URL = os.getenv('TERMS_URL', '')
|
||||
@ -556,6 +554,21 @@ DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'webmaster@localhost')
|
||||
ACCOUNT_EMAIL_SUBJECT_PREFIX = os.getenv(
|
||||
'ACCOUNT_EMAIL_SUBJECT_PREFIX', '[Tandoor Recipes] ') # allauth sender prefix
|
||||
|
||||
# ACCOUNT_SIGNUP_FORM_CLASS = 'cookbook.forms.AllAuthSignupForm'
|
||||
ACCOUNT_FORMS = {
|
||||
'signup': 'cookbook.forms.AllAuthSignupForm',
|
||||
'reset_password': 'cookbook.forms.CustomPasswordResetForm'
|
||||
}
|
||||
|
||||
ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS = False
|
||||
ACCOUNT_RATE_LIMITS = {
|
||||
"change_password": "1/m/user",
|
||||
"reset_password": "1/m/ip,1/m/key",
|
||||
"reset_password_from_key": "1/m/ip",
|
||||
"signup": "5/m/ip",
|
||||
"login": "5/m/ip",
|
||||
}
|
||||
|
||||
DISABLE_EXTERNAL_CONNECTORS = bool(int(os.getenv('DISABLE_EXTERNAL_CONNECTORS', False)))
|
||||
EXTERNAL_CONNECTORS_QUEUE_SIZE = int(os.getenv('EXTERNAL_CONNECTORS_QUEUE_SIZE', 100))
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
Django==4.2.7
|
||||
cryptography===41.0.7
|
||||
Django==4.2.10
|
||||
cryptography===42.0.0
|
||||
django-annoying==0.10.6
|
||||
django-autocomplete-light==3.9.7
|
||||
django-cleanup==8.0.0
|
||||
django-crispy-forms==2.0
|
||||
crispy-bootstrap4==2022.1
|
||||
django-tables2==2.5.3
|
||||
django-tables2==2.7.0
|
||||
djangorestframework==3.14.0
|
||||
drf-writable-nested==0.7.0
|
||||
django-oauth-toolkit==2.3.0
|
||||
@ -30,7 +30,7 @@ mock==5.1.0
|
||||
Jinja2==3.1.3
|
||||
django-webpack-loader==1.8.1
|
||||
git+https://github.com/BITSOLVER/django-js-reverse@071e304fd600107bc64bbde6f2491f1fe049ec82
|
||||
django-allauth==0.58.1
|
||||
django-allauth==0.61.1
|
||||
recipe-scrapers==14.52.0
|
||||
django-scopes==2.0.0
|
||||
pytest==7.4.3
|
||||
@ -42,8 +42,8 @@ django-storages==1.14.2
|
||||
boto3==1.28.75
|
||||
django-prometheus==2.2.0
|
||||
django-hCaptcha==0.2.0
|
||||
python-ldap==3.4.3
|
||||
django-auth-ldap==4.4.0
|
||||
python-ldap==3.4.4
|
||||
django-auth-ldap==4.6.0
|
||||
pytest-factoryboy==2.6.0
|
||||
pyppeteer==1.0.2
|
||||
validators==0.20.0
|
||||
|
@ -13,21 +13,20 @@
|
||||
"@codemirror/commands": "^6.3.2",
|
||||
"@codemirror/lang-markdown": "^6.2.3",
|
||||
"@codemirror/state": "^6.3.3",
|
||||
"@codemirror/view": "^6.22.2",
|
||||
"@codemirror/view": "^6.23.1",
|
||||
"@popperjs/core": "^2.11.7",
|
||||
"@vue/cli": "^5.0.8",
|
||||
"@vue/composition-api": "1.7.1",
|
||||
"axios": "^1.6.0",
|
||||
"axios": "^1.6.7",
|
||||
"babel": "^6.23.0",
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-loader": "^9.1.0",
|
||||
"bootstrap-vue": "^2.23.1",
|
||||
"core-js": "^3.29.1",
|
||||
"html2pdf.js": "^0.10.1",
|
||||
"lodash": "^4.17.21",
|
||||
"mavon-editor": "^2.10.4",
|
||||
"moment": "^2.29.4",
|
||||
"pinia": "^2.0.30",
|
||||
"pinia": "^2.1.7",
|
||||
"prismjs": "^1.29.0",
|
||||
"string-similarity": "^4.0.4",
|
||||
"vue": "^2.6.14",
|
||||
@ -62,9 +61,9 @@
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^8.46.0",
|
||||
"eslint-plugin-vue": "^8.7.1",
|
||||
"typescript": "~5.1.6",
|
||||
"typescript": "~5.3.3",
|
||||
"vue-cli-plugin-i18n": "^2.3.2",
|
||||
"webpack-bundle-tracker": "1.8.1",
|
||||
"webpack-bundle-tracker": "3.0.1",
|
||||
"workbox-background-sync": "^7.0.0",
|
||||
"workbox-expiration": "^6.5.4",
|
||||
"workbox-navigation-preload": "^7.0.0",
|
||||
@ -86,7 +85,8 @@
|
||||
"parser": "@typescript-eslint/parser"
|
||||
},
|
||||
"rules": {
|
||||
"no-unused-vars": "off"
|
||||
"no-unused-vars": "off",
|
||||
"vue/no-unused-components": "warn"
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
|
@ -11,51 +11,91 @@
|
||||
<b-button variant="primary" v-b-tooltip.hover :title="$t('Create')" @click="createNew">
|
||||
<i class="fas fa-plus"></i>
|
||||
</b-button>
|
||||
<b-dropdown variant="primary" id="sortDropDown" text="Order By" class="border-left">
|
||||
<b-dropdown-item @click = "orderBy('id','asc')" :disabled= "isActiveSort('id','asc')">oldest to newest</b-dropdown-item>
|
||||
<b-dropdown-item @click = "orderBy('id','desc')" :disabled= "isActiveSort('id','desc')">newest to oldest</b-dropdown-item>
|
||||
<b-dropdown-item @click = "orderBy('name','asc')" :disabled= "isActiveSort('name','asc')">alphabetical order</b-dropdown-item>
|
||||
<b-dropdown-item @click = "orderBy('order','asc')" :disabled= "isActiveSort('order','asc')" >manually</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</b-input-group-append>
|
||||
<b-button class= "ml-2" variant="primary" v-if="isActiveSort('order','asc')" @click="handleEditButton" >
|
||||
{{submitText}}
|
||||
</b-button>
|
||||
</b-input-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding-bottom: 55px">
|
||||
<div class="mb-3" v-for="book in filteredBooks" :key="book.id">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<b-card class="d-flex flex-column" v-hover v-on:click="openBook(book.id)">
|
||||
<b-row no-gutters style="height: inherit">
|
||||
<b-col no-gutters md="10" style="height: inherit">
|
||||
<b-card-body class="m-0 py-0" style="height: inherit">
|
||||
<b-card-text class="h-100 my-0 d-flex flex-column" style="text-overflow: ellipsis">
|
||||
<h5 class="m-0 mt-1 text-truncate">
|
||||
{{ book.name }} <span class="float-right"><i class="fa fa-book"></i></span>
|
||||
</h5>
|
||||
<div class="m-0 text-truncate">{{ book.description }}</div>
|
||||
<div class="mt-auto mb-1 d-flex flex-row justify-content-end"></div>
|
||||
</b-card-text>
|
||||
</b-card-body>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-card>
|
||||
<div v-if="!isActiveSort('order','asc') || !manSubmitted">
|
||||
<div style="padding-bottom: 55px">
|
||||
<div class="mb-3" v-for="(book) in filteredBooks" :key="book.id">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<b-card class="d-flex flex-column" v-hover >
|
||||
<b-row no-gutters style="height: inherit" class="d-flex align-items-center">
|
||||
<b-col no-gutters style="height: inherit">
|
||||
<b-card-body class="m-0 py-0" style="height: inherit">
|
||||
<b-card-text class="h-100 my-0 d-flex flex-column" style="text-overflow: ellipsis">
|
||||
<b-button v-on:click="openBook(book.id)" style="color: #000; background-color: white" variant="primary">
|
||||
<h5 class="m-0 mt-1 text-truncate" >
|
||||
{{ book.name }} <span class="float-right"><i class="fa fa-book"></i></span>
|
||||
</h5></b-button>
|
||||
<div class="m-0 text-truncate">{{ book.description }}</div>
|
||||
<div class="mt-auto mb-1 d-flex flex-row justify-content-end"></div>
|
||||
</b-card-text>
|
||||
</b-card-body>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<loading-spinner v-if="current_book === book.id && loading"></loading-spinner>
|
||||
<transition name="slide-fade">
|
||||
<cookbook-slider
|
||||
:recipes="recipes"
|
||||
:book="book"
|
||||
:key="`slider_${book.id}`"
|
||||
v-if="current_book === book.id && !loading"
|
||||
v-on:refresh="refreshData"
|
||||
@reload="openBook(current_book, true)"
|
||||
></cookbook-slider>
|
||||
</transition>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<loading-spinner v-if="current_book === book.id && loading"></loading-spinner>
|
||||
<transition name="slide-fade">
|
||||
<cookbook-slider
|
||||
:recipes="recipes"
|
||||
:book="book"
|
||||
:key="`slider_${book.id}`"
|
||||
v-if="current_book === book.id && !loading"
|
||||
v-on:refresh="refreshData"
|
||||
@reload="openBook(current_book, true)"
|
||||
></cookbook-slider>
|
||||
</transition>
|
||||
|
||||
</div>
|
||||
<div v-else>
|
||||
<draggable
|
||||
@change="updateManualSorting"
|
||||
:list="cookbooks" ghost-class="ghost">
|
||||
<b-card no-body class="mt-1 list-group-item p-2"
|
||||
style="cursor: move"
|
||||
v-for=" (book,index) in filteredBooks"
|
||||
v-hover
|
||||
:key="book.id">
|
||||
<b-card-header class="p-2 border-0">
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<button type="button"
|
||||
class="btn btn-lg shadow-none"><i
|
||||
class="fas fa-arrows-alt-v"></i></button>
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<h5 class="mt-1 mb-1">
|
||||
<b-badge class="float-left text-white mr-2">
|
||||
#{{ index + 1 }}
|
||||
</b-badge>
|
||||
{{ book.name }}
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</b-card-header>
|
||||
</b-card>
|
||||
</draggable>
|
||||
</div>
|
||||
|
||||
|
||||
<bottom-navigation-bar active-view="view_books">
|
||||
<template #custom_create_functions>
|
||||
<div class="dropdown-divider" ></div>
|
||||
@ -72,7 +112,7 @@
|
||||
<script>
|
||||
import Vue from "vue"
|
||||
import { BootstrapVue } from "bootstrap-vue"
|
||||
|
||||
import draggable from "vuedraggable"
|
||||
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||
import { ApiApiFactory } from "@/utils/openapi/api"
|
||||
import CookbookSlider from "@/components/CookbookSlider"
|
||||
@ -85,7 +125,7 @@ Vue.use(BootstrapVue)
|
||||
export default {
|
||||
name: "CookbookView",
|
||||
mixins: [ApiMixin],
|
||||
components: { LoadingSpinner, CookbookSlider, BottomNavigationBar },
|
||||
components: { LoadingSpinner, CookbookSlider, BottomNavigationBar, draggable },
|
||||
data() {
|
||||
return {
|
||||
cookbooks: [],
|
||||
@ -94,6 +134,11 @@ export default {
|
||||
current_book: undefined,
|
||||
loading: false,
|
||||
search: "",
|
||||
activeSortField : 'id',
|
||||
activeSortDirection: 'desc',
|
||||
inputValue: "",
|
||||
manSubmitted : false,
|
||||
submitText: "Edit"
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -168,6 +213,44 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
orderBy: function(order_field,order_direction){
|
||||
let apiClient = new ApiApiFactory()
|
||||
const options = {
|
||||
order_field: order_field,
|
||||
order_direction: order_direction
|
||||
}
|
||||
this.activeSortField = order_field
|
||||
this.activeSortDirection = order_direction
|
||||
apiClient.listRecipeBooks(options).then((result) => {
|
||||
this.cookbooks = result.data
|
||||
})
|
||||
},
|
||||
isActiveSort: function(field, direction) {
|
||||
// Check if the current item is the active sorting option
|
||||
return this.activeSortField === field && this.activeSortDirection === direction;
|
||||
},
|
||||
handleEditButton: function(){
|
||||
if (!this.manSubmitted){
|
||||
this.submitText = "Back"
|
||||
this.manSubmitted = true
|
||||
} else {
|
||||
this.submitText = "Edit"
|
||||
this.manSubmitted = false
|
||||
}
|
||||
},
|
||||
updateManualSorting: function(){
|
||||
let old_order = Object.assign({}, this.cookbooks);
|
||||
let promises = []
|
||||
this.cookbooks.forEach((element, index) => {
|
||||
let apiClient = new ApiApiFactory()
|
||||
promises.push(apiClient.partialUpdateManualOrderBooks(element.id, {order: index}))
|
||||
})
|
||||
return Promise.all(promises).then(() => {
|
||||
}).catch((err) => {
|
||||
this.cookbooks = old_order
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
|
||||
})
|
||||
}
|
||||
},
|
||||
directives: {
|
||||
hover: {
|
||||
|
@ -126,7 +126,7 @@
|
||||
<div class="flex-grow-1 ml-2"
|
||||
style="text-overflow: ellipsis; overflow-wrap: anywhere;">
|
||||
<span class="two-row-text">
|
||||
<a :href="resolveDjangoUrl('view_recipe', plan.entry.recipe.id)" v-if="plan.entry.recipe">{{ plan.entry.recipe.name }}</a>
|
||||
<a :href="getRecipeURL(plan.entry.recipe, plan.entry.servings)" v-if="plan.entry.recipe">{{ plan.entry.recipe.name }}</a>
|
||||
<span v-else>{{ plan.entry.title }}</span> <br/>
|
||||
</span>
|
||||
<span v-if="plan.entry.note" class="two-row-text">
|
||||
@ -169,7 +169,7 @@
|
||||
v-if="contextData && contextData.originalItem && contextData.originalItem.entry.recipe != null"
|
||||
@click="
|
||||
$refs.menu.close()
|
||||
openRecipe(contextData.originalItem.entry.recipe)
|
||||
openRecipe(contextData.originalItem.entry.recipe, contextData.originalItem.entry.servings)
|
||||
"
|
||||
>
|
||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-pizza-slice"></i>
|
||||
@ -392,8 +392,13 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openRecipe: function (recipe) {
|
||||
window.open(this.resolveDjangoUrl("view_recipe", recipe.id))
|
||||
|
||||
getRecipeURL: function (recipe, servings) {
|
||||
return this.resolveDjangoUrl("view_recipe",`${recipe.id}?servings=${servings}`)
|
||||
},
|
||||
|
||||
openRecipe: function (recipe, servings) {
|
||||
window.open(this.getRecipeURL(recipe, servings))
|
||||
},
|
||||
setStartingDay(days) {
|
||||
if (this.settings.startingDayOfWeek + days < 0) {
|
||||
|
@ -38,11 +38,11 @@
|
||||
@change="uploadImage($event.target.files[0])"/>
|
||||
|
||||
<div
|
||||
class="h-100 w-100 border border-primary rounded"
|
||||
style="border-width: 2px !important; border-style: dashed !important"
|
||||
@drop.prevent="uploadImage($event.dataTransfer.files[0])"
|
||||
@dragover.prevent
|
||||
@click="$refs.file_upload.click()"
|
||||
class="h-100 w-100 border border-primary rounded"
|
||||
style="border-width: 2px !important; border-style: dashed !important"
|
||||
@drop.prevent="uploadImage($event.dataTransfer.files[0])"
|
||||
@dragover.prevent
|
||||
@click="$refs.file_upload.click()"
|
||||
>
|
||||
<i class="far fa-image fa-10x text-primary"
|
||||
style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%)"
|
||||
@ -71,27 +71,27 @@
|
||||
<br/>
|
||||
<label for="id_name"> {{ $t("Keywords") }}</label>
|
||||
<multiselect
|
||||
v-model="recipe.keywords"
|
||||
:options="keywords"
|
||||
:close-on-select="false"
|
||||
:clear-on-select="true"
|
||||
:hide-selected="true"
|
||||
:preserve-search="true"
|
||||
:internal-search="false"
|
||||
:limit="options_limit"
|
||||
:placeholder="$t('select_keyword')"
|
||||
:tag-placeholder="$t('add_keyword')"
|
||||
:select-label="$t('Select')"
|
||||
:selected-label="$t('Selected')"
|
||||
:deselect-label="$t('remove_selection')"
|
||||
:taggable="true"
|
||||
@tag="addKeyword"
|
||||
label="label"
|
||||
track-by="id"
|
||||
id="id_keywords"
|
||||
:multiple="true"
|
||||
:loading="keywords_loading"
|
||||
@search-change="searchKeywords"
|
||||
v-model="recipe.keywords"
|
||||
:options="keywords"
|
||||
:close-on-select="false"
|
||||
:clear-on-select="true"
|
||||
:hide-selected="true"
|
||||
:preserve-search="true"
|
||||
:internal-search="false"
|
||||
:limit="options_limit"
|
||||
:placeholder="$t('select_keyword')"
|
||||
:tag-placeholder="$t('add_keyword')"
|
||||
:select-label="$t('Select')"
|
||||
:selected-label="$t('Selected')"
|
||||
:deselect-label="$t('remove_selection')"
|
||||
:taggable="true"
|
||||
@tag="addKeyword"
|
||||
label="label"
|
||||
track-by="id"
|
||||
id="id_keywords"
|
||||
:multiple="true"
|
||||
:loading="keywords_loading"
|
||||
@search-change="searchKeywords"
|
||||
>
|
||||
<template v-slot:noOptions>{{ $t("empty_list") }}</template>
|
||||
</multiselect>
|
||||
@ -103,21 +103,21 @@
|
||||
<div class="col-md-12">
|
||||
<div class="card mt-2 mb-2">
|
||||
<div class="card-body pr-2 pl-2 pr-md-5 pl-md-5 pt-3 pb-3">
|
||||
<h6>{{ $t('Properties') }} <small class="text-muted"> {{$t('per_serving')}}</small></h6>
|
||||
<h6>{{ $t('Properties') }} <small class="text-muted"> {{ $t('per_serving') }}</small></h6>
|
||||
|
||||
<div class="alert alert-info" role="alert">
|
||||
{{ $t('recipe_property_info')}}
|
||||
{{ $t('recipe_property_info') }}
|
||||
</div>
|
||||
|
||||
<div class="d-flex mt-2" v-for="p in recipe.properties" v-bind:key="p.id">
|
||||
<div class="flex-fill w-50">
|
||||
<generic-multiselect
|
||||
@change="p.property_type = $event.val"
|
||||
:initial_single_selection="p.property_type"
|
||||
:label="'name'"
|
||||
:model="Models.PROPERTY_TYPE"
|
||||
:limit="25"
|
||||
:multiple="false"
|
||||
@change="p.property_type = $event.val"
|
||||
:initial_single_selection="p.property_type"
|
||||
:label="'name'"
|
||||
:model="Models.PROPERTY_TYPE"
|
||||
:limit="25"
|
||||
:multiple="false"
|
||||
></generic-multiselect>
|
||||
</div>
|
||||
<div class="flex-fill w-50">
|
||||
@ -190,14 +190,14 @@
|
||||
<br/>
|
||||
<label> {{ $t("Share") }}</label>
|
||||
<generic-multiselect
|
||||
@change="recipe.shared = $event.val"
|
||||
parent_variable="recipe.shared"
|
||||
:initial_selection="recipe.shared"
|
||||
:label="'display_name'"
|
||||
:model="Models.USER_NAME"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
v-bind:placeholder="$t('Share')"
|
||||
:limit="25"
|
||||
@change="recipe.shared = $event.val"
|
||||
parent_variable="recipe.shared"
|
||||
:initial_selection="recipe.shared"
|
||||
:label="'display_name'"
|
||||
:model="Models.USER_NAME"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
v-bind:placeholder="$t('Share')"
|
||||
:limit="25"
|
||||
></generic-multiselect>
|
||||
|
||||
|
||||
@ -228,7 +228,7 @@
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuLink">
|
||||
<button class="dropdown-item" @click="removeStep(step)"><i
|
||||
class="fa fa-trash fa-fw"></i> {{ $t("Delete") }}
|
||||
class="fa fa-trash fa-fw"></i> {{ $t("Delete") }}
|
||||
</button>
|
||||
|
||||
<button class="dropdown-item" @click="moveStep(step, step_index - 1)"
|
||||
@ -300,11 +300,11 @@
|
||||
<i class="fas fa-plus-circle"></i> {{ $t("File") }}
|
||||
</b-button>
|
||||
<b-button
|
||||
pill
|
||||
variant="primary"
|
||||
size="sm"
|
||||
class="ml-1 mb-1 mb-md-0"
|
||||
@click="
|
||||
pill
|
||||
variant="primary"
|
||||
size="sm"
|
||||
class="ml-1 mb-1 mb-md-0"
|
||||
@click="
|
||||
paste_step = step
|
||||
$bvModal.show('id_modal_paste_ingredients')
|
||||
"
|
||||
@ -327,31 +327,31 @@
|
||||
<label :for="'id_step_' + step.id + '_file'">{{ $t("File") }}</label>
|
||||
<b-input-group>
|
||||
<multiselect
|
||||
ref="file"
|
||||
v-model="step.file"
|
||||
:options="files"
|
||||
:close-on-select="true"
|
||||
:clear-on-select="true"
|
||||
:allow-empty="true"
|
||||
:preserve-search="true"
|
||||
:placeholder="$t('select_file')"
|
||||
:select-label="$t('Select')"
|
||||
:selected-label="$t('Selected')"
|
||||
:deselect-label="$t('remove_selection')"
|
||||
:id="'id_step_' + step.id + '_file'"
|
||||
label="name"
|
||||
track-by="name"
|
||||
:multiple="false"
|
||||
:loading="files_loading"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
@search-change="searchFiles"
|
||||
ref="file"
|
||||
v-model="step.file"
|
||||
:options="files"
|
||||
:close-on-select="true"
|
||||
:clear-on-select="true"
|
||||
:allow-empty="true"
|
||||
:preserve-search="true"
|
||||
:placeholder="$t('select_file')"
|
||||
:select-label="$t('Select')"
|
||||
:selected-label="$t('Selected')"
|
||||
:deselect-label="$t('remove_selection')"
|
||||
:id="'id_step_' + step.id + '_file'"
|
||||
label="name"
|
||||
track-by="name"
|
||||
:multiple="false"
|
||||
:loading="files_loading"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
@search-change="searchFiles"
|
||||
>
|
||||
<template v-slot:noOptions>{{ $t("empty_list") }}</template>
|
||||
</multiselect>
|
||||
<b-input-group-append>
|
||||
<b-button
|
||||
variant="primary"
|
||||
@click="
|
||||
variant="primary"
|
||||
@click="
|
||||
step_for_file_create = step
|
||||
show_file_create = true
|
||||
"
|
||||
@ -367,24 +367,24 @@
|
||||
<div class="col-md-12">
|
||||
<label :for="'id_step_' + step.id + '_recipe'">{{ $t("Recipe") }}</label>
|
||||
<multiselect
|
||||
ref="step_recipe"
|
||||
v-model="step.step_recipe"
|
||||
:options="recipes.map((recipe) => recipe.id)"
|
||||
:close-on-select="true"
|
||||
:clear-on-select="true"
|
||||
:allow-empty="true"
|
||||
:preserve-search="true"
|
||||
:internal-search="false"
|
||||
:limit="options_limit"
|
||||
:placeholder="$t('select_recipe')"
|
||||
:select-label="$t('Select')"
|
||||
:selected-label="$t('Selected')"
|
||||
:deselect-label="$t('remove_selection')"
|
||||
:id="'id_step_' + step.id + '_recipe'"
|
||||
:custom-label="(opt) => recipes.find((x) => x.id === opt).name"
|
||||
:multiple="false"
|
||||
:loading="recipes_loading"
|
||||
@search-change="searchRecipes"
|
||||
ref="step_recipe"
|
||||
v-model="step.step_recipe"
|
||||
:options="recipes.map((recipe) => recipe.id)"
|
||||
:close-on-select="true"
|
||||
:clear-on-select="true"
|
||||
:allow-empty="true"
|
||||
:preserve-search="true"
|
||||
:internal-search="false"
|
||||
:limit="options_limit"
|
||||
:placeholder="$t('select_recipe')"
|
||||
:select-label="$t('Select')"
|
||||
:selected-label="$t('Selected')"
|
||||
:deselect-label="$t('remove_selection')"
|
||||
:id="'id_step_' + step.id + '_recipe'"
|
||||
:custom-label="(opt) => recipes.find((x) => x.id === opt).name"
|
||||
:multiple="false"
|
||||
:loading="recipes_loading"
|
||||
@search-change="searchRecipes"
|
||||
>
|
||||
<template v-slot:noOptions>{{ $t("empty_list") }}</template>
|
||||
</multiselect>
|
||||
@ -424,12 +424,12 @@
|
||||
<div class="col-lg-2 col-md-6 small-padding"
|
||||
v-if="!ingredient.is_header">
|
||||
<input
|
||||
class="form-control"
|
||||
v-model="ingredient.amount"
|
||||
type="number"
|
||||
step="any"
|
||||
v-if="!ingredient.no_amount"
|
||||
:id="`amount_${step_index}_${index}`"
|
||||
class="form-control"
|
||||
v-model="ingredient.amount"
|
||||
type="number"
|
||||
step="any"
|
||||
v-if="!ingredient.no_amount"
|
||||
:id="`amount_${step_index}_${index}`"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -437,29 +437,29 @@
|
||||
v-if="!ingredient.is_header">
|
||||
<!-- search set to false to allow API to drive results & order -->
|
||||
<multiselect
|
||||
v-if="!ingredient.no_amount"
|
||||
ref="unit"
|
||||
v-model="ingredient.unit"
|
||||
:options="units"
|
||||
:close-on-select="true"
|
||||
:clear-on-select="true"
|
||||
:allow-empty="true"
|
||||
:preserve-search="true"
|
||||
:internal-search="false"
|
||||
:limit="options_limit"
|
||||
:placeholder="$t('select_unit')"
|
||||
:tag-placeholder="$t('Create')"
|
||||
:select-label="$t('Select')"
|
||||
:selected-label="$t('Selected')"
|
||||
:deselect-label="$t('remove_selection')"
|
||||
:taggable="true"
|
||||
@tag="addUnitType"
|
||||
:id="`unit_${step_index}_${index}`"
|
||||
label="name"
|
||||
track-by="name"
|
||||
:multiple="false"
|
||||
:loading="units_loading"
|
||||
@search-change="searchUnits"
|
||||
v-if="!ingredient.no_amount"
|
||||
ref="unit"
|
||||
v-model="ingredient.unit"
|
||||
:options="units"
|
||||
:close-on-select="true"
|
||||
:clear-on-select="true"
|
||||
:allow-empty="true"
|
||||
:preserve-search="true"
|
||||
:internal-search="false"
|
||||
:limit="options_limit"
|
||||
:placeholder="$t('select_unit')"
|
||||
:tag-placeholder="$t('Create')"
|
||||
:select-label="$t('Select')"
|
||||
:selected-label="$t('Selected')"
|
||||
:deselect-label="$t('remove_selection')"
|
||||
:taggable="true"
|
||||
@tag="addUnitType"
|
||||
:id="`unit_${step_index}_${index}`"
|
||||
label="name"
|
||||
track-by="name"
|
||||
:multiple="false"
|
||||
:loading="units_loading"
|
||||
@search-change="searchUnits"
|
||||
>
|
||||
<template v-slot:noOptions>{{
|
||||
$t("empty_list")
|
||||
@ -472,28 +472,28 @@
|
||||
<!-- search set to false to allow API to drive results & order -->
|
||||
|
||||
<multiselect
|
||||
ref="food"
|
||||
v-model="ingredient.food"
|
||||
:options="foods"
|
||||
:close-on-select="true"
|
||||
:clear-on-select="true"
|
||||
:allow-empty="true"
|
||||
:preserve-search="true"
|
||||
:internal-search="false"
|
||||
:limit="options_limit"
|
||||
:placeholder="$t('select_food')"
|
||||
:tag-placeholder="$t('Create')"
|
||||
:select-label="$t('Select')"
|
||||
:selected-label="$t('Selected')"
|
||||
:deselect-label="$t('remove_selection')"
|
||||
:taggable="true"
|
||||
@tag="addFoodType"
|
||||
:id="`ingredient_${step_index}_${index}`"
|
||||
label="name"
|
||||
track-by="name"
|
||||
:multiple="false"
|
||||
:loading="foods_loading"
|
||||
@search-change="searchFoods"
|
||||
ref="food"
|
||||
v-model="ingredient.food"
|
||||
:options="foods"
|
||||
:close-on-select="true"
|
||||
:clear-on-select="true"
|
||||
:allow-empty="true"
|
||||
:preserve-search="true"
|
||||
:internal-search="false"
|
||||
:limit="options_limit"
|
||||
:placeholder="$t('select_food')"
|
||||
:tag-placeholder="$t('Create')"
|
||||
:select-label="$t('Select')"
|
||||
:selected-label="$t('Selected')"
|
||||
:deselect-label="$t('remove_selection')"
|
||||
:taggable="true"
|
||||
@tag="addFoodType"
|
||||
:id="`ingredient_${step_index}_${index}`"
|
||||
label="name"
|
||||
track-by="name"
|
||||
:multiple="false"
|
||||
:loading="foods_loading"
|
||||
@search-change="searchFoods"
|
||||
>
|
||||
<template v-slot:noOptions>{{
|
||||
$t("empty_list")
|
||||
@ -504,11 +504,11 @@
|
||||
<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 }">
|
||||
<input
|
||||
class="form-control"
|
||||
maxlength="256"
|
||||
v-model="ingredient.note"
|
||||
v-bind:placeholder="$t('Note')"
|
||||
v-on:keydown.tab="
|
||||
class="form-control"
|
||||
maxlength="256"
|
||||
v-model="ingredient.note"
|
||||
v-bind:placeholder="$t('Note')"
|
||||
v-on:keydown.tab="
|
||||
(event) => {
|
||||
if (step.ingredients.indexOf(ingredient) === step.ingredients.length - 1) {
|
||||
event.preventDefault()
|
||||
@ -522,13 +522,13 @@
|
||||
|
||||
<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="#"
|
||||
role="button"
|
||||
id="dropdownMenuLink2"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
class="btn shadow-none btn-lg pr-1 pl-0 pr-md-2 pl-md-2"
|
||||
href="#"
|
||||
role="button"
|
||||
id="dropdownMenuLink2"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<i class="fas fa-ellipsis-v text-muted"></i>
|
||||
</a>
|
||||
@ -645,7 +645,18 @@
|
||||
<mavon-editor v-model="step.instruction" :autofocus="false"
|
||||
style="z-index: auto" :id="'id_instruction_' + step.id"
|
||||
:language="'en'"
|
||||
:toolbars="md_editor_toolbars" :defaultOpen="'edit'"/>
|
||||
:toolbars="md_editor_toolbars" :defaultOpen="'edit'">
|
||||
<template #left-toolbar-after>
|
||||
<span class="op-icon-divider"></span>
|
||||
<button
|
||||
type="button"
|
||||
@click="step.instruction+= ' {{ scale(100) }}'"
|
||||
class="op-icon fas fa-times"
|
||||
aria-hidden="true"
|
||||
title="Scalable Number"
|
||||
></button>
|
||||
</template>
|
||||
</mavon-editor>
|
||||
|
||||
<!-- TODO markdown DOCS link and markdown editor -->
|
||||
</div>
|
||||
@ -662,7 +673,7 @@
|
||||
</button>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -694,7 +705,7 @@
|
||||
<div class="col-3 col-md-6 mb-1 mb-md-0 pr-2 pl-2">
|
||||
<a :href="resolveDjangoUrl('delete_recipe', recipe.id)"
|
||||
class="d-block d-md-none btn btn-block btn-danger shadow-none btn-sm"><i
|
||||
class="fa fa-trash fa-lg"></i></a>
|
||||
class="fa fa-trash fa-lg"></i></a>
|
||||
<a :href="resolveDjangoUrl('delete_recipe', recipe.id)"
|
||||
class="d-none d-md-block btn btn-block btn-danger shadow-none btn-sm">{{ $t("Delete") }}</a>
|
||||
</div>
|
||||
@ -749,11 +760,11 @@
|
||||
|
||||
<!-- modal for pasting list of ingredients -->
|
||||
<b-modal
|
||||
id="id_modal_paste_ingredients"
|
||||
v-bind:title="$t('ingredient_list')"
|
||||
@ok="appendIngredients(paste_step)"
|
||||
@cancel="paste_ingredients = paste_step = undefined"
|
||||
@close="paste_ingredients = paste_step = undefined"
|
||||
id="id_modal_paste_ingredients"
|
||||
v-bind:title="$t('ingredient_list')"
|
||||
@ok="appendIngredients(paste_step)"
|
||||
@cancel="paste_ingredients = paste_step = undefined"
|
||||
@close="paste_ingredients = paste_step = undefined"
|
||||
>
|
||||
<b-form-textarea id="paste_ingredients" v-model="paste_ingredients"
|
||||
:placeholder="$t('paste_ingredients_placeholder')" rows="10"></b-form-textarea>
|
||||
@ -832,7 +843,7 @@ export default {
|
||||
header: true,
|
||||
underline: true,
|
||||
strikethrough: true,
|
||||
mark: true,
|
||||
mark: false,
|
||||
superscript: true,
|
||||
subscript: true,
|
||||
quote: true,
|
||||
@ -1293,7 +1304,7 @@ export default {
|
||||
})
|
||||
Promise.allSettled(promises).then(() => {
|
||||
ing_list.forEach(ing => {
|
||||
if(ing.trim() !== ""){
|
||||
if (ing.trim() !== "") {
|
||||
step.ingredients.push(parsed_ing_list.find(x => x.original_text === ing))
|
||||
}
|
||||
})
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -2,8 +2,48 @@
|
||||
|
||||
<div id="app">
|
||||
<div>
|
||||
<markdown-editor-component></markdown-editor-component>
|
||||
|
||||
<div class="swipe-container">
|
||||
<div class="swipe-action bg-success">
|
||||
<i class="swipe-icon fa-fw fas fa-check"></i>
|
||||
</div>
|
||||
|
||||
<b-button-group class="swipe-element">
|
||||
|
||||
<div class="card flex-grow-1 btn-block p-2">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex flex-column pr-2">
|
||||
<span>
|
||||
<span><i class="fas fa-check"></i> <b>100 g </b></span>
|
||||
<br/>
|
||||
</span>
|
||||
<span>
|
||||
<span><i class="fas fa-check"></i> <b>200 kg </b></span>
|
||||
<br/>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
<div class="d-flex flex-column flex-grow-1 align-self-center">
|
||||
Erdbeeren <br/>
|
||||
<span><small class="text-muted">vabene111</small></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<b-button variant="success">
|
||||
<i class="d-print-none fa-fw fas fa-check"></i>
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
<div class="swipe-action bg-primary justify-content-end">
|
||||
<i class="fa-fw fas fa-hourglass-half swipe-icon"></i>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<markdown-editor-component></markdown-editor-component>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -49,5 +89,46 @@ export default {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.swipe-container {
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
overflow-x: scroll;
|
||||
scroll-snap-type: x mandatory;
|
||||
}
|
||||
|
||||
/* scrollbar should be hidden */
|
||||
.swipe-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.swipe-container {
|
||||
scrollbar-width: none; /* For Firefox */
|
||||
}
|
||||
|
||||
/* main element should always snap into view */
|
||||
.swipe-element {
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
.swipe-icon {
|
||||
color: white;
|
||||
position: sticky;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
/* swipe-actions and element should be 100% wide */
|
||||
.swipe-action,
|
||||
.swipe-element {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.swipe-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
@ -11,14 +11,14 @@
|
||||
<div class="flex-column" v-if="show_button_1">
|
||||
<slot name="button_1">
|
||||
<a class="nav-link bottom-nav-link p-0" v-bind:class="{'bottom-nav-link-active': activeView === 'view_search' }" v-bind:href="resolveDjangoUrl('view_search')">
|
||||
<i class="fas fa-fw fa-book " style="font-size: 1.5em"></i><br/><small>{{ $t('Recipes') }}</small></a> <!-- TODO localize -->
|
||||
<i class="fas fa-fw fa-book " style="font-size: 1.4em"></i><br/><small>{{ $t('Recipes') }}</small></a> <!-- TODO localize -->
|
||||
</slot>
|
||||
|
||||
</div>
|
||||
<div class="flex-column" v-if="show_button_2">
|
||||
<slot name="button_2">
|
||||
<a class="nav-link bottom-nav-link p-0" v-bind:class="{'bottom-nav-link-active': activeView === 'view_plan' }" v-bind:href="resolveDjangoUrl('view_plan')">
|
||||
<i class="fas fa-calendar-alt" style="font-size: 1.5em"></i><br/><small>{{ $t('Meal_Plan') }}</small></a>
|
||||
<i class="fas fa-calendar-alt" style="font-size: 1.4em"></i><br/><small>{{ $t('Meal_Plan') }}</small></a>
|
||||
</slot>
|
||||
|
||||
</div>
|
||||
@ -53,14 +53,14 @@
|
||||
<div class="flex-column" v-if="show_button_3">
|
||||
<slot name="button_3">
|
||||
<a class="nav-link bottom-nav-link p-0" v-bind:class="{'bottom-nav-link-active': activeView === 'view_shopping' }" v-bind:href="resolveDjangoUrl('view_shopping')">
|
||||
<i class="fas fa-shopping-cart" style="font-size: 1.5em"></i><br/><small>{{ $t('Shopping_list') }}</small></a>
|
||||
<i class="fas fa-shopping-cart" style="font-size: 1.4em"></i><br/><small>{{ $t('Shopping_list') }}</small></a>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="flex-column">
|
||||
|
||||
<slot name="button_4" v-if="show_button_4">
|
||||
<a class="nav-link bottom-nav-link p-0" v-bind:class="{'bottom-nav-link-active': activeView === 'view_books' }" v-bind:href="resolveDjangoUrl('view_books')">
|
||||
<i class="fas fa-book-open" style="font-size: 1.5em"></i><br/><small>{{ $t('Books') }}</small></a> <!-- TODO localize -->
|
||||
<i class="fas fa-book-open" style="font-size: 1.4em"></i><br/><small>{{ $t('Books') }}</small></a> <!-- TODO localize -->
|
||||
</slot>
|
||||
|
||||
</div>
|
||||
|
@ -6,7 +6,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import html2pdf from "html2pdf.js"
|
||||
|
||||
export default {
|
||||
name: "DownloadPDF",
|
||||
@ -20,12 +19,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
downloadFile() {
|
||||
const doc = document.querySelector(this.dom)
|
||||
var options = {
|
||||
margin: 1,
|
||||
filename: this.name,
|
||||
}
|
||||
html2pdf().from(doc).set(options).save()
|
||||
window.print()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -1,44 +1,60 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>EDITOR</h1>
|
||||
<div id="editor" style="" class="bg-info">
|
||||
|
||||
<b-button @click="toolbarTest()">Heading</b-button>
|
||||
<b-button @click="bold()">B</b-button>
|
||||
<div id="editor" style="background-color: #fff; border: solid 1px">
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {EditorState} from "@codemirror/state"
|
||||
import {keymap, EditorView, MatchDecorator, Decoration, WidgetType, ViewPlugin} from "@codemirror/view"
|
||||
import {defaultKeymap} from "@codemirror/commands"
|
||||
import {EditorSelection, EditorState} from "@codemirror/state"
|
||||
import {keymap, EditorView, MatchDecorator, Decoration, WidgetType, ViewPlugin, DecorationSet} from "@codemirror/view"
|
||||
import {defaultKeymap, history} from "@codemirror/commands"
|
||||
|
||||
import {markdown} from "@codemirror/lang-markdown"
|
||||
import {markdown, markdownLanguage} from "@codemirror/lang-markdown"
|
||||
import {autocompletion} from "@codemirror/autocomplete"
|
||||
|
||||
class PlaceholderWidget extends WidgetType { //TODO this is not working for some javascript magic reason
|
||||
import {defaultHighlightStyle, syntaxHighlighting, syntaxTree} from "@codemirror/language";
|
||||
|
||||
class TemplatePreviewWidget extends WidgetType {
|
||||
name = undefined
|
||||
constructor(name) {
|
||||
console.log(name)
|
||||
ingredients = []
|
||||
|
||||
constructor(name, ingredients) {
|
||||
super()
|
||||
this.name = name
|
||||
this.ingredients = ingredients
|
||||
}
|
||||
|
||||
eq(other) {
|
||||
return this.name == other.name
|
||||
getIngredientLabel(ingredient) {
|
||||
// TODO all possible null combinations
|
||||
return `${ingredient.amount} ${ingredient.unit.name} ${ingredient.food.name}`
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
let elt = document.createElement("span")
|
||||
elt.style.cssText = `
|
||||
border: 1px solid blue;
|
||||
border-radius: 4px;
|
||||
padding: 0 3px;
|
||||
background: lightblue;`
|
||||
let preview_span = document.createElement("span")
|
||||
|
||||
elt.textContent = "Food"
|
||||
return elt
|
||||
let display_text = 'ERROR'
|
||||
if (this.name.includes('ingredients')) {
|
||||
let ingredient_index = this.name.replace('{{ ingredients[', '').replace('] }}', '') // TODO support calculations ingredients[0]*0.5
|
||||
display_text = this.getIngredientLabel(this.ingredients[ingredient_index])
|
||||
}
|
||||
|
||||
if (this.name.includes('scale(')) {
|
||||
display_text = this.name.replace('{{ scale(', '').replace(') }}', '') // TODO support calculations scale(100)*2
|
||||
}
|
||||
|
||||
preview_span.innerHTML = display_text
|
||||
preview_span.style.cssText = ` border: 1px solid blue; border-radius: 4px; padding: 0 3px; background: lightblue;`
|
||||
return preview_span
|
||||
}
|
||||
|
||||
ignoreEvent() {
|
||||
@ -49,39 +65,69 @@ class PlaceholderWidget extends WidgetType { //TODO this is not working for some
|
||||
export default {
|
||||
name: "MarkdownEditorComponent",
|
||||
props: {},
|
||||
computed: {},
|
||||
computed: {
|
||||
autocomplete_options() {
|
||||
let autocomplete_options = []
|
||||
|
||||
let index = 0
|
||||
for (let i of this.ingredients) {
|
||||
autocomplete_options.push({label: i.food.name, type: "text", apply: `{{ ingredients[${index}] }}`, detail: `${i.amount} ${i.unit.name} ${i.food.name}`})
|
||||
index++
|
||||
}
|
||||
|
||||
autocomplete_options.push({label: "Scale", type: "text", apply: "{{ scale(100) }}", detail: "simple scalable number"})
|
||||
return autocomplete_options
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editor_view: null,
|
||||
ingredients: [
|
||||
{amount: 20, food: {'name': 'raspberry'}, unit: {'name': 'pcs'}},
|
||||
{amount: 100, food: {'name': 'sugar'}, unit: {'name': 'g'}},
|
||||
{amount: 250, food: {'name': 'water'}, unit: {'name': 'ml'}},
|
||||
{amount: 1, food: {'name': 'salt'}, unit: {'name': 'pinch'}},
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
const placeholderMatcher = new MatchDecorator({
|
||||
regexp: /\{\{\singredients\[\d\]\s\}\}/g,
|
||||
decoration: match => Decoration.replace({
|
||||
widget: new PlaceholderWidget(match[0]),
|
||||
})
|
||||
const decoMatcher = new MatchDecorator({
|
||||
regexp: /\{\{ (?:scale\(\d+\)|ingredients\[\d+\]) \}\}/g,
|
||||
decorate: (add, from, to, match, view) => {
|
||||
const templatePreview = new TemplatePreviewWidget(match[0], this.ingredients);
|
||||
add(to, to, Decoration.widget({widget: templatePreview, side: 1}));
|
||||
},
|
||||
})
|
||||
|
||||
const placeholders = ViewPlugin.fromClass(class {
|
||||
placeholders
|
||||
decorations
|
||||
|
||||
constructor(view) {
|
||||
this.placeholders = placeholderMatcher.createDeco(view)
|
||||
this.decorations = decoMatcher.createDeco(view)
|
||||
}
|
||||
|
||||
update(update) {
|
||||
this.placeholders = placeholderMatcher.updateDeco(update, this.placeholders)
|
||||
update(viewUpdate) {
|
||||
this.decorations = decoMatcher.updateDeco(viewUpdate, this.decorations)
|
||||
}
|
||||
}, {
|
||||
decorations: instance => instance.placeholders,
|
||||
provide: plugin => EditorView.atomicRanges.of(view => {
|
||||
return view.plugin(plugin)?.placeholders || Decoration.none
|
||||
})
|
||||
decorations: instance => instance.decorations,
|
||||
|
||||
})
|
||||
|
||||
let startState = EditorState.create({
|
||||
doc: "Das ist eine Beschreibung \nPacke {{ ingredients[1] }} in das Fass mit {{ ingredients[3] }}\nTest Bla Bla",
|
||||
extensions: [keymap.of(defaultKeymap), placeholders, markdown(), autocompletion({override: [this.foodTemplateAutoComplete]})]
|
||||
doc: "## Header\n\nDas ist eine Beschreibung [test](https://google.com) \nPacke {{ ingredients[1] }} in das Fass mit {{ ingredients[3] }}\nTest Bla Bla {{ scale(100) }} \n\n- test\n- test 2\n- test3",
|
||||
extensions: [
|
||||
history(),
|
||||
syntaxHighlighting(defaultHighlightStyle),
|
||||
keymap.of(defaultKeymap),
|
||||
placeholders,
|
||||
markdown({base: markdownLanguage, highlightFormatting: true}),
|
||||
autocompletion({override: [this.foodTemplateAutoComplete]})
|
||||
]
|
||||
})
|
||||
|
||||
let view = new EditorView({
|
||||
this.editor_view = new EditorView({
|
||||
state: startState,
|
||||
extensions: [],
|
||||
parent: document.getElementById("editor")
|
||||
@ -90,16 +136,74 @@ export default {
|
||||
methods: {
|
||||
foodTemplateAutoComplete: function (context) {
|
||||
let word = context.matchBefore(/\w*/)
|
||||
if (word.from == word.to && !context.explicit)
|
||||
if (word.from === word.to && !context.explicit)
|
||||
return null
|
||||
return {
|
||||
from: word.from,
|
||||
options: [
|
||||
{label: "Mehl", type: "text", apply: "{{ ingredients[1] }}", detail: "template"},
|
||||
{label: "Butter", type: "text", apply: "{{ ingredients[2] }}", detail: "template"},
|
||||
{label: "Salz", type: "text", apply: "{{ ingredients[3] }}", detail: "template"},
|
||||
]
|
||||
options: this.autocomplete_options
|
||||
}
|
||||
},
|
||||
toolbarTest() {
|
||||
const transaction = this.editor_view.state.changeByRange((range) => {
|
||||
const prefix = '###'
|
||||
const docText = this.editor_view.state.doc.toString()
|
||||
let text = this.editor_view.state.sliceDoc(range.from, range.to)
|
||||
|
||||
let rangeFrom = range.from
|
||||
|
||||
while (rangeFrom > 0) {
|
||||
if (docText[rangeFrom - 1] === "\n") {
|
||||
break
|
||||
}
|
||||
rangeFrom -= 1
|
||||
}
|
||||
|
||||
text = this.editor_view.state.sliceDoc(rangeFrom, range.to)
|
||||
const textBefore = `\n${text}`
|
||||
const newText = textBefore.replace(/\n/g, `\n${prefix}`)
|
||||
|
||||
const changes = {
|
||||
from: rangeFrom,
|
||||
to: range.to,
|
||||
insert: newText,
|
||||
anchor: rangeFrom + prefix.length + 1,
|
||||
textBefore: textBefore
|
||||
}
|
||||
|
||||
return {
|
||||
changes,
|
||||
range: EditorSelection.range(changes.anchor, changes.anchor)
|
||||
}
|
||||
})
|
||||
|
||||
this.editor_view.dispatch(transaction)
|
||||
},
|
||||
bold() {
|
||||
const transaction = this.editor_view.state.changeByRange((range) => {
|
||||
|
||||
if (range.anchor === range.head) {
|
||||
console.log('nothing selected --> nothing bold')
|
||||
} else {
|
||||
let selected_text = this.editor_view.state.sliceDoc(range.from, range.to)
|
||||
|
||||
let new_text = `**${selected_text}**`
|
||||
|
||||
const changes = {
|
||||
from: range.from,
|
||||
to: range.to,
|
||||
insert: new_text,
|
||||
}
|
||||
|
||||
return {changes, range: EditorSelection.range(range.anchor + 2, range.head + 2)}
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
|
||||
this.editor_view.dispatch(transaction)
|
||||
},
|
||||
heading(editor, heading_size) {
|
||||
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -186,8 +186,5 @@ export default {
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.b-form-spinbutton.form-control {
|
||||
background-color: #e9ecef;
|
||||
border: 1px solid #ced4da;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
74
vue/src/components/NumberScalerComponent.vue
Normal file
74
vue/src/components/NumberScalerComponent.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-button-group class="w-100 mt-1">
|
||||
<b-button @click="updateNumber( 'half')" variant="outline-info"
|
||||
:disabled="disable"><i class="fas fa-divide"></i> 2
|
||||
</b-button>
|
||||
<b-button variant="outline-info" @click="updateNumber( 'sub')"
|
||||
:disabled="disable"><i class="fas fa-minus"></i>
|
||||
</b-button>
|
||||
<b-button variant="outline-info" @click="updateNumber('prompt')"
|
||||
:disabled="disable">
|
||||
{{ number }}
|
||||
</b-button>
|
||||
<b-button variant="outline-info" @click="updateNumber( 'add')"
|
||||
:disabled="disable"><i class="fas fa-plus"></i>
|
||||
</b-button>
|
||||
<b-button @click="updateNumber('multiply')" variant="outline-info"
|
||||
:disabled="disable"><i class="fas fa-times"></i> 2
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue"
|
||||
import {BootstrapVue} from "bootstrap-vue"
|
||||
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
export default {
|
||||
name: "NumberScalerComponent",
|
||||
props: {
|
||||
number: {type: Number, default:0},
|
||||
disable: {type: Boolean, default: false}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* perform given operation on linked number
|
||||
* @param operation update mode
|
||||
*/
|
||||
updateNumber: function(operation) {
|
||||
if (operation === 'half') {
|
||||
this.$emit('change', this.number / 2)
|
||||
}
|
||||
if (operation === 'multiply') {
|
||||
this.$emit('change', this.number * 2)
|
||||
}
|
||||
if (operation === 'add') {
|
||||
this.$emit('change', this.number + 1)
|
||||
}
|
||||
if (operation === 'sub') {
|
||||
this.$emit('change', this.number - 1)
|
||||
}
|
||||
if (operation === 'prompt') {
|
||||
let input_number = prompt(this.$t('Input'), this.number);
|
||||
if (input_number !== null && input_number !== "" && !isNaN(input_number) && !isNaN(parseFloat(input_number))) {
|
||||
this.$emit('change', parseFloat(input_number))
|
||||
} else {
|
||||
console.log('Invalid number input in prompt', input_number)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -237,6 +237,7 @@ export default {
|
||||
},
|
||||
props: {
|
||||
recipe_id: Number,
|
||||
def_servings: Number,
|
||||
recipe_obj: {type: Object, default: null},
|
||||
show_context_menu: {type: Boolean, default: true},
|
||||
enable_keyword_links: {type: Boolean, default: true},
|
||||
@ -320,8 +321,13 @@ export default {
|
||||
|
||||
|
||||
if (this.recipe.image === null) this.printReady()
|
||||
|
||||
this.servings = this.servings_cache[this.rootrecipe.id] = this.recipe.servings
|
||||
window.RECIPE_SERVINGS = Number(window.RECIPE_SERVINGS)
|
||||
if (window.RECIPE_SERVINGS && ! isNaN(window.RECIPE_SERVINGS)) {
|
||||
//I am not sure this is the best way. This overwrites our servings cache, which may not be intended?
|
||||
this.servings = window.RECIPE_SERVINGS
|
||||
} else {
|
||||
this.servings = this.servings_cache[this.rootrecipe.id] = this.recipe.servings
|
||||
}
|
||||
this.loading = false
|
||||
|
||||
setTimeout(() => {
|
||||
|
@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div v-if="user_preferences !== undefined">
|
||||
<div v-if="useUserPreferenceStore().user_settings !== undefined">
|
||||
<b-form-group :label="$t('shopping_share')" :description="$t('shopping_share_desc')">
|
||||
<generic-multiselect
|
||||
@change="user_preferences.shopping_share = $event.val; updateSettings(false)"
|
||||
@change="useUserPreferenceStore().user_settings.shopping_share = $event.val; updateSettings(false)"
|
||||
:model="Models.USER"
|
||||
:initial_selection="user_preferences.shopping_share"
|
||||
:initial_selection="useUserPreferenceStore().user_settings.shopping_share"
|
||||
label="display_name"
|
||||
:multiple="true"
|
||||
:placeholder="$t('User')"
|
||||
@ -12,101 +12,98 @@
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :label="$t('shopping_auto_sync')" :description="$t('shopping_auto_sync_desc')">
|
||||
<b-form-input type="range" :min="SHOPPING_MIN_AUTOSYNC_INTERVAL" max="60" step="1" v-model="user_preferences.shopping_auto_sync"
|
||||
@change="updateSettings(false)"></b-form-input>
|
||||
<b-form-input type="range" :min="SHOPPING_MIN_AUTOSYNC_INTERVAL" max="60" step="1" v-model="useUserPreferenceStore().user_settings.shopping_auto_sync"
|
||||
@change="updateSettings(false)" :disabled="useUserPreferenceStore().user_settings.shopping_auto_sync < 1"></b-form-input>
|
||||
<div class="text-center">
|
||||
<span v-if="user_preferences.shopping_auto_sync > 0">
|
||||
{{ Math.round(user_preferences.shopping_auto_sync) }}
|
||||
<span v-if="user_preferences.shopping_auto_sync === 1">{{ $t('Second') }}</span>
|
||||
<span v-if="useUserPreferenceStore().user_settings.shopping_auto_sync > 0">
|
||||
{{ Math.round(useUserPreferenceStore().user_settings.shopping_auto_sync) }}
|
||||
<span v-if="useUserPreferenceStore().user_settings.shopping_auto_sync === 1">{{ $t('Second') }}</span>
|
||||
<span v-else> {{ $t('Seconds') }}</span>
|
||||
</span>
|
||||
|
||||
<span v-if="user_preferences.shopping_auto_sync < 1">{{ $t('Disable') }}</span>
|
||||
<span v-if="useUserPreferenceStore().user_settings.shopping_auto_sync < 1">{{ $t('Disable') }}</span>
|
||||
</div>
|
||||
<br/>
|
||||
<b-button class="btn btn-sm" @click="user_preferences.shopping_auto_sync = 0; updateSettings(false)">{{ $t('Disabled') }}</b-button>
|
||||
<b-button class="btn btn-sm" @click="useUserPreferenceStore().user_settings.shopping_auto_sync = 0; updateSettings(false)"
|
||||
v-if="useUserPreferenceStore().user_settings.shopping_auto_sync > 0">{{ $t('Disable') }}</b-button>
|
||||
<b-button class="btn btn-sm btn-success" @click="useUserPreferenceStore().user_settings.shopping_auto_sync = SHOPPING_MIN_AUTOSYNC_INTERVAL; updateSettings(false)"
|
||||
v-if="useUserPreferenceStore().user_settings.shopping_auto_sync < 1">{{ $t('Enable') }}</b-button>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :description="$t('mealplan_autoadd_shopping_desc')">
|
||||
<b-form-checkbox v-model="user_preferences.mealplan_autoadd_shopping"
|
||||
<b-form-checkbox v-model="useUserPreferenceStore().user_settings.mealplan_autoadd_shopping"
|
||||
@change="updateSettings(false)">{{ $t('mealplan_autoadd_shopping') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :description="$t('mealplan_autoexclude_onhand_desc')">
|
||||
<b-form-checkbox v-model="user_preferences.mealplan_autoexclude_onhand"
|
||||
<b-form-checkbox v-model="useUserPreferenceStore().user_settings.mealplan_autoexclude_onhand"
|
||||
@change="updateSettings(false)">{{ $t('mealplan_autoexclude_onhand') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :description="$t('mealplan_autoinclude_related_desc')">
|
||||
<b-form-checkbox v-model="user_preferences.mealplan_autoinclude_related"
|
||||
<b-form-checkbox v-model="useUserPreferenceStore().user_settings.mealplan_autoinclude_related"
|
||||
@change="updateSettings(false)">{{ $t('mealplan_autoinclude_related') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :description="$t('shopping_add_onhand_desc')">
|
||||
<b-form-checkbox v-model="user_preferences.shopping_add_onhand"
|
||||
<b-form-checkbox v-model="useUserPreferenceStore().user_settings.shopping_add_onhand"
|
||||
@change="updateSettings(false)">{{ $t('shopping_add_onhand') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :label="$t('default_delay')" :description="$t('default_delay_desc')">
|
||||
<b-form-input type="range" min="1" max="72" step="1" v-model="user_preferences.default_delay"
|
||||
<b-form-input type="range" min="1" max="72" step="1" v-model="useUserPreferenceStore().user_settings.default_delay"
|
||||
@change="updateSettings(false)"></b-form-input>
|
||||
<div class="text-center">
|
||||
<span>{{ Math.round(user_preferences.default_delay) }}
|
||||
<span v-if="user_preferences.default_delay === 1">{{ $t('Hour') }}</span>
|
||||
<span>{{ Math.round(useUserPreferenceStore().user_settings.default_delay) }}
|
||||
<span v-if="useUserPreferenceStore().user_settings.default_delay === 1">{{ $t('Hour') }}</span>
|
||||
<span v-else> {{ $t('Hours') }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :description="$t('filter_to_supermarket_desc')">
|
||||
<b-form-checkbox v-model="user_preferences.filter_to_supermarket"
|
||||
<b-form-checkbox v-model="useUserPreferenceStore().user_settings.filter_to_supermarket"
|
||||
@change="updateSettings(false)">{{ $t('filter_to_supermarket') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :label="$t('shopping_recent_days')" :description="$t('shopping_recent_days_desc')">
|
||||
<b-form-input type="range" min="0" max="14" step="1" v-model="user_preferences.shopping_recent_days"
|
||||
<b-form-input type="range" min="0" max="14" step="1" v-model="useUserPreferenceStore().user_settings.shopping_recent_days"
|
||||
@change="updateSettings(false)"></b-form-input>
|
||||
<div class="text-center">
|
||||
<span>{{ Math.round(user_preferences.shopping_recent_days) }}
|
||||
<span v-if="user_preferences.shopping_recent_days === 1">{{ $t('Day') }}</span>
|
||||
<span>{{ Math.round(useUserPreferenceStore().user_settings.shopping_recent_days) }}
|
||||
<span v-if="useUserPreferenceStore().user_settings.shopping_recent_days === 1">{{ $t('Day') }}</span>
|
||||
<span v-else> {{ $t('Days') }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :label="$t('csv_delim_label')" :description="$t('csv_delim_help')">
|
||||
<b-form-input v-model="user_preferences.csv_delim" @change="updateSettings(false)"></b-form-input>
|
||||
<b-form-input v-model="useUserPreferenceStore().user_settings.csv_delim" @change="updateSettings(false)"></b-form-input>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :label="$t('csv_prefix_label')" :description="$t('csv_prefix_help')">
|
||||
<b-form-input v-model="user_preferences.csv_prefix" @change="updateSettings(false)"></b-form-input>
|
||||
<b-form-input v-model="useUserPreferenceStore().user_settings.csv_prefix" @change="updateSettings(false)"></b-form-input>
|
||||
</b-form-group>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {ApiApiFactory} from "@/utils/openapi/api";
|
||||
import {ApiMixin, StandardToasts} from "@/utils/utils";
|
||||
|
||||
import axios from "axios";
|
||||
import GenericMultiselect from "@/components/GenericMultiselect";
|
||||
|
||||
axios.defaults.xsrfCookieName = 'csrftoken'
|
||||
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
||||
|
||||
export default {
|
||||
name: "ShoppingSettingsComponent",
|
||||
mixins: [ApiMixin],
|
||||
components: {GenericMultiselect},
|
||||
props: {
|
||||
user_id: Number,
|
||||
},
|
||||
props: { },
|
||||
data() {
|
||||
return {
|
||||
user_preferences: undefined,
|
||||
@ -115,30 +112,15 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.user_preferences = this.preferences
|
||||
this.languages = window.AVAILABLE_LANGUAGES
|
||||
this.loadSettings()
|
||||
|
||||
useUserPreferenceStore().loadUserSettings(false)
|
||||
},
|
||||
methods: {
|
||||
loadSettings: function () {
|
||||
let apiFactory = new ApiApiFactory()
|
||||
apiFactory.retrieveUserPreference(this.user_id.toString()).then(result => {
|
||||
this.user_preferences = result.data
|
||||
}).catch(err => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
|
||||
})
|
||||
},
|
||||
useUserPreferenceStore,
|
||||
updateSettings: function (reload) {
|
||||
let apiFactory = new ApiApiFactory()
|
||||
this.$emit('updated', this.user_preferences)
|
||||
apiFactory.partialUpdateUserPreference(this.user_id.toString(), this.user_preferences).then(result => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
|
||||
if (reload) {
|
||||
location.reload()
|
||||
}
|
||||
}).catch(err => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
|
||||
})
|
||||
useUserPreferenceStore().updateUserSettings()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,176 +1,134 @@
|
||||
<template>
|
||||
<div id="shopping_line_item" class="pt-1">
|
||||
<b-row align-h="start"
|
||||
ref="shopping_line_item" class="invis-border">
|
||||
<b-col cols="2" md="2" class="justify-content-start align-items-center d-flex d-md-none pr-0"
|
||||
v-if="settings.left_handed">
|
||||
<input type="checkbox" class="form-control form-control-sm checkbox-control-mobile"
|
||||
:checked="formatChecked" @change="updateChecked" :key="entries[0].id"/>
|
||||
<b-button size="sm" @click="showDetails = !showDetails" class="d-inline-block d-md-none p-0"
|
||||
variant="link">
|
||||
<div class="text-nowrap"><i class="fa fa-chevron-right rotate"
|
||||
:class="showDetails ? 'rotated' : ''"></i></div>
|
||||
</b-button>
|
||||
</b-col>
|
||||
<b-col cols="1" class="align-items-center d-flex">
|
||||
<div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true"
|
||||
@click.stop="$emit('open-context-menu', $event, entries)">
|
||||
<button
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
type="button"
|
||||
:class="settings.left_handed ? 'dropdown-spacing' : ''"
|
||||
class="btn dropdown-toggle btn-link text-decoration-none text-body pr-0 pl-1 pr-md-3 pl-md-3 dropdown-toggle-no-caret">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<div class="swipe-container" :id="item_container_id" @touchend="handleSwipe()"
|
||||
v-if="(useUserPreferenceStore().device_settings.shopping_show_checked_entries || !is_checked) && (useUserPreferenceStore().device_settings.shopping_show_delayed_entries || !is_delayed)"
|
||||
>
|
||||
<div class="swipe-action" :class="{'bg-success': !is_checked , 'bg-warning': is_checked }">
|
||||
<i class="swipe-icon fa-fw fas" :class="{'fa-check': !is_checked , 'fa-cart-plus': is_checked }"></i>
|
||||
</div>
|
||||
|
||||
<b-button-group class="swipe-element">
|
||||
<b-button variant="primary" v-if="is_delayed">
|
||||
<i class="fa-fw fas fa-hourglass-half"></i>
|
||||
</b-button>
|
||||
<div class="card flex-grow-1 btn-block p-2" @click="detail_modal_visible = true">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex flex-column pr-2" v-if="Object.keys(amounts).length> 0">
|
||||
<span v-for="a in amounts" v-bind:key="a.id">
|
||||
|
||||
<span><i class="fas fa-check" v-if="a.checked && !is_checked"></i><i class="fas fa-hourglass-half" v-if="a.delayed && !a.checked"></i> <b>{{ a.amount }} {{
|
||||
a.unit
|
||||
}} </b></span>
|
||||
<br/></span>
|
||||
|
||||
</div>
|
||||
<div class="d-flex flex-column flex-grow-1 align-self-center">
|
||||
{{ food.name }} <br/>
|
||||
<span v-if="info_row"><small class="text-muted">{{ info_row }}</small></span>
|
||||
</div>
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="1" class="px-1 justify-content-center align-items-center d-none d-md-flex">
|
||||
<input type="checkbox" class="form-control form-control-sm checkbox-control"
|
||||
:checked="formatChecked" @change="updateChecked" :key="entries[0].id"/>
|
||||
</b-col>
|
||||
<b-col cols="8">
|
||||
<b-row class="d-flex h-100">
|
||||
<b-col cols="6" md="3" class="d-flex align-items-center" v-touch:start="startHandler" v-touch:moving="moveHandler" v-touch:end="endHandler"
|
||||
v-if="Object.entries(formatAmount).length == 1">
|
||||
<strong class="mr-1">{{ Object.entries(formatAmount)[0][1] }}</strong>
|
||||
{{ Object.entries(formatAmount)[0][0] }}
|
||||
</b-col>
|
||||
<b-col cols="6" md="3" class="d-flex flex-column" v-touch:start="startHandler" v-touch:moving="moveHandler" v-touch:end="endHandler"
|
||||
v-if="Object.entries(formatAmount).length != 1">
|
||||
<div class="small" v-for="(x, i) in Object.entries(formatAmount)" :key="i">
|
||||
{{ x[1] }}  
|
||||
{{ x[0] }}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<b-button variant="success" @click="useShoppingListStore().setEntriesCheckedState(entries, !is_checked, true)"
|
||||
:class="{'btn-success': !is_checked, 'btn-warning': is_checked}">
|
||||
<i class="d-print-none fa-fw fas" :class="{'fa-check': !is_checked , 'fa-cart-plus': is_checked }"></i>
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
<div class="swipe-action bg-primary justify-content-end">
|
||||
<i class="fa-fw fas fa-hourglass-half swipe-icon"></i>
|
||||
</div>
|
||||
|
||||
<b-modal v-model="detail_modal_visible" @hidden="detail_modal_visible = false" body-class="pr-4 pl-4 pt-0">
|
||||
<template #modal-title>
|
||||
<h5> {{ food_row }}</h5>
|
||||
<small class="text-muted">{{ food.description }}</small>
|
||||
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<h5 class="mt-2">{{ $t('Quick actions') }}</h5>
|
||||
{{ $t('Category') }}
|
||||
<b-form-select
|
||||
class="form-control mb-2"
|
||||
:options="useShoppingListStore().supermarket_categories"
|
||||
text-field="name"
|
||||
value-field="id"
|
||||
v-model="food.supermarket_category"
|
||||
@change="detail_modal_visible = false; updateFoodCategory(food)"
|
||||
></b-form-select>
|
||||
|
||||
<b-button variant="info" block
|
||||
@click="detail_modal_visible = false;useShoppingListStore().delayEntries(entries,!is_delayed, true)">
|
||||
{{ $t('Postpone') }}
|
||||
</b-button>
|
||||
|
||||
|
||||
<h6 class="mt-2">{{ $t('Entries') }}</h6>
|
||||
|
||||
|
||||
<b-row v-for="e in entries" v-bind:key="e.id">
|
||||
<b-col cold="12">
|
||||
|
||||
<b-button-group class="w-100">
|
||||
<div class="card flex-grow-1 btn-block p-2">
|
||||
<span><i class="fas fa-check" v-if="e.checked"></i><i class="fas fa-hourglass-half" v-if="e.delay_until !== null && !e.checked"></i>
|
||||
<b><span v-if="e.amount > 0">{{ e.amount }}</span> {{ e.unit?.name }}</b> {{ food.name }}</span>
|
||||
<span><small class="text-muted">
|
||||
<span v-if="e.recipe_mealplan && e.recipe_mealplan.recipe_name !== ''">
|
||||
<a :href="resolveDjangoUrl('view_recipe', e.recipe_mealplan.recipe)"> <b> {{
|
||||
e.recipe_mealplan.recipe_name
|
||||
}} </b></a>({{
|
||||
e.recipe_mealplan.servings
|
||||
}} {{ $t('Servings') }})<br/>
|
||||
</span>
|
||||
<span
|
||||
v-if="e.recipe_mealplan && e.recipe_mealplan.mealplan_type !== undefined"> {{
|
||||
e.recipe_mealplan.mealplan_type
|
||||
}} {{ formatDate(e.recipe_mealplan.mealplan_from_date) }} <br/></span>
|
||||
|
||||
{{ e.created_by.display_name }} {{ formatDate(e.created_at) }}<br/>
|
||||
</small></span>
|
||||
|
||||
</div>
|
||||
<b-button variant="outline-danger"
|
||||
@click="useShoppingListStore().deleteObject(e)"><i
|
||||
class="fas fa-trash"></i></b-button>
|
||||
</b-button-group>
|
||||
|
||||
<generic-multiselect
|
||||
class="mt-1"
|
||||
v-if="e.recipe_mealplan === null"
|
||||
:initial_single_selection="e.unit"
|
||||
:model="Models.UNIT"
|
||||
:multiple="false"
|
||||
@change="e.unit = $event.val; useShoppingListStore().updateObject(e)"
|
||||
>
|
||||
</generic-multiselect>
|
||||
|
||||
<number-scaler-component :number="e.amount"
|
||||
@change="e.amount = $event; useShoppingListStore().updateObject(e)"
|
||||
v-if="e.recipe_mealplan === null"></number-scaler-component>
|
||||
<hr class="m-2"/>
|
||||
</b-col>
|
||||
|
||||
<b-col cols="6" md="6" class="align-items-center d-flex pl-0 pr-0 pl-md-2 pr-md-2">
|
||||
{{ formatFood }}
|
||||
</b-col>
|
||||
<b-col cols="3" data-html2canvas-ignore="true" v-touch:start="startHandler" v-touch:moving="moveHandler" v-touch:end="endHandler"
|
||||
class="align-items-center d-none d-md-flex justify-content-end">
|
||||
<b-button size="sm" @click="showDetails = !showDetails"
|
||||
class="p-0 mr-0 mr-md-2 p-md-2 text-decoration-none" variant="link">
|
||||
<div class="text-nowrap">
|
||||
<i class="fa fa-chevron-right rotate" :class="showDetails ? 'rotated' : ''"></i>
|
||||
<span class="d-none d-md-inline-block"><span class="ml-2">{{
|
||||
$t("Details")
|
||||
}}</span></span>
|
||||
</div>
|
||||
</b-button>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-col>
|
||||
<b-col cols="2" class="justify-content-start align-items-center d-flex d-md-none pl-0 pr-0" v-touch:start="startHandler" v-touch:moving="moveHandler" v-touch:end="endHandler"
|
||||
v-if="!settings.left_handed">
|
||||
<b-button size="sm" @click="showDetails = !showDetails" class="d-inline-block d-md-none p-0"
|
||||
variant="link">
|
||||
<div class="text-nowrap"><i class="fa fa-chevron-right rotate"
|
||||
:class="showDetails ? 'rotated' : ''"></i></div>
|
||||
|
||||
<b-button variant="success" block @click="useShoppingListStore().createObject({ amount: 0, unit: null, food: food, })"> {{ $t("Add") }}</b-button>
|
||||
<b-button variant="warning" block @click="detail_modal_visible = false; setFoodIgnoredAndChecked(food)"> {{ $t("Ignore_Shopping") }}</b-button>
|
||||
<b-button variant="danger" block class="mt-2"
|
||||
@click="detail_modal_visible = false;useShoppingListStore().deleteEntries(entries)">
|
||||
{{ $t('Delete_All') }}
|
||||
</b-button>
|
||||
<input type="checkbox" class="form-control form-control-sm checkbox-control-mobile"
|
||||
:checked="formatChecked" @change="updateChecked" :key="entries[0].id"/>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row align-h="center" class="d-none d-md-flex">
|
||||
<b-col cols="12">
|
||||
<div class="small text-muted text-truncate">{{ formatHint }}</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<!-- detail rows -->
|
||||
<div class="card no-body mb-1 pt-2 align-content-center shadow-sm" v-if="showDetails">
|
||||
<div v-for="(e, x) in entries" :key="e.id">
|
||||
<b-row class="small justify-content-around">
|
||||
<b-col cols="auto" md="4" class="overflow-hidden text-nowrap">
|
||||
<button
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
type="button"
|
||||
class="btn btn-link btn-sm m-0 p-0 pl-2"
|
||||
style="text-overflow: ellipsis"
|
||||
@click.stop="openRecipeCard($event, e)"
|
||||
@mouseover="openRecipeCard($event, e)"
|
||||
>
|
||||
{{ formatOneRecipe(e) }}
|
||||
</button>
|
||||
</b-col>
|
||||
<b-col cols="auto" md="4" class="text-muted">{{ formatOneMealPlan(e) }}</b-col>
|
||||
<b-col cols="auto" md="4" class="text-muted text-right overflow-hidden text-nowrap pr-4">
|
||||
{{ formatOneCreatedBy(e) }}
|
||||
<div v-if="formatOneCompletedAt(e)">{{ formatOneCompletedAt(e) }}</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row align-h="start">
|
||||
<b-col cols="3" md="2" class="justify-content-start align-items-center d-flex d-md-none pr-0"
|
||||
v-if="settings.left_handed">
|
||||
<input type="checkbox" class="form-control form-control-sm checkbox-control-mobile"
|
||||
:checked="formatChecked" @change="updateChecked" :key="entries[0].id"/>
|
||||
</b-col>
|
||||
<b-col cols="2" md="1" class="align-items-center d-flex">
|
||||
<div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true"
|
||||
@click.stop="$emit('open-context-menu', $event, e)">
|
||||
<button
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
type="button"
|
||||
:class="settings.left_handed ? 'dropdown-spacing' : ''"
|
||||
class="btn dropdown-toggle btn-link text-decoration-none text-body pr-1 pr-md-3 pl-md-3 dropdown-toggle-no-caret"
|
||||
>
|
||||
<i class="fas fa-ellipsis-v fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="1" class="justify-content-center align-items-center d-none d-md-flex">
|
||||
<input type="checkbox" class="form-control form-control-sm checkbox-control"
|
||||
:checked="formatChecked" @change="updateChecked" :key="entries[0].id"/>
|
||||
</b-col>
|
||||
<b-col cols="7" md="9">
|
||||
<b-row class="d-flex align-items-center h-100">
|
||||
<b-col cols="5" md="3" class="d-flex align-items-center">
|
||||
<strong class="mr-1">{{ formatOneAmount(e) }}</strong> {{ formatOneUnit(e) }}
|
||||
</b-col>
|
||||
<b-col cols="7" md="6" class="align-items-center d-flex pl-0 pr-0 pl-md-2 pr-md-2">
|
||||
{{ formatOneFood(e) }}
|
||||
</b-col>
|
||||
<b-col cols="12" class="d-flex d-md-none">
|
||||
<div class="small text-muted text-truncate" v-for="(n, i) in formatOneNote(e)"
|
||||
:key="i">{{ n }}
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-col>
|
||||
<b-col cols="3" md="2" class="justify-content-start align-items-center d-flex d-md-none"
|
||||
v-if="!settings.left_handed">
|
||||
<input type="checkbox" class="form-control form-control-sm checkbox-control-mobile"
|
||||
:checked="formatChecked" @change="updateChecked" :key="entries[0].id"/>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<hr class="w-75 mt-1 mb-1 mt-md-3 mb-md-3" v-if="x !== entries.length - 1"/>
|
||||
<div class="pb-1 pb-md-4" v-if="x === entries.length - 1"></div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="m-1" v-if="!showDetails"/>
|
||||
<ContextMenu ref="recipe_card" triggers="click, hover" :title="$t('Filters')" style="max-width: 300">
|
||||
<template #menu="{ contextData }" v-if="recipe">
|
||||
<ContextMenuItem>
|
||||
<RecipeCard :recipe="contextData" :detail="false"></RecipeCard>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem @click="$refs.menu.close()">
|
||||
<b-form-group label-cols="9" content-cols="3" class="text-nowrap m-0 mr-2">
|
||||
<template #label>
|
||||
<a class="dropdown-item p-2" href="#"><i class="fas fa-pizza-slice"></i>
|
||||
{{ $t("Servings") }}</a>
|
||||
</template>
|
||||
<div @click.prevent.stop>
|
||||
<b-form-input class="mt-2" min="0" type="number" v-model="servings"></b-form-input>
|
||||
</div>
|
||||
</b-form-group>
|
||||
</ContextMenuItem>
|
||||
</template>
|
||||
</ContextMenu>
|
||||
<i class="fa fa-hourglass fa-lg" style="display: none; position: absolute" aria-hidden="true"
|
||||
ref="delay_icon"></i>
|
||||
<i class="fa fa-check fa-lg" style="display: none; position: absolute" aria-hidden="true" ref="check_icon"></i>
|
||||
|
||||
<template #modal-footer>
|
||||
<span></span>
|
||||
</template>
|
||||
</b-modal>
|
||||
|
||||
<generic-modal-form :model="Models.FOOD" :show="editing_food !== null"
|
||||
@hidden="editing_food = null; useShoppingListStore().refreshFromAPI()"></generic-modal-form>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -178,292 +136,237 @@
|
||||
import Vue from "vue"
|
||||
import {BootstrapVue} from "bootstrap-vue"
|
||||
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||
import ContextMenu from "@/components/ContextMenu/ContextMenu"
|
||||
import ContextMenuItem from "@/components/ContextMenu/ContextMenuItem"
|
||||
import {ApiMixin} from "@/utils/utils"
|
||||
import RecipeCard from "./RecipeCard.vue"
|
||||
import Vue2TouchEvents from "vue2-touch-events"
|
||||
import {ApiMixin, FormatMixin, resolveDjangoUrl, StandardToasts} from "@/utils/utils"
|
||||
import {useMealPlanStore} from "@/stores/MealPlanStore";
|
||||
import {useShoppingListStore} from "@/stores/ShoppingListStore";
|
||||
import {ApiApiFactory} from "@/utils/openapi/api";
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
||||
import NumberScalerComponent from "@/components/NumberScalerComponent.vue";
|
||||
import GenericModalForm from "@/components/Modals/GenericModalForm.vue";
|
||||
import GenericMultiselect from "@/components/GenericMultiselect.vue";
|
||||
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
Vue.use(Vue2TouchEvents)
|
||||
|
||||
export default {
|
||||
// TODO ApiGenerator doesn't capture and share error information - would be nice to share error details when available
|
||||
// or i'm capturing it incorrectly
|
||||
name: "ShoppingLineItem",
|
||||
mixins: [ApiMixin],
|
||||
components: {RecipeCard, ContextMenu, ContextMenuItem},
|
||||
mixins: [ApiMixin, FormatMixin],
|
||||
components: {GenericMultiselect, GenericModalForm, NumberScalerComponent},
|
||||
props: {
|
||||
entries: {
|
||||
type: Array,
|
||||
},
|
||||
settings: Object,
|
||||
groupby: {type: String},
|
||||
entries: {type: Object,},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showDetails: false,
|
||||
recipe: undefined,
|
||||
servings: 1,
|
||||
dragStartX: 0,
|
||||
distance_left: 0
|
||||
detail_modal_visible: false,
|
||||
editing_food: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
formatAmount: function () {
|
||||
let amount = {}
|
||||
this.entries.forEach((entry) => {
|
||||
let unit = entry?.unit?.name ?? "---"
|
||||
if (entry.amount) {
|
||||
if (amount[unit]) {
|
||||
amount[unit] += entry.amount
|
||||
} else {
|
||||
amount[unit] = entry.amount
|
||||
item_container_id: function () {
|
||||
let id = 'id_sli_'
|
||||
for (let i in this.entries) {
|
||||
id += i + '_'
|
||||
}
|
||||
return id
|
||||
},
|
||||
is_checked: function () {
|
||||
for (let i in this.entries) {
|
||||
if (!this.entries[i].checked) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
is_delayed: function () {
|
||||
for (let i in this.entries) {
|
||||
if (Date.parse(this.entries[i].delay_until) > new Date(Date.now())) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
food: function () {
|
||||
return this.entries[Object.keys(this.entries)[0]]['food']
|
||||
},
|
||||
amounts: function () {
|
||||
let unit_amounts = {}
|
||||
|
||||
for (let i in this.entries) {
|
||||
let e = this.entries[i]
|
||||
|
||||
if (!e.checked && e.delay_until === null
|
||||
|| (e.checked && useUserPreferenceStore().device_settings.shopping_show_checked_entries)
|
||||
|| (e.delay_until !== null && useUserPreferenceStore().device_settings.shopping_show_delayed_entries)) {
|
||||
|
||||
let unit = -1
|
||||
if (e.unit !== undefined && e.unit !== null) {
|
||||
unit = e.unit.id
|
||||
}
|
||||
if (e.amount > 0) {
|
||||
if (unit in unit_amounts) {
|
||||
unit_amounts[unit]['amount'] += e.amount
|
||||
} else {
|
||||
if (unit === -1) {
|
||||
unit_amounts[unit] = {id: -1, unit: "", amount: e.amount, checked: e.checked, delayed: (e.delay_until !== null)}
|
||||
} else {
|
||||
unit_amounts[unit] = {id: e.unit.id, unit: e.unit.name, amount: e.amount, checked: e.checked, delayed: (e.delay_until !== null)}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
for (const [k, v] of Object.entries(amount)) {
|
||||
amount[k] = Math.round(v * 100 + Number.EPSILON) / 100 // javascript hack to force rounding at 2 places
|
||||
}
|
||||
return amount
|
||||
return unit_amounts
|
||||
},
|
||||
formatCategory: function () {
|
||||
return this.formatOneCategory(this.entries[0]) || this.$t("Undefined")
|
||||
food_row: function () {
|
||||
return this.food.name
|
||||
},
|
||||
formatChecked: function () {
|
||||
return this.entries.map((x) => x.checked).every((x) => x === true)
|
||||
},
|
||||
formatHint: function () {
|
||||
if (this.groupby == "recipe") {
|
||||
return this.formatCategory
|
||||
} else {
|
||||
return this.formatRecipe
|
||||
}
|
||||
},
|
||||
formatFood: function () {
|
||||
return this.formatOneFood(this.entries[0])
|
||||
},
|
||||
formatUnit: function () {
|
||||
return this.formatOneUnit(this.entries[0])
|
||||
},
|
||||
formatRecipe: function () {
|
||||
if (this.entries?.length == 1) {
|
||||
return this.formatOneMealPlan(this.entries[0]) || ""
|
||||
} else {
|
||||
let mealplan_name = this.entries.filter((x) => x?.recipe_mealplan?.name)
|
||||
// return [this.formatOneMealPlan(mealplan_name?.[0]), this.$t("CountMore", { count: this.entries?.length - 1 })].join(" ")
|
||||
info_row: function () {
|
||||
let info_row = []
|
||||
|
||||
return mealplan_name
|
||||
.map((x) => {
|
||||
return this.formatOneMealPlan(x)
|
||||
})
|
||||
.join(" - ")
|
||||
let authors = []
|
||||
let recipes = []
|
||||
let meal_pans = []
|
||||
|
||||
for (let i in this.entries) {
|
||||
let e = this.entries[i]
|
||||
|
||||
if (authors.indexOf(e.created_by.display_name) === -1) {
|
||||
authors.push(e.created_by.display_name)
|
||||
}
|
||||
|
||||
|
||||
if (e.recipe_mealplan !== null) {
|
||||
let recipe_name = e.recipe_mealplan.recipe_name
|
||||
if (recipes.indexOf(recipe_name) === -1) {
|
||||
recipes.push(recipe_name.substring(0, 14) + (recipe_name.length > 14 ? '..' : ''))
|
||||
}
|
||||
|
||||
if ('mealplan_from_date' in e.recipe_mealplan) {
|
||||
let meal_plan_entry = (e?.recipe_mealplan?.mealplan_type || '') + ' (' + this.formatDate(e.recipe_mealplan.mealplan_from_date) + ')'
|
||||
if (meal_pans.indexOf(meal_plan_entry) === -1) {
|
||||
meal_pans.push(meal_plan_entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
formatNotes: function () {
|
||||
if (this.entries?.length == 1) {
|
||||
return this.formatOneNote(this.entries[0]) || ""
|
||||
|
||||
if (useUserPreferenceStore().device_settings.shopping_item_info_created_by && authors.length > 0) {
|
||||
info_row.push(authors.join(', '))
|
||||
}
|
||||
return ""
|
||||
},
|
||||
if (useUserPreferenceStore().device_settings.shopping_item_info_recipe && recipes.length > 0) {
|
||||
info_row.push(recipes.join(', '))
|
||||
}
|
||||
if (useUserPreferenceStore().device_settings.shopping_item_info_mealplan && meal_pans.length > 0) {
|
||||
info_row.push(meal_pans.join(', '))
|
||||
}
|
||||
|
||||
return info_row.join(' - ')
|
||||
}
|
||||
},
|
||||
watch: {},
|
||||
mounted() {
|
||||
this.servings = this.entries?.[0]?.recipe_mealplan?.servings ?? 0
|
||||
|
||||
},
|
||||
methods: {
|
||||
// this.genericAPI inherited from ApiMixin
|
||||
|
||||
formatDate: function (datetime) {
|
||||
if (!datetime) {
|
||||
return
|
||||
useUserPreferenceStore,
|
||||
useShoppingListStore,
|
||||
resolveDjangoUrl,
|
||||
/**
|
||||
* update the food after the category was changed
|
||||
* handle changing category to category ID as a workaround
|
||||
* @param food
|
||||
*/
|
||||
updateFoodCategory: function (food) {
|
||||
if (typeof food.supermarket_category === "number") { // not the best solution, but as long as generic multiselect does not support caching, I don't want to use a proper model
|
||||
food.supermarket_category = this.useShoppingListStore().supermarket_categories.filter(sc => sc.id === food.supermarket_category)[0]
|
||||
}
|
||||
return Intl.DateTimeFormat(window.navigator.language, {
|
||||
dateStyle: "short",
|
||||
timeStyle: "short",
|
||||
}).format(Date.parse(datetime))
|
||||
},
|
||||
startHandler: function (event) {
|
||||
if (event.changedTouches.length > 0) {
|
||||
this.dragStartX = event.changedTouches[0].clientX
|
||||
}
|
||||
},
|
||||
getOffset(el) {
|
||||
let rect = el.getBoundingClientRect();
|
||||
return {
|
||||
left: rect.left + window.scrollX,
|
||||
top: rect.top + window.scrollY,
|
||||
right: rect.right - window.scrollX,
|
||||
};
|
||||
},
|
||||
moveHandler: function (event) {
|
||||
let item = this.$refs['shopping_line_item'];
|
||||
this.distance_left = event.changedTouches[0].clientX - this.dragStartX;
|
||||
item.style.marginLeft = this.distance_left
|
||||
item.style.marginRight = -this.distance_left
|
||||
item.style.backgroundColor = '#ddbf86'
|
||||
item.style.border = "1px solid #000"
|
||||
|
||||
let delay_icon = this.$refs['delay_icon']
|
||||
let check_icon = this.$refs['check_icon']
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.updateFood(food.id, food).then(r => {
|
||||
|
||||
let color_factor = Math.abs(this.distance_left) / 100
|
||||
|
||||
if (this.distance_left > 0) {
|
||||
item.parentElement.parentElement.style.backgroundColor = 'rgba(130,170,139,0)'.replace(/[^,]+(?=\))/, color_factor)
|
||||
check_icon.style.display = "block"
|
||||
check_icon.style.left = this.getOffset(item.parentElement.parentElement).left + 40
|
||||
check_icon.style.top = this.getOffset(item.parentElement.parentElement).top - 92
|
||||
check_icon.style.opacity = color_factor - 0.3
|
||||
} else {
|
||||
item.parentElement.parentElement.style.backgroundColor = 'rgba(185,135,102,0)'.replace(/[^,]+(?=\))/, color_factor)
|
||||
delay_icon.style.display = "block"
|
||||
delay_icon.style.left = this.getOffset(item.parentElement.parentElement).right - 40
|
||||
delay_icon.style.top = this.getOffset(item.parentElement.parentElement).top - 92
|
||||
delay_icon.style.opacity = color_factor - 0.3
|
||||
}
|
||||
},
|
||||
endHandler: function (event) {
|
||||
let item = this.$refs['shopping_line_item'];
|
||||
item.removeAttribute('style');
|
||||
item.parentElement.parentElement.removeAttribute('style');
|
||||
|
||||
let delay_icon = this.$refs['delay_icon']
|
||||
let check_icon = this.$refs['check_icon']
|
||||
|
||||
delay_icon.style.display = "none"
|
||||
check_icon.style.display = "none"
|
||||
|
||||
if (Math.abs(this.distance_left) > window.screen.width / 6) {
|
||||
if (this.distance_left > 0) {
|
||||
let checked = false;
|
||||
this.entries.forEach((cur) => {
|
||||
checked = cur.checked
|
||||
})
|
||||
let update = {entries: this.entries.map((x) => x.id), checked: !checked}
|
||||
this.$emit("update-checkbox", update)
|
||||
} else {
|
||||
this.$emit("update-delaythis", this.entries)
|
||||
}
|
||||
}
|
||||
},
|
||||
formatOneAmount: function (item) {
|
||||
return item?.amount ?? 1
|
||||
},
|
||||
formatOneUnit: function (item) {
|
||||
return item?.unit?.name ?? ""
|
||||
},
|
||||
formatOneCategory: function (item) {
|
||||
return item?.food?.supermarket_category?.name
|
||||
},
|
||||
formatOneCompletedAt: function (item) {
|
||||
if (!item.completed_at) {
|
||||
return false
|
||||
}
|
||||
return [this.$t("Completed"), "@", this.formatDate(item.completed_at)].join(" ")
|
||||
},
|
||||
formatOneFood: function (item) {
|
||||
return item.food.name
|
||||
},
|
||||
formatOneDelayUntil: function (item) {
|
||||
if (!item.delay_until || (item.delay_until && item.checked)) {
|
||||
return false
|
||||
}
|
||||
return [this.$t("DelayUntil"), "-", this.formatDate(item.delay_until)].join(" ")
|
||||
},
|
||||
formatOneMealPlan: function (item) {
|
||||
return item?.recipe_mealplan?.name ?? ""
|
||||
},
|
||||
formatOneRecipe: function (item) {
|
||||
return item?.recipe_mealplan?.recipe_name ?? ""
|
||||
},
|
||||
formatOneNote: function (item) {
|
||||
if (!item) {
|
||||
item = this.entries[0]
|
||||
}
|
||||
return [item?.recipe_mealplan?.mealplan_note, item?.ingredient_note].filter(String)
|
||||
},
|
||||
formatOneCreatedBy: function (item) {
|
||||
return [this.$t("Added_by"), item?.created_by.display_name, "@", this.formatDate(item.created_at)].join(" ")
|
||||
},
|
||||
openRecipeCard: function (e, item) {
|
||||
this.genericAPI(this.Models.RECIPE, this.Actions.FETCH, {id: item.recipe_mealplan.recipe}).then((result) => {
|
||||
let recipe = result.data
|
||||
recipe.steps = undefined
|
||||
this.recipe = true
|
||||
this.$refs.recipe_card.open(e, recipe)
|
||||
}).catch((err) => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
|
||||
})
|
||||
},
|
||||
updateChecked: function (e, item) {
|
||||
let update = undefined
|
||||
if (!item) {
|
||||
update = {entries: this.entries.map((x) => x.id), checked: !this.formatChecked}
|
||||
} else {
|
||||
update = {entries: [item], checked: !item.checked}
|
||||
}
|
||||
console.log(update)
|
||||
this.$emit("update-checkbox", update)
|
||||
/**
|
||||
* set food on_hand status to true and check all associated entries
|
||||
* @param food
|
||||
*/
|
||||
setFoodIgnoredAndChecked: function (food) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
|
||||
food.ignore_shopping = true
|
||||
apiClient.updateFood(food.id, food).then(r => {
|
||||
}).catch((err) => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
|
||||
})
|
||||
|
||||
useShoppingListStore().setEntriesCheckedState(this.entries, true, false)
|
||||
},
|
||||
/**
|
||||
* function triggered by touchend event of swipe container
|
||||
* check if min distance is reached and execute desired action
|
||||
*/
|
||||
handleSwipe: function () {
|
||||
const minDistance = 80;
|
||||
const container = document.querySelector('#' + this.item_container_id);
|
||||
// get the distance the user swiped
|
||||
const swipeDistance = container.scrollLeft - container.clientWidth;
|
||||
if (swipeDistance < minDistance * -1) {
|
||||
useShoppingListStore().setEntriesCheckedState(this.entries, !this.is_checked, true)
|
||||
} else if (swipeDistance > minDistance) {
|
||||
useShoppingListStore().delayEntries(this.entries, !this.is_delayed, true)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<!--style src="vue-multiselect/dist/vue-multiselect.min.css"></style-->
|
||||
|
||||
<style>
|
||||
/* table { border-collapse:collapse } /* Ensure no space between cells */
|
||||
/* tr.strikeout td { position:relative } /* Setup a new coordinate system */
|
||||
/* tr.strikeout td:before { /* Create a new element that */
|
||||
/* content: " "; /* …has no text content */
|
||||
/* position: absolute; /* …is absolutely positioned */
|
||||
/* left: 0; top: 50%; width: 100%; /* …with the top across the middle */
|
||||
/* border-bottom: 1px solid #000; /* …and with a border on the top */
|
||||
/* } */
|
||||
.checkbox-control {
|
||||
font-size: 0.6rem;
|
||||
/* scroll snap takes care of restoring scroll position */
|
||||
.swipe-container {
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
overflow-x: scroll;
|
||||
scroll-snap-type: x mandatory;
|
||||
}
|
||||
|
||||
.checkbox-control-mobile {
|
||||
font-size: 1rem;
|
||||
/* scrollbar should be hidden */
|
||||
.swipe-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rotate {
|
||||
-moz-transition: all 0.25s linear;
|
||||
-webkit-transition: all 0.25s linear;
|
||||
transition: all 0.25s linear;
|
||||
.swipe-container {
|
||||
scrollbar-width: none; /* For Firefox */
|
||||
}
|
||||
|
||||
.rotated {
|
||||
-moz-transform: rotate(90deg);
|
||||
-webkit-transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
/* main element should always snap into view */
|
||||
.swipe-element {
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
.unit-badge-lg {
|
||||
font-size: 1rem !important;
|
||||
font-weight: 500 !important;
|
||||
.swipe-icon {
|
||||
color: white;
|
||||
position: sticky;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dropdown-spacing {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
/* swipe-actions and element should be 100% wide */
|
||||
.swipe-action,
|
||||
.swipe-element {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.invis-border {
|
||||
border: 1px solid transparent;
|
||||
.swipe-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.fa-ellipsis-v {
|
||||
font-size: 20px;
|
||||
}
|
||||
.right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.fa-ellipsis-v {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -158,7 +158,7 @@
|
||||
"Create_New_Unit": "Neue Einheit hinzufügen",
|
||||
"Instructions": "Anleitung",
|
||||
"Time": "Zeit",
|
||||
"New_Keyword": "Neues Schlagwort",
|
||||
"New_Keyword": "Neues Stichwort",
|
||||
"Delete_Keyword": "Schlagwort löschen",
|
||||
"show_split_screen": "Geteilte Ansicht",
|
||||
"Recipes_per_page": "Rezepte pro Seite",
|
||||
@ -555,5 +555,13 @@
|
||||
"FDC_ID_help": "FDC Datenbank ID",
|
||||
"CustomImageHelp": "Laden Sie ein Bild hoch, das in der Space-Übersicht angezeigt werden soll.",
|
||||
"CustomNavLogoHelp": "Laden Sie ein Bild hoch, das als Logo für die Navigationsleiste verwendet werden soll.",
|
||||
"CustomLogos": "Individuelle Logos"
|
||||
"CustomLogos": "Individuelle Logos",
|
||||
"Input": "Eingabe",
|
||||
"Undo": "Rückgängig",
|
||||
"NoMoreUndo": "Rückgängig: Keine Änderungen",
|
||||
"created_by": "Erstellt von",
|
||||
"ShoppingBackgroundSyncWarning": "Schlechte Netzwerkverbindung, Warten auf Synchronisation ...",
|
||||
"ShowRecentlyCompleted": "Zuletzt abgehakte Zutaten zeigen",
|
||||
"Enable": "Aktivieren",
|
||||
"Delete_All": "Alles löschen"
|
||||
}
|
||||
|
@ -96,6 +96,9 @@
|
||||
"base_unit": "Base Unit",
|
||||
"base_amount": "Base Amount",
|
||||
"Datatype": "Datatype",
|
||||
"Input": "Input",
|
||||
"Undo": "Undo",
|
||||
"NoMoreUndo": "No changes to be undone.",
|
||||
"Number of Objects": "Number of Objects",
|
||||
"Add_Step": "Add Step",
|
||||
"Keywords": "Keywords",
|
||||
@ -141,6 +144,7 @@
|
||||
"Edit": "Edit",
|
||||
"Image": "Image",
|
||||
"Delete": "Delete",
|
||||
"Delete_All": "Delete all",
|
||||
"Open": "Open",
|
||||
"Ok": "Ok",
|
||||
"Save": "Save",
|
||||
@ -239,6 +243,7 @@
|
||||
"Week": "Week",
|
||||
"Month": "Month",
|
||||
"Year": "Year",
|
||||
"created_by": "Created by",
|
||||
"Planner": "Planner",
|
||||
"Planner_Settings": "Planner settings",
|
||||
"Period": "Period",
|
||||
@ -295,9 +300,11 @@
|
||||
"Warning": "Warning",
|
||||
"NoCategory": "No category selected.",
|
||||
"InheritWarning": "{food} is set to inherit, changes may not persist.",
|
||||
"ShowDelayed": "Show Delayed Items",
|
||||
"ShowDelayed": "Show delayed items",
|
||||
"ShowRecentlyCompleted": "Show recently completed items",
|
||||
"Completed": "Completed",
|
||||
"OfflineAlert": "You are offline, shopping list may not syncronize.",
|
||||
"ShoppingBackgroundSyncWarning": "Bad network, waiting to sync ...",
|
||||
"shopping_share": "Share Shopping List",
|
||||
"shopping_auto_sync": "Autosync",
|
||||
"one_url_per_line": "One URL per line",
|
||||
@ -496,6 +503,7 @@
|
||||
"Reset": "Reset",
|
||||
"Disabled": "Disabled",
|
||||
"Disable": "Disable",
|
||||
"Enable": "Enable",
|
||||
"Options": "Options",
|
||||
"Create Food": "Create Food",
|
||||
"create_food_desc": "Create a food and link it to this recipe.",
|
||||
|
@ -554,5 +554,13 @@
|
||||
"CustomLogos": "Własne loga",
|
||||
"Show_Logo": "Pokaż logo",
|
||||
"Nav_Text_Mode": "Tryb nawigacji tekstowej",
|
||||
"Nav_Text_Mode_Help": "Zachowuje się inaczej dla każdego motywu."
|
||||
"Nav_Text_Mode_Help": "Zachowuje się inaczej dla każdego motywu.",
|
||||
"Input": "Wprowadź",
|
||||
"Undo": "Cofnij",
|
||||
"NoMoreUndo": "Brak zmian do wycofania.",
|
||||
"Delete_All": "Usuń wszystko",
|
||||
"ShowRecentlyCompleted": "Pokaż ostatnio zakończone elementy",
|
||||
"ShoppingBackgroundSyncWarning": "Słaba sieć, oczekiwanie na synchronizację...",
|
||||
"Enable": "Włączyć",
|
||||
"created_by": "Stworzone przez"
|
||||
}
|
||||
|
@ -480,5 +480,83 @@
|
||||
"Create Recipe": "创建食谱",
|
||||
"Import Recipe": "导入食谱",
|
||||
"recipe_property_info": "您也可以为食物添加属性,以便根据食谱自动计算!",
|
||||
"per_serving": "每份"
|
||||
"per_serving": "每份",
|
||||
"converted_amount": "换算量",
|
||||
"Open_Data_Import": "开放数据导入",
|
||||
"StartDate": "开始日期",
|
||||
"EndDate": "结束日期",
|
||||
"OrderInformation": "对象按照从小到大的顺序排列。",
|
||||
"total": "全部",
|
||||
"pound": "磅(重量)",
|
||||
"imperial_quart": "英制夸脱【imp qt】(英制,体积)",
|
||||
"imperial_pint": "英制品脱【imp pt】(英制,体积)",
|
||||
"imperial_tbsp": "英制汤匙【imp tbsp】(英制,体积)",
|
||||
"make_now_count": "至多缺少的成分",
|
||||
"l": "升【l】(公制,体积)",
|
||||
"Welcome": "欢迎",
|
||||
"Input": "输入",
|
||||
"Undo": "撤销",
|
||||
"Number of Objects": "对象数量",
|
||||
"Alignment": "校准",
|
||||
"Delete_All": "全部删除",
|
||||
"Conversion": "转换",
|
||||
"Properties": "属性",
|
||||
"ShowRecentlyCompleted": "显示最近完成的项目",
|
||||
"ShoppingBackgroundSyncWarning": "网络状况不佳,正在等待进行同步……",
|
||||
"show_step_ingredients_setting": "在食谱步骤旁边显示成分",
|
||||
"show_step_ingredients_setting_help": "在食谱步骤旁边添加成分表。在创建时应用。可以在编辑配方视图中覆盖。",
|
||||
"show_step_ingredients": "显示该步骤的成分",
|
||||
"hide_step_ingredients": "隐藏该步骤的成分",
|
||||
"Logo": "徽标",
|
||||
"Show_Logo": "显示徽标",
|
||||
"Show_Logo_Help": "在导航栏中显示 Tandoor 或空间徽标。",
|
||||
"Nav_Text_Mode": "文本导航模式",
|
||||
"Nav_Text_Mode_Help": "每个主题的行为都不同。",
|
||||
"Space_Cosmetic_Settings": "空间管理员可以更改某些装饰设置,并将覆盖该空间的客户端设置。",
|
||||
"show_ingredients_table": "在步骤文本旁边显示成分表",
|
||||
"Enable": "启用",
|
||||
"g": "克【g】(公制,重量)",
|
||||
"gallon": "加仑【gal】美制,体积)",
|
||||
"tbsp": "汤匙【tbsp】(美制,体积)",
|
||||
"tsp": "茶匙【tsp】(美制,体积)",
|
||||
"imperial_gallon": "英制加仑【imp gal】(英制,体积)",
|
||||
"imperial_tsp": "英制茶匙【imp tsp】(英制,体积)",
|
||||
"Choose_Category": "选择类别",
|
||||
"Back": "后退",
|
||||
"Food_Replace": "食物替换",
|
||||
"Unit_Replace": "单位替换",
|
||||
"Datatype": "数据类型",
|
||||
"NoMoreUndo": "没有可撤消的更改。",
|
||||
"FDC_Search": "FDC搜索",
|
||||
"FDC_ID_help": "FDC数据库ID",
|
||||
"property_type_fdc_hint": "只有具有 FDC ID 的属性类型才能自动从 FDC 数据库中提取数据",
|
||||
"Data_Import_Info": "通过导入社区精选的食物、单位等列表来增强您的空间,以提升您的食谱收藏。",
|
||||
"Update_Existing_Data": "更新现有数据",
|
||||
"Use_Metric": "使用公制单位",
|
||||
"Learn_More": "了解更多",
|
||||
"converted_unit": "换算单位",
|
||||
"base_unit": "基本单位",
|
||||
"base_amount": "基本量",
|
||||
"Property": "属性",
|
||||
"Property_Editor": "属性编辑器",
|
||||
"imperial_fluid_ounce": "英制液体盎司【imp fl oz】(英制,体积)",
|
||||
"kg": "千克【kg】(公制,重量)",
|
||||
"ounce": "盎司【oz】(重量)",
|
||||
"ml": "毫升【ml】(公制,体积)",
|
||||
"fluid_ounce": "液体盎司【fl oz】(美制,体积)",
|
||||
"pint": "品脱 【pt】(美制,体积)",
|
||||
"quart": "夸脱【qt】(美制,体积)",
|
||||
"Name_Replace": "名称替换",
|
||||
"FDC_ID": "FDC ID",
|
||||
"err_importing_recipe": "导入菜谱时出错!",
|
||||
"open_data_help_text": "Tandoor开放数据项目为Tandoor提供社区贡献的数据。该字段在导入时会自动填充,并可以之后更新。",
|
||||
"Open_Data_Slug": "开放数据标识",
|
||||
"Properties_Food_Amount": "食物数量属性",
|
||||
"Properties_Food_Unit": "食品单位属性",
|
||||
"CustomTheme": "自定义主题",
|
||||
"CustomThemeHelp": "通过上传自定义 CSS 文件覆盖所选主题的样式。",
|
||||
"CustomImageHelp": "上传图片以在空间概览中显示。",
|
||||
"CustomNavLogoHelp": "上传图像以用作导航栏徽标。",
|
||||
"CustomLogoHelp": "上传不同尺寸的方形图像以更改为浏览器选项卡和安装的网络应用程序中的徽标。",
|
||||
"CustomLogos": "自定义徽标"
|
||||
}
|
||||
|
491
vue/src/stores/ShoppingListStore.js
Normal file
491
vue/src/stores/ShoppingListStore.js
Normal file
@ -0,0 +1,491 @@
|
||||
import {ApiApiFactory} from "@/utils/openapi/api"
|
||||
import {StandardToasts} from "@/utils/utils"
|
||||
import {defineStore} from "pinia"
|
||||
import Vue from "vue"
|
||||
import _ from 'lodash';
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
||||
import moment from "moment/moment";
|
||||
|
||||
const _STORE_ID = "shopping_list_store"
|
||||
/*
|
||||
* test store to play around with pinia and see if it can work for my use cases
|
||||
* don't trust that all shopping list entries are in store as there is no cache validation logic, its just a shared data holder
|
||||
* */
|
||||
export const useShoppingListStore = defineStore(_STORE_ID, {
|
||||
state: () => ({
|
||||
// shopping data
|
||||
entries: {},
|
||||
supermarket_categories: [],
|
||||
supermarkets: [],
|
||||
|
||||
total_unchecked: 0,
|
||||
total_checked: 0,
|
||||
total_unchecked_food: 0,
|
||||
total_checked_food: 0,
|
||||
|
||||
// internal
|
||||
currently_updating: false,
|
||||
last_autosync: null,
|
||||
autosync_has_focus: true,
|
||||
autosync_timeout_id: null,
|
||||
undo_stack: [],
|
||||
|
||||
queue_timeout_id: undefined,
|
||||
item_check_sync_queue: {},
|
||||
|
||||
// constants
|
||||
GROUP_CATEGORY: 'food.supermarket_category.name',
|
||||
GROUP_CREATED_BY: 'created_by.display_name',
|
||||
GROUP_RECIPE: 'recipe_mealplan.recipe_name',
|
||||
|
||||
UNDEFINED_CATEGORY: 'shopping_undefined_category'
|
||||
}),
|
||||
getters: {
|
||||
/**
|
||||
* build a multi-level data structure ready for display from shopping list entries
|
||||
* group by selected grouping key
|
||||
* @return {{}}
|
||||
*/
|
||||
get_entries_by_group: function () {
|
||||
let structure = {}
|
||||
let ordered_structure = []
|
||||
|
||||
// build structure
|
||||
for (let i in this.entries) {
|
||||
structure = this.updateEntryInStructure(structure, this.entries[i], useUserPreferenceStore().device_settings.shopping_selected_grouping)
|
||||
}
|
||||
|
||||
// statistics for UI conditions and display
|
||||
let total_unchecked = 0
|
||||
let total_checked = 0
|
||||
let total_unchecked_food = 0
|
||||
let total_checked_food = 0
|
||||
for (let i in structure) {
|
||||
let count_unchecked = 0
|
||||
let count_checked = 0
|
||||
let count_unchecked_food = 0
|
||||
let count_checked_food = 0
|
||||
|
||||
for (let fi in structure[i]['foods']) {
|
||||
let food_checked = true
|
||||
for (let ei in structure[i]['foods'][fi]['entries']) {
|
||||
if (structure[i]['foods'][fi]['entries'][ei].checked) {
|
||||
count_checked++
|
||||
} else {
|
||||
food_checked = false
|
||||
count_unchecked++
|
||||
}
|
||||
}
|
||||
if (food_checked) {
|
||||
count_checked_food++
|
||||
} else {
|
||||
count_unchecked_food++
|
||||
}
|
||||
}
|
||||
|
||||
Vue.set(structure[i], 'count_unchecked', count_unchecked)
|
||||
Vue.set(structure[i], 'count_checked', count_checked)
|
||||
Vue.set(structure[i], 'count_unchecked_food', count_unchecked_food)
|
||||
Vue.set(structure[i], 'count_checked_food', count_checked_food)
|
||||
|
||||
total_unchecked += count_unchecked
|
||||
total_checked += count_checked
|
||||
total_unchecked_food += count_unchecked_food
|
||||
total_checked_food += count_checked_food
|
||||
}
|
||||
|
||||
this.total_unchecked = total_unchecked
|
||||
this.total_checked = total_checked
|
||||
this.total_unchecked_food = total_unchecked_food
|
||||
this.total_checked_food = total_checked_food
|
||||
|
||||
// ordering
|
||||
if (this.UNDEFINED_CATEGORY in structure) {
|
||||
ordered_structure.push(structure[this.UNDEFINED_CATEGORY])
|
||||
Vue.delete(structure, this.UNDEFINED_CATEGORY)
|
||||
}
|
||||
|
||||
if (useUserPreferenceStore().device_settings.shopping_selected_grouping === this.GROUP_CATEGORY && useUserPreferenceStore().device_settings.shopping_selected_supermarket !== null) {
|
||||
for (let c of useUserPreferenceStore().device_settings.shopping_selected_supermarket.category_to_supermarket) {
|
||||
if (c.category.name in structure) {
|
||||
ordered_structure.push(structure[c.category.name])
|
||||
Vue.delete(structure, c.category.name)
|
||||
}
|
||||
}
|
||||
if (!useUserPreferenceStore().device_settings.shopping_show_selected_supermarket_only) {
|
||||
for (let i in structure) {
|
||||
ordered_structure.push(structure[i])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i in structure) {
|
||||
ordered_structure.push(structure[i])
|
||||
}
|
||||
}
|
||||
|
||||
return ordered_structure
|
||||
},
|
||||
/**
|
||||
* flattened list of entries used for exporters
|
||||
* kinda uncool but works for now
|
||||
* @return {*[]}
|
||||
*/
|
||||
get_flat_entries: function () {
|
||||
let items = []
|
||||
for (let i in this.get_entries_by_group) {
|
||||
for (let f in this.get_entries_by_group[i]['foods']) {
|
||||
for (let e in this.get_entries_by_group[i]['foods'][f]['entries']) {
|
||||
items.push({
|
||||
amount: this.get_entries_by_group[i]['foods'][f]['entries'][e].amount,
|
||||
unit: this.get_entries_by_group[i]['foods'][f]['entries'][e].unit?.name ?? '',
|
||||
food: this.get_entries_by_group[i]['foods'][f]['entries'][e].food?.name ?? '',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return items
|
||||
},
|
||||
/**
|
||||
* list of options available for grouping entry display
|
||||
* @return {[{id: *, translatable_label: string},{id: *, translatable_label: string},{id: *, translatable_label: string}]}
|
||||
*/
|
||||
grouping_options: function () {
|
||||
return [
|
||||
{'id': this.GROUP_CATEGORY, 'translatable_label': 'Category'},
|
||||
{'id': this.GROUP_CREATED_BY, 'translatable_label': 'created_by'},
|
||||
{'id': this.GROUP_RECIPE, 'translatable_label': 'Recipe'}
|
||||
]
|
||||
},
|
||||
/**
|
||||
* checks if failed items are contained in the sync queue
|
||||
*/
|
||||
has_failed_items: function () {
|
||||
for (let i in this.item_check_sync_queue) {
|
||||
if (this.item_check_sync_queue[i]['status'] === 'syncing_failed_before' || this.item_check_sync_queue[i]['status'] === 'waiting_failed_before') {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
/**
|
||||
* Retrieves all shopping related data (shopping list entries, supermarkets, supermarket categories and shopping list recipes) from API
|
||||
*/
|
||||
refreshFromAPI() {
|
||||
if (!this.currently_updating) {
|
||||
this.currently_updating = true
|
||||
this.last_autosync = new Date().getTime();
|
||||
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.listShoppingListEntrys().then((r) => {
|
||||
this.entries = {}
|
||||
|
||||
r.data.forEach((e) => {
|
||||
Vue.set(this.entries, e.id, e)
|
||||
})
|
||||
this.currently_updating = false
|
||||
}).catch((err) => {
|
||||
this.currently_updating = false
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
|
||||
})
|
||||
|
||||
apiClient.listSupermarketCategorys().then(r => {
|
||||
this.supermarket_categories = r.data
|
||||
}).catch((err) => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
|
||||
})
|
||||
|
||||
apiClient.listSupermarkets().then(r => {
|
||||
this.supermarkets = r.data
|
||||
}).catch((err) => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
|
||||
})
|
||||
}
|
||||
},
|
||||
/**
|
||||
* perform auto sync request to special endpoint returning only entries changed since last auto sync
|
||||
* only updates local entries that are older than the server version
|
||||
*/
|
||||
autosync() {
|
||||
if (!this.currently_updating && this.autosync_has_focus) {
|
||||
console.log('running autosync')
|
||||
|
||||
this.currently_updating = true
|
||||
|
||||
let previous_autosync = this.last_autosync
|
||||
this.last_autosync = new Date().getTime();
|
||||
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.listShoppingListEntrys(undefined, undefined, undefined, {
|
||||
'query': {'last_autosync': previous_autosync}
|
||||
}).then((r) => {
|
||||
r.data.forEach((e) => {
|
||||
// dont update stale client data
|
||||
if (!(Object.keys(this.entries).includes(e.id.toString())) || Date.parse(this.entries[e.id].updated_at) < Date.parse(e.updated_at)) {
|
||||
console.log('auto sync updating entry ', e)
|
||||
Vue.set(this.entries, e.id, e)
|
||||
}
|
||||
})
|
||||
this.currently_updating = false
|
||||
}).catch((err) => {
|
||||
console.warn('auto sync failed')
|
||||
this.currently_updating = false
|
||||
})
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Create a new shopping list entry
|
||||
* adds new entry to store
|
||||
* @param object entry object to create
|
||||
* @return {Promise<T | void>} promise of creation call to subscribe to
|
||||
*/
|
||||
createObject(object) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
return apiClient.createShoppingListEntry(object).then((r) => {
|
||||
Vue.set(this.entries, r.data.id, r.data)
|
||||
this.registerChange('CREATED', {[r.data.id]: r.data},)
|
||||
}).catch((err) => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
|
||||
})
|
||||
},
|
||||
/**
|
||||
* update existing entry object and updated_at timestamp
|
||||
* updates data in store
|
||||
* IMPORTANT: always use this method to update objects to keep client state consistent
|
||||
* @param object entry object to update
|
||||
* @return {Promise<T | void>} promise of updating call to subscribe to
|
||||
*/
|
||||
updateObject(object) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
// sets the update_at timestamp on the client to prevent auto sync from overriding with older changes
|
||||
// moment().format() yields locale aware datetime without ms 2024-01-04T13:39:08.607238+01:00
|
||||
Vue.set(object, 'updated_at', moment().format())
|
||||
|
||||
return apiClient.updateShoppingListEntry(object.id, object).then((r) => {
|
||||
Vue.set(this.entries, r.data.id, r.data)
|
||||
}).catch((err) => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
|
||||
})
|
||||
},
|
||||
/**
|
||||
* delete shopping list entry object from DB and store
|
||||
* @param object entry object to delete
|
||||
* @return {Promise<T | void>} promise of delete call to subscribe to
|
||||
*/
|
||||
deleteObject(object) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
return apiClient.destroyShoppingListEntry(object.id).then((r) => {
|
||||
Vue.delete(this.entries, object.id)
|
||||
}).catch((err) => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_DELETE, err)
|
||||
})
|
||||
},
|
||||
/**
|
||||
* returns a distinct list of recipes associated with unchecked shopping list entries
|
||||
*/
|
||||
getAssociatedRecipes: function () {
|
||||
let recipes = {}
|
||||
|
||||
for (let i in this.entries) {
|
||||
let e = this.entries[i]
|
||||
if (e.recipe_mealplan !== null) {
|
||||
Vue.set(recipes, e.recipe_mealplan.recipe, {
|
||||
'shopping_list_recipe_id': e.list_recipe,
|
||||
'recipe_id': e.recipe_mealplan.recipe,
|
||||
'recipe_name': e.recipe_mealplan.recipe_name,
|
||||
'servings': e.recipe_mealplan.servings,
|
||||
'mealplan_from_date': e.recipe_mealplan.mealplan_from_date,
|
||||
'mealplan_type': e.recipe_mealplan.mealplan_type,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return recipes
|
||||
},
|
||||
// convenience methods
|
||||
/**
|
||||
* function to set entry to its proper place in the data structure to perform grouping
|
||||
* @param {{}} structure datastructure
|
||||
* @param {*} entry entry to place
|
||||
* @param {*} group group to place entry into (must be of ShoppingListStore.GROUP_XXX/dot notation of entry property)
|
||||
* @returns {{}} datastructure including entry
|
||||
*/
|
||||
updateEntryInStructure(structure, entry, group) {
|
||||
let grouping_key = _.get(entry, group, this.UNDEFINED_CATEGORY)
|
||||
|
||||
if (grouping_key === undefined || grouping_key === null) {
|
||||
grouping_key = this.UNDEFINED_CATEGORY
|
||||
}
|
||||
|
||||
if (!(grouping_key in structure)) {
|
||||
Vue.set(structure, grouping_key, {'name': grouping_key, 'foods': {}})
|
||||
}
|
||||
if (!(entry.food.id in structure[grouping_key]['foods'])) {
|
||||
Vue.set(structure[grouping_key]['foods'], entry.food.id, {
|
||||
'id': entry.food.id,
|
||||
'name': entry.food.name,
|
||||
'entries': {}
|
||||
})
|
||||
}
|
||||
Vue.set(structure[grouping_key]['foods'][entry.food.id]['entries'], entry.id, entry)
|
||||
return structure
|
||||
},
|
||||
/**
|
||||
* function to handle user checking or unchecking a set of entries
|
||||
* @param {{}} entries set of entries
|
||||
* @param checked boolean to set checked state of entry to
|
||||
* @param undo if the user should be able to undo the change or not
|
||||
*/
|
||||
setEntriesCheckedState(entries, checked, undo) {
|
||||
if (undo) {
|
||||
this.registerChange((checked ? 'CHECKED' : 'UNCHECKED'), entries)
|
||||
}
|
||||
|
||||
let entry_id_list = []
|
||||
for (let i in entries) {
|
||||
Vue.set(this.entries[i], 'checked', checked)
|
||||
Vue.set(this.entries[i], 'updated_at', moment().format())
|
||||
entry_id_list.push(i)
|
||||
}
|
||||
|
||||
this.item_check_sync_queue
|
||||
Vue.set(this.item_check_sync_queue, Math.random(), {
|
||||
'ids': entry_id_list,
|
||||
'checked': checked,
|
||||
'status': 'waiting'
|
||||
})
|
||||
this.runSyncQueue(5)
|
||||
},
|
||||
/**
|
||||
* go through the list of queued requests and try to run them
|
||||
* add request back to queue if it fails due to offline or timeout
|
||||
* Do NOT call this method directly, always call using runSyncQueue method to prevent simultaneous runs
|
||||
* @private
|
||||
*/
|
||||
_replaySyncQueue() {
|
||||
if (navigator.onLine || document.location.href.includes('localhost')) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
let promises = []
|
||||
|
||||
for (let i in this.item_check_sync_queue) {
|
||||
let entry = this.item_check_sync_queue[i]
|
||||
Vue.set(entry, 'status', ((entry['status'] === 'waiting') ? 'syncing' : 'syncing_failed_before'))
|
||||
Vue.set(this.item_check_sync_queue, i, entry)
|
||||
|
||||
let p = apiClient.bulkShoppingListEntry(entry, {timeout: 15000}).then((r) => {
|
||||
Vue.delete(this.item_check_sync_queue, i)
|
||||
}).catch((err) => {
|
||||
if (err.code === "ERR_NETWORK" || err.code === "ECONNABORTED") {
|
||||
Vue.set(entry, 'status', 'waiting_failed_before')
|
||||
Vue.set(this.item_check_sync_queue, i, entry)
|
||||
} else {
|
||||
Vue.delete(this.item_check_sync_queue, i)
|
||||
console.error('Failed API call for entry ', entry)
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
|
||||
}
|
||||
})
|
||||
promises.push(p)
|
||||
}
|
||||
|
||||
Promise.allSettled(promises).finally(r => {
|
||||
this.runSyncQueue(500)
|
||||
})
|
||||
} else {
|
||||
this.runSyncQueue(5000)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* manages running the replaySyncQueue function after the given timeout
|
||||
* calling this function might cancel a previously created timeout
|
||||
* @param timeout time in ms after which to run the replaySyncQueue function
|
||||
*/
|
||||
runSyncQueue(timeout) {
|
||||
clearTimeout(this.queue_timeout_id)
|
||||
|
||||
this.queue_timeout_id = setTimeout(() => {
|
||||
this._replaySyncQueue()
|
||||
}, timeout)
|
||||
},
|
||||
/**
|
||||
* function to handle user "delaying" and "undelaying" shopping entries
|
||||
* @param {{}} entries set of entries
|
||||
* @param delay if entries should be delayed or if delay should be removed
|
||||
* @param undo if the user should be able to undo the change or not
|
||||
*/
|
||||
delayEntries(entries, delay, undo) {
|
||||
let delay_hours = useUserPreferenceStore().user_settings.default_delay
|
||||
let delay_date = new Date(Date.now() + delay_hours * (60 * 60 * 1000))
|
||||
|
||||
if (undo) {
|
||||
this.registerChange((delay ? 'DELAY' : 'UNDELAY'), entries)
|
||||
}
|
||||
|
||||
for (let i in entries) {
|
||||
this.entries[i].delay_until = (delay ? delay_date : null)
|
||||
this.updateObject(this.entries[i])
|
||||
}
|
||||
},
|
||||
/**
|
||||
* delete list of entries
|
||||
* @param {{}} entries set of entries
|
||||
*/
|
||||
deleteEntries(entries) {
|
||||
for (let i in entries) {
|
||||
this.deleteObject(this.entries[i])
|
||||
}
|
||||
},
|
||||
deleteShoppingListRecipe(shopping_list_recipe_id) {
|
||||
let api = new ApiApiFactory()
|
||||
|
||||
for (let i in this.entries) {
|
||||
if (this.entries[i].list_recipe === shopping_list_recipe_id) {
|
||||
Vue.delete(this.entries, i)
|
||||
}
|
||||
}
|
||||
|
||||
api.destroyShoppingListRecipe(shopping_list_recipe_id).then((x) => {
|
||||
// no need to update anything, entries were already removed
|
||||
}).catch((err) => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_DELETE, err)
|
||||
})
|
||||
},
|
||||
/**
|
||||
* register the change to a set of entries to allow undoing it
|
||||
* throws an Error if the operation type is not known
|
||||
* @param type the type of change to register. This determines what undoing the change does. (CREATE->delete object,
|
||||
* CHECKED->uncheck entry, UNCHECKED->check entry, DELAY->remove delay)
|
||||
* @param {{}} entries set of entries
|
||||
*/
|
||||
registerChange(type, entries) {
|
||||
if (!['CREATED', 'CHECKED', 'UNCHECKED', 'DELAY', 'UNDELAY'].includes(type)) {
|
||||
throw Error('Tried to register unknown change type')
|
||||
}
|
||||
this.undo_stack.push({'type': type, 'entries': entries})
|
||||
},
|
||||
/**
|
||||
* takes the last item from the undo stack and reverts it
|
||||
*/
|
||||
undoChange() {
|
||||
let last_item = this.undo_stack.pop()
|
||||
if (last_item !== undefined) {
|
||||
let type = last_item['type']
|
||||
let entries = last_item['entries']
|
||||
|
||||
if (type === 'CHECKED' || type === 'UNCHECKED') {
|
||||
this.setEntriesCheckedState(entries, (type === 'UNCHECKED'), false)
|
||||
} else if (type === 'DELAY' || type === 'UNDELAY') {
|
||||
this.delayEntries(entries, (type === 'UNDELAY'), false)
|
||||
} else if (type === 'CREATED') {
|
||||
for (let i in entries) {
|
||||
let e = entries[i]
|
||||
this.deleteObject(e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// can use localization in store
|
||||
//StandardToasts.makeStandardToast(this, this.$t('NoMoreUndo'))
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
@ -1,20 +1,136 @@
|
||||
import {defineStore} from 'pinia'
|
||||
|
||||
|
||||
import {ApiApiFactory} from "@/utils/openapi/api";
|
||||
import {ApiApiFactory, UserPreference} from "@/utils/openapi/api";
|
||||
import Vue from "vue";
|
||||
import {StandardToasts} from "@/utils/utils";
|
||||
|
||||
const _STALE_TIME_IN_MS = 1000 * 30
|
||||
const _STORE_ID = 'user_preference_store'
|
||||
|
||||
const _LS_DEVICE_SETTINGS = 'TANDOOR_LOCAL_SETTINGS'
|
||||
const _LS_USER_SETTINGS = 'TANDOOR_USER_SETTINGS'
|
||||
const _USER_ID = localStorage.getItem('USER_ID')
|
||||
|
||||
export const useUserPreferenceStore = defineStore(_STORE_ID, {
|
||||
state: () => ({
|
||||
data: null,
|
||||
updated_at: null,
|
||||
currently_updating: false,
|
||||
}),
|
||||
getters: {
|
||||
|
||||
},
|
||||
user_settings_loaded_at: new Date(0),
|
||||
user_settings: {
|
||||
image: null,
|
||||
theme: "TANDOOR",
|
||||
nav_bg_color: "#ddbf86",
|
||||
nav_text_color: "DARK",
|
||||
nav_show_logo: true,
|
||||
default_unit: "g",
|
||||
default_page: "SEARCH",
|
||||
use_fractions: false,
|
||||
use_kj: false,
|
||||
plan_share: [],
|
||||
nav_sticky: true,
|
||||
ingredient_decimals: 2,
|
||||
comments: true,
|
||||
shopping_auto_sync: 5,
|
||||
mealplan_autoadd_shopping: false,
|
||||
food_inherit_default: [],
|
||||
default_delay: "4.0000",
|
||||
mealplan_autoinclude_related: true,
|
||||
mealplan_autoexclude_onhand: true,
|
||||
shopping_share: [],
|
||||
shopping_recent_days: 7,
|
||||
csv_delim: ",",
|
||||
csv_prefix: "",
|
||||
filter_to_supermarket: false,
|
||||
shopping_add_onhand: false,
|
||||
left_handed: false,
|
||||
show_step_ingredients: true,
|
||||
food_children_exist: false,
|
||||
locally_updated_at: new Date(0),
|
||||
},
|
||||
|
||||
device_settings_initialized: false,
|
||||
device_settings_loaded_at: new Date(0),
|
||||
device_settings: {
|
||||
// shopping
|
||||
shopping_show_checked_entries: false,
|
||||
shopping_show_delayed_entries: false,
|
||||
shopping_show_selected_supermarket_only: false,
|
||||
shopping_selected_grouping: 'food.supermarket_category.name',
|
||||
shopping_selected_supermarket: null,
|
||||
shopping_item_info_created_by: false,
|
||||
shopping_item_info_mealplan: false,
|
||||
shopping_item_info_recipe: true,
|
||||
},
|
||||
}),
|
||||
getters: {},
|
||||
actions: {
|
||||
// Device settings (on device settings stored in local storage)
|
||||
/**
|
||||
* Load device settings from local storage and update state device_settings
|
||||
*/
|
||||
loadDeviceSettings() {
|
||||
let s = localStorage.getItem(_LS_DEVICE_SETTINGS)
|
||||
if (!(s === null || s === {})) {
|
||||
let settings = JSON.parse(s)
|
||||
for (s in settings) {
|
||||
Vue.set(this.device_settings, s, settings[s])
|
||||
}
|
||||
}
|
||||
this.device_settings_initialized = true
|
||||
},
|
||||
/**
|
||||
* persist changes to device settings into local storage
|
||||
*/
|
||||
updateDeviceSettings: function () {
|
||||
localStorage.setItem(_LS_DEVICE_SETTINGS, JSON.stringify(this.device_settings))
|
||||
},
|
||||
// ---------------- new methods for user settings
|
||||
loadUserSettings: function (allow_cached_results) {
|
||||
let s = localStorage.getItem(_LS_USER_SETTINGS)
|
||||
if (!(s === null || s === {})) {
|
||||
let settings = JSON.parse(s)
|
||||
for (s in settings) {
|
||||
Vue.set(this.user_settings, s, settings[s])
|
||||
}
|
||||
console.log(`loaded local user settings age ${((new Date().getTime()) - this.user_settings.locally_updated_at) / 1000} `)
|
||||
}
|
||||
if (((new Date().getTime()) - this.user_settings.locally_updated_at) > _STALE_TIME_IN_MS || !allow_cached_results) {
|
||||
console.log('refreshing user settings from API')
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.retrieveUserPreference(localStorage.getItem('USER_ID')).then(r => {
|
||||
for (s in r.data) {
|
||||
if (!(s in this.user_settings) && s !== 'user') {
|
||||
// dont load new keys if no default exists (to prevent forgetting to add defaults)
|
||||
console.error(`API returned UserPreference key "${s}" which has no default in UserPreferenceStore.user_settings.`)
|
||||
} else {
|
||||
Vue.set(this.user_settings, s, r.data[s])
|
||||
}
|
||||
}
|
||||
Vue.set(this.user_settings, 'locally_updated_at', new Date().getTime())
|
||||
localStorage.setItem(_LS_USER_SETTINGS, JSON.stringify(this.user_settings))
|
||||
}).catch(err => {
|
||||
this.currently_updating = false
|
||||
})
|
||||
}
|
||||
|
||||
},
|
||||
updateUserSettings: function () {
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.partialUpdateUserPreference(_USER_ID, this.user_settings).then(r => {
|
||||
this.user_settings = r.data
|
||||
Vue.set(this.user_settings, 'locally_updated_at', new Date().getTime())
|
||||
localStorage.setItem(_LS_USER_SETTINGS, JSON.stringify(this.user_settings))
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
|
||||
}).catch(err => {
|
||||
this.currently_updating = false
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
|
||||
})
|
||||
},
|
||||
// ----------------
|
||||
// User Preferences (database settings stored in user preference model)
|
||||
/**
|
||||
* gets data from the store either directly or refreshes from API if data is considered stale
|
||||
* @returns {UserPreference|*|Promise<axios.AxiosResponse<UserPreference>>}
|
||||
@ -69,12 +185,16 @@ export const useUserPreferenceStore = defineStore(_STORE_ID, {
|
||||
*/
|
||||
refreshFromAPI() {
|
||||
let apiClient = new ApiApiFactory()
|
||||
if(!this.currently_updating){
|
||||
if (!this.currently_updating) {
|
||||
this.currently_updating = true
|
||||
return apiClient.retrieveUserPreference(localStorage.getItem('USER_ID')).then(r => {
|
||||
this.data = r.data
|
||||
this.updated_at = new Date()
|
||||
this.currently_updating = false
|
||||
|
||||
this.user_settings = r.data
|
||||
this.user_settings_loaded_at = new Date()
|
||||
|
||||
return this.data
|
||||
}).catch(err => {
|
||||
this.currently_updating = false
|
||||
|
@ -408,6 +408,7 @@ export class Models {
|
||||
static SHOPPING_CATEGORY = {
|
||||
name: "Shopping_Category",
|
||||
apiName: "SupermarketCategory",
|
||||
merge: true,
|
||||
create: {
|
||||
params: [["name", "description"]],
|
||||
form: {
|
||||
|
@ -4013,18 +4013,6 @@ export interface ShoppingListEntries {
|
||||
* @memberof ShoppingListEntries
|
||||
*/
|
||||
unit?: FoodPropertiesFoodUnit | null;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof ShoppingListEntries
|
||||
*/
|
||||
ingredient?: number | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ShoppingListEntries
|
||||
*/
|
||||
ingredient_note?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@ -4061,6 +4049,12 @@ export interface ShoppingListEntries {
|
||||
* @memberof ShoppingListEntries
|
||||
*/
|
||||
created_at?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ShoppingListEntries
|
||||
*/
|
||||
updated_at?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@ -4104,18 +4098,6 @@ export interface ShoppingListEntry {
|
||||
* @memberof ShoppingListEntry
|
||||
*/
|
||||
unit?: FoodPropertiesFoodUnit | null;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof ShoppingListEntry
|
||||
*/
|
||||
ingredient?: number | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ShoppingListEntry
|
||||
*/
|
||||
ingredient_note?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@ -4152,6 +4134,12 @@ export interface ShoppingListEntry {
|
||||
* @memberof ShoppingListEntry
|
||||
*/
|
||||
created_at?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ShoppingListEntry
|
||||
*/
|
||||
updated_at?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@ -4165,6 +4153,25 @@ export interface ShoppingListEntry {
|
||||
*/
|
||||
delay_until?: string | null;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ShoppingListEntryBulk
|
||||
*/
|
||||
export interface ShoppingListEntryBulk {
|
||||
/**
|
||||
*
|
||||
* @type {Array<any>}
|
||||
* @memberof ShoppingListEntryBulk
|
||||
*/
|
||||
ids: Array<any>;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof ShoppingListEntryBulk
|
||||
*/
|
||||
checked: boolean;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@ -4213,6 +4220,18 @@ export interface ShoppingListRecipe {
|
||||
* @memberof ShoppingListRecipe
|
||||
*/
|
||||
mealplan_note?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ShoppingListRecipe
|
||||
*/
|
||||
mealplan_from_date?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ShoppingListRecipe
|
||||
*/
|
||||
mealplan_type?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
@ -4262,6 +4281,18 @@ export interface ShoppingListRecipeMealplan {
|
||||
* @memberof ShoppingListRecipeMealplan
|
||||
*/
|
||||
mealplan_note?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ShoppingListRecipeMealplan
|
||||
*/
|
||||
mealplan_from_date?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ShoppingListRecipeMealplan
|
||||
*/
|
||||
mealplan_type?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
@ -4311,6 +4342,18 @@ export interface ShoppingListRecipes {
|
||||
* @memberof ShoppingListRecipes
|
||||
*/
|
||||
mealplan_note?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ShoppingListRecipes
|
||||
*/
|
||||
mealplan_from_date?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ShoppingListRecipes
|
||||
*/
|
||||
mealplan_type?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
@ -4507,19 +4550,97 @@ export interface Space {
|
||||
* @memberof Space
|
||||
*/
|
||||
nav_logo?: RecipeFile | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Space
|
||||
*/
|
||||
space_theme?: SpaceSpaceThemeEnum;
|
||||
/**
|
||||
*
|
||||
* @type {RecipeFile}
|
||||
* @memberof Space
|
||||
*/
|
||||
space_theme?: RecipeFile | null;
|
||||
custom_space_theme?: RecipeFile | null;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @type {string}
|
||||
* @memberof Space
|
||||
*/
|
||||
use_plural?: boolean;
|
||||
nav_bg_color?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Space
|
||||
*/
|
||||
nav_text_color?: SpaceNavTextColorEnum;
|
||||
/**
|
||||
*
|
||||
* @type {RecipeFile}
|
||||
* @memberof Space
|
||||
*/
|
||||
logo_color_32?: RecipeFile | null;
|
||||
/**
|
||||
*
|
||||
* @type {RecipeFile}
|
||||
* @memberof Space
|
||||
*/
|
||||
logo_color_128?: RecipeFile | null;
|
||||
/**
|
||||
*
|
||||
* @type {RecipeFile}
|
||||
* @memberof Space
|
||||
*/
|
||||
logo_color_144?: RecipeFile | null;
|
||||
/**
|
||||
*
|
||||
* @type {RecipeFile}
|
||||
* @memberof Space
|
||||
*/
|
||||
logo_color_180?: RecipeFile | null;
|
||||
/**
|
||||
*
|
||||
* @type {RecipeFile}
|
||||
* @memberof Space
|
||||
*/
|
||||
logo_color_192?: RecipeFile | null;
|
||||
/**
|
||||
*
|
||||
* @type {RecipeFile}
|
||||
* @memberof Space
|
||||
*/
|
||||
logo_color_512?: RecipeFile | null;
|
||||
/**
|
||||
*
|
||||
* @type {RecipeFile}
|
||||
* @memberof Space
|
||||
*/
|
||||
logo_color_svg?: RecipeFile | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @export
|
||||
* @enum {string}
|
||||
*/
|
||||
export enum SpaceSpaceThemeEnum {
|
||||
Blank = 'BLANK',
|
||||
Tandoor = 'TANDOOR',
|
||||
Bootstrap = 'BOOTSTRAP',
|
||||
Darkly = 'DARKLY',
|
||||
Flatly = 'FLATLY',
|
||||
Superhero = 'SUPERHERO',
|
||||
TandoorDark = 'TANDOOR_DARK'
|
||||
}
|
||||
/**
|
||||
* @export
|
||||
* @enum {string}
|
||||
*/
|
||||
export enum SpaceNavTextColorEnum {
|
||||
Blank = 'BLANK',
|
||||
Light = 'LIGHT',
|
||||
Dark = 'DARK'
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@ -5382,6 +5503,39 @@ export interface ViewLog {
|
||||
*/
|
||||
export const ApiApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {ShoppingListEntryBulk} [shoppingListEntryBulk]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
bulkShoppingListEntry: async (shoppingListEntryBulk?: ShoppingListEntryBulk, options: any = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/api/shopping-list-entry/bulk/`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(shoppingListEntryBulk, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {AccessToken} [accessToken]
|
||||
@ -9018,6 +9172,13 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
|
||||
if (options.order_field !== undefined) {
|
||||
localVarQueryParameter['order_field'] = options.order_field;
|
||||
}
|
||||
|
||||
if (options.order_direction!== undefined) {
|
||||
localVarQueryParameter['order_direction'] = options.order_direction;
|
||||
}
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
@ -9486,10 +9647,11 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} [query] Query string matched against supermarket name.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listSupermarkets: async (options: any = {}): Promise<RequestArgs> => {
|
||||
listSupermarkets: async (query?: string, options: any = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/api/supermarket/`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
@ -9502,6 +9664,10 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
if (query !== undefined) {
|
||||
localVarQueryParameter['query'] = query;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
|
||||
@ -9661,10 +9827,11 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} [query] Query string matched against user-file name.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listUserFiles: async (options: any = {}): Promise<RequestArgs> => {
|
||||
listUserFiles: async (query?: string, options: any = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/api/user-file/`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
@ -9677,6 +9844,10 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
if (query !== undefined) {
|
||||
localVarQueryParameter['query'] = query;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
|
||||
@ -9935,6 +10106,47 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id A unique integer value identifying this supermarket category.
|
||||
* @param {string} target
|
||||
* @param {SupermarketCategory} [supermarketCategory]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
mergeSupermarketCategory: async (id: string, target: string, supermarketCategory?: SupermarketCategory, options: any = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('mergeSupermarketCategory', 'id', id)
|
||||
// verify required parameter 'target' is not null or undefined
|
||||
assertParamExists('mergeSupermarketCategory', 'target', target)
|
||||
const localVarPath = `/api/supermarket-category/{id}/merge/{target}/`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)))
|
||||
.replace(`{${"target"}}`, encodeURIComponent(String(target)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(supermarketCategory, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id A unique integer value identifying this unit.
|
||||
@ -10058,6 +10270,40 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id A unique integer value identifying the book.
|
||||
* @param {RecipeBook} [recipeBook]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
partialUpdateManualOrderBooks: async (id: string, recipeBook?: RecipeBook , options: any = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/api/recipe-book/{id}/`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(recipeBook , localVarRequestOptions, configuration)
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id A unique integer value identifying this access token.
|
||||
@ -14820,6 +15066,16 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
|
||||
export const ApiApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator = ApiApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {ShoppingListEntryBulk} [shoppingListEntryBulk]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async bulkShoppingListEntry(shoppingListEntryBulk?: ShoppingListEntryBulk, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ShoppingListEntryBulk>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.bulkShoppingListEntry(shoppingListEntryBulk, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {AccessToken} [accessToken]
|
||||
@ -16034,11 +16290,12 @@ export const ApiApiFp = function(configuration?: Configuration) {
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} [query] Query string matched against supermarket name.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listSupermarkets(options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<Supermarket>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listSupermarkets(options);
|
||||
async listSupermarkets(query?: string, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<Supermarket>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listSupermarkets(query, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
@ -16085,11 +16342,12 @@ export const ApiApiFp = function(configuration?: Configuration) {
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} [query] Query string matched against user-file name.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listUserFiles(options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<UserFile>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listUserFiles(options);
|
||||
async listUserFiles(query?: string, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<UserFile>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listUserFiles(query, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
@ -16165,6 +16423,18 @@ export const ApiApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.mergeKeyword(id, target, keyword, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id A unique integer value identifying this supermarket category.
|
||||
* @param {string} target
|
||||
* @param {SupermarketCategory} [supermarketCategory]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async mergeSupermarketCategory(id: string, target: string, supermarketCategory?: SupermarketCategory, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SupermarketCategory>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.mergeSupermarketCategory(id, target, supermarketCategory, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id A unique integer value identifying this unit.
|
||||
@ -16201,6 +16471,17 @@ export const ApiApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.moveKeyword(id, parent, keyword, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id A unique integer value identifying this supermarket category relation.
|
||||
* @param {RecipeBook} [recipeBook]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async partialUpdateManualOrderBooks(id: string, recipeBook?: RecipeBook, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SupermarketCategoryRelation>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.partialUpdateManualOrderBooks(id, recipeBook, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id A unique integer value identifying this access token.
|
||||
@ -17623,6 +17904,15 @@ export const ApiApiFp = function(configuration?: Configuration) {
|
||||
export const ApiApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||
const localVarFp = ApiApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {ShoppingListEntryBulk} [shoppingListEntryBulk]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
bulkShoppingListEntry(shoppingListEntryBulk?: ShoppingListEntryBulk, options?: any): AxiosPromise<ShoppingListEntryBulk> {
|
||||
return localVarFp.bulkShoppingListEntry(shoppingListEntryBulk, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {AccessToken} [accessToken]
|
||||
@ -18719,11 +19009,12 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} [query] Query string matched against supermarket name.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listSupermarkets(options?: any): AxiosPromise<Array<Supermarket>> {
|
||||
return localVarFp.listSupermarkets(options).then((request) => request(axios, basePath));
|
||||
listSupermarkets(query?: string, options?: any): AxiosPromise<Array<Supermarket>> {
|
||||
return localVarFp.listSupermarkets(query, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
@ -18765,11 +19056,12 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} [query] Query string matched against user-file name.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listUserFiles(options?: any): AxiosPromise<Array<UserFile>> {
|
||||
return localVarFp.listUserFiles(options).then((request) => request(axios, basePath));
|
||||
listUserFiles(query?: string, options?: any): AxiosPromise<Array<UserFile>> {
|
||||
return localVarFp.listUserFiles(query, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
@ -18837,6 +19129,17 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
|
||||
mergeKeyword(id: string, target: string, keyword?: Keyword, options?: any): AxiosPromise<Keyword> {
|
||||
return localVarFp.mergeKeyword(id, target, keyword, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id A unique integer value identifying this supermarket category.
|
||||
* @param {string} target
|
||||
* @param {SupermarketCategory} [supermarketCategory]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
mergeSupermarketCategory(id: string, target: string, supermarketCategory?: SupermarketCategory, options?: any): AxiosPromise<SupermarketCategory> {
|
||||
return localVarFp.mergeSupermarketCategory(id, target, supermarketCategory, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id A unique integer value identifying this unit.
|
||||
@ -18870,6 +19173,16 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
|
||||
moveKeyword(id: string, parent: string, keyword?: Keyword, options?: any): AxiosPromise<Keyword> {
|
||||
return localVarFp.moveKeyword(id, parent, keyword, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id A unique integer value identifying this supermarket category relation.
|
||||
* @param {RecipeBook} [recipeBook]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
partialUpdateManualOrderBooks(id: string, recipeBook?: RecipeBook, options?: any): AxiosPromise<SupermarketCategoryRelation> {
|
||||
return localVarFp.partialUpdateManualOrderBooks(id, recipeBook, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id A unique integer value identifying this access token.
|
||||
@ -20160,6 +20473,17 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
|
||||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class ApiApi extends BaseAPI {
|
||||
/**
|
||||
*
|
||||
* @param {ShoppingListEntryBulk} [shoppingListEntryBulk]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiApi
|
||||
*/
|
||||
public bulkShoppingListEntry(shoppingListEntryBulk?: ShoppingListEntryBulk, options?: any) {
|
||||
return ApiApiFp(this.configuration).bulkShoppingListEntry(shoppingListEntryBulk, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {AccessToken} [accessToken]
|
||||
@ -20445,7 +20769,6 @@ export class ApiApi extends BaseAPI {
|
||||
public createRecipeBookEntry(recipeBookEntry?: RecipeBookEntry, options?: any) {
|
||||
return ApiApiFp(this.configuration).createRecipeBookEntry(recipeBookEntry, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* function to retrieve a recipe from a given url or source string :param request: standard request with additional post parameters - url: url to use for importing recipe - data: if no url is given recipe is imported from provided source data - (optional) bookmarklet: id of bookmarklet import to use, overrides URL and data attributes :return: JsonResponse containing the parsed json and images
|
||||
* @param {any} [body]
|
||||
@ -21492,12 +21815,13 @@ export class ApiApi extends BaseAPI {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} [query] Query string matched against supermarket name.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiApi
|
||||
*/
|
||||
public listSupermarkets(options?: any) {
|
||||
return ApiApiFp(this.configuration).listSupermarkets(options).then((request) => request(this.axios, this.basePath));
|
||||
public listSupermarkets(query?: string, options?: any) {
|
||||
return ApiApiFp(this.configuration).listSupermarkets(query, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -21548,12 +21872,13 @@ export class ApiApi extends BaseAPI {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} [query] Query string matched against user-file name.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiApi
|
||||
*/
|
||||
public listUserFiles(options?: any) {
|
||||
return ApiApiFp(this.configuration).listUserFiles(options).then((request) => request(this.axios, this.basePath));
|
||||
public listUserFiles(query?: string, options?: any) {
|
||||
return ApiApiFp(this.configuration).listUserFiles(query, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -21636,6 +21961,19 @@ export class ApiApi extends BaseAPI {
|
||||
return ApiApiFp(this.configuration).mergeKeyword(id, target, keyword, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} id A unique integer value identifying this supermarket category.
|
||||
* @param {string} target
|
||||
* @param {SupermarketCategory} [supermarketCategory]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiApi
|
||||
*/
|
||||
public mergeSupermarketCategory(id: string, target: string, supermarketCategory?: SupermarketCategory, options?: any) {
|
||||
return ApiApiFp(this.configuration).mergeSupermarketCategory(id, target, supermarketCategory, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} id A unique integer value identifying this unit.
|
||||
@ -21674,7 +22012,16 @@ export class ApiApi extends BaseAPI {
|
||||
public moveKeyword(id: string, parent: string, keyword?: Keyword, options?: any) {
|
||||
return ApiApiFp(this.configuration).moveKeyword(id, parent, keyword, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} id A unique integer value identifying this supermarket category relation.
|
||||
* @param {RecipeBook} [recipeBook]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public partialUpdateManualOrderBooks(id: string, recipeBook?: RecipeBook, options?: any) {
|
||||
return ApiApiFp(this.configuration).partialUpdateManualOrderBooks(id, recipeBook, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {string} id A unique integer value identifying this access token.
|
||||
|
@ -131,7 +131,7 @@ export class StandardToasts {
|
||||
}
|
||||
|
||||
|
||||
let DEBUG = localStorage.getItem("DEBUG") === "True" || always_show_errors
|
||||
let DEBUG = (localStorage.getItem("DEBUG") === "True" || always_show_errors) && variant !== 'success'
|
||||
if (DEBUG){
|
||||
console.log('ERROR ', err, JSON.stringify(err?.response?.data))
|
||||
console.trace();
|
||||
@ -365,6 +365,23 @@ export function energyHeading() {
|
||||
}
|
||||
}
|
||||
|
||||
export const FormatMixin = {
|
||||
name: "FormatMixin",
|
||||
methods: {
|
||||
/**
|
||||
* format short date from datetime
|
||||
* @param datetime any string that can be parsed by Date.parse()
|
||||
* @return {string}
|
||||
*/
|
||||
formatDate: function (datetime) {
|
||||
return Intl.DateTimeFormat(window.navigator.language, {
|
||||
dateStyle: "short",
|
||||
}).format(Date.parse(datetime))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
axios.defaults.xsrfCookieName = "csrftoken"
|
||||
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
|
||||
|
||||
@ -376,7 +393,7 @@ export const ApiMixin = {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// if passing parameters that are not part of the offical schema of the endpoint use parameter: options: {query: {simple: 1}}
|
||||
// if passing parameters that are not part of the official schema of the endpoint use parameter: options: {query: {simple: 1}}
|
||||
genericAPI: function (model, action, options) {
|
||||
let setup = getConfig(model, action)
|
||||
if (setup?.config?.function) {
|
||||
|
194
vue/yarn.lock
194
vue/yarn.lock
@ -1198,7 +1198,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
|
||||
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
|
||||
|
||||
"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4":
|
||||
"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4":
|
||||
version "7.22.15"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.15.tgz#38f46494ccf6cf020bd4eed7124b425e83e523b8"
|
||||
integrity sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==
|
||||
@ -1341,17 +1341,17 @@
|
||||
"@codemirror/view" "^6.0.0"
|
||||
crelt "^1.0.5"
|
||||
|
||||
"@codemirror/state@^6.0.0", "@codemirror/state@^6.1.4", "@codemirror/state@^6.2.0", "@codemirror/state@^6.3.3":
|
||||
version "6.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.3.3.tgz#6a647c2fa62b68604187152de497e91aabf43f82"
|
||||
integrity sha512-0wufKcTw2dEwEaADajjHf6hBy1sh3M6V0e+q4JKIhLuiMSe5td5HOWpUdvKth1fT1M9VYOboajoBHpkCd7PG7A==
|
||||
"@codemirror/state@^6.0.0", "@codemirror/state@^6.2.0", "@codemirror/state@^6.3.3", "@codemirror/state@^6.4.0":
|
||||
version "6.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.4.0.tgz#8bc3e096c84360b34525a84696a84f86b305363a"
|
||||
integrity sha512-hm8XshYj5Fo30Bb922QX9hXB/bxOAVH+qaqHBzw5TKa72vOeslyGwd4X8M0c1dJ9JqxlaMceOQ8RsL9tC7gU0A==
|
||||
|
||||
"@codemirror/view@^6.0.0", "@codemirror/view@^6.17.0", "@codemirror/view@^6.22.2":
|
||||
version "6.22.2"
|
||||
resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.22.2.tgz#79a4b87f5bb3f057cb046295b102eb04fd31a50d"
|
||||
integrity sha512-cJp64cPXm7QfSBWEXK+76+hsZCGHupUgy8JAbSzMG6Lr0rfK73c1CaWITVW6hZVkOnAFxJTxd0PIuynNbzxYPw==
|
||||
"@codemirror/view@^6.0.0", "@codemirror/view@^6.17.0", "@codemirror/view@^6.23.1":
|
||||
version "6.23.1"
|
||||
resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.23.1.tgz#1ce3039a588d6b93f153b7c4c035c2075ede34a6"
|
||||
integrity sha512-J2Xnn5lFYT1ZN/5ewEoMBCmLlL71lZ3mBdb7cUEuHhX2ESoSrNEucpsDXpX22EuTGm9LOgC9v4Z0wx+Ez8QmGA==
|
||||
dependencies:
|
||||
"@codemirror/state" "^6.1.4"
|
||||
"@codemirror/state" "^6.4.0"
|
||||
style-mod "^4.1.0"
|
||||
w3c-keyname "^2.2.4"
|
||||
|
||||
@ -2046,11 +2046,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.8.tgz#f2a7de3c107b89b441e071d5472e6b726b4adf45"
|
||||
integrity sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==
|
||||
|
||||
"@types/raf@^3.4.0":
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/raf/-/raf-3.4.0.tgz#2b72cbd55405e071f1c4d29992638e022b20acc2"
|
||||
integrity sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw==
|
||||
|
||||
"@types/range-parser@*":
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
||||
@ -3410,12 +3405,12 @@ available-typed-arrays@^1.0.5:
|
||||
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
|
||||
integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
|
||||
|
||||
axios@^1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.0.tgz#f1e5292f26b2fd5c2e66876adc5b06cdbd7d2102"
|
||||
integrity sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==
|
||||
axios@^1.6.7:
|
||||
version "1.6.7"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.7.tgz#7b48c2e27c96f9c68a2f8f31e2ab19f59b06b0a7"
|
||||
integrity sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.0"
|
||||
follow-redirects "^1.15.4"
|
||||
form-data "^4.0.0"
|
||||
proxy-from-env "^1.1.0"
|
||||
|
||||
@ -3645,11 +3640,6 @@ balanced-match@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
base64-arraybuffer@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc"
|
||||
integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==
|
||||
|
||||
base64-js@^1.0.2, base64-js@^1.3.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||
@ -3922,11 +3912,6 @@ browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.3, browserslist@^4
|
||||
node-releases "^2.0.13"
|
||||
update-browserslist-db "^1.0.11"
|
||||
|
||||
btoa@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73"
|
||||
integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==
|
||||
|
||||
buffer-alloc-unsafe@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
|
||||
@ -4097,20 +4082,6 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001517, caniuse-lite@^1.0.30001520:
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001533.tgz#1180daeb2518b93c82f19b904d1fefcf82197707"
|
||||
integrity sha512-9aY/b05NKU4Yl2sbcJhn4A7MsGwR1EPfW/nrqsnqVA0Oq50wpmPaGI+R1Z0UKlUl96oxUkGEOILWtOHck0eCWw==
|
||||
|
||||
canvg@^3.0.6:
|
||||
version "3.0.10"
|
||||
resolved "https://registry.yarnpkg.com/canvg/-/canvg-3.0.10.tgz#8e52a2d088b6ffa23ac78970b2a9eebfae0ef4b3"
|
||||
integrity sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@types/raf" "^3.4.0"
|
||||
core-js "^3.8.3"
|
||||
raf "^3.4.1"
|
||||
regenerator-runtime "^0.13.7"
|
||||
rgbcolor "^1.0.1"
|
||||
stackblur-canvas "^2.0.0"
|
||||
svg-pathdata "^6.0.3"
|
||||
|
||||
case-sensitive-paths-webpack-plugin@^2.3.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4"
|
||||
@ -4586,7 +4557,7 @@ core-js@^2.4.0, core-js@^2.5.0:
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
|
||||
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
|
||||
|
||||
core-js@^3.29.1, core-js@^3.6.0, core-js@^3.7.0, core-js@^3.8.3:
|
||||
core-js@^3.29.1, core-js@^3.7.0, core-js@^3.8.3:
|
||||
version "3.32.2"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.32.2.tgz#172fb5949ef468f93b4be7841af6ab1f21992db7"
|
||||
integrity sha512-pxXSw1mYZPDGvTQqEc5vgIb83jGQKFGYWY76z4a7weZXUolw3G+OvpZqSRcfYOoOVUQJYEPsWeQK8pKEnUtWxQ==
|
||||
@ -4723,13 +4694,6 @@ css-declaration-sorter@^6.3.1:
|
||||
resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz#28beac7c20bad7f1775be3a7129d7eae409a3a71"
|
||||
integrity sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==
|
||||
|
||||
css-line-break@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0"
|
||||
integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==
|
||||
dependencies:
|
||||
utrie "^1.0.2"
|
||||
|
||||
css-loader@^6.5.0:
|
||||
version "6.8.1"
|
||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.8.1.tgz#0f8f52699f60f5e679eab4ec0fcd68b8e8a50a88"
|
||||
@ -5124,11 +5088,6 @@ domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1:
|
||||
dependencies:
|
||||
domelementtype "^2.2.0"
|
||||
|
||||
dompurify@^2.2.0:
|
||||
version "2.4.7"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.7.tgz#277adeb40a2c84be2d42a8bcd45f582bfa4d0cfc"
|
||||
integrity sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ==
|
||||
|
||||
domutils@^2.5.2, domutils@^2.8.0:
|
||||
version "2.8.0"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
|
||||
@ -5394,11 +5353,6 @@ es-to-primitive@^1.2.1:
|
||||
is-date-object "^1.0.1"
|
||||
is-symbol "^1.0.2"
|
||||
|
||||
es6-promise@^4.2.5:
|
||||
version "4.2.8"
|
||||
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
|
||||
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
|
||||
|
||||
escalade@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
|
||||
@ -5834,11 +5788,6 @@ fd-slicer@~1.1.0:
|
||||
dependencies:
|
||||
pend "~1.2.0"
|
||||
|
||||
fflate@^0.4.8:
|
||||
version "0.4.8"
|
||||
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae"
|
||||
integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==
|
||||
|
||||
figgy-pudding@^3.5.1:
|
||||
version "3.5.2"
|
||||
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
|
||||
@ -6048,10 +5997,10 @@ flush-write-stream@^1.0.0:
|
||||
inherits "^2.0.3"
|
||||
readable-stream "^2.3.6"
|
||||
|
||||
follow-redirects@^1.0.0, follow-redirects@^1.15.0:
|
||||
version "1.15.4"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
|
||||
integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
|
||||
follow-redirects@^1.0.0, follow-redirects@^1.15.4:
|
||||
version "1.15.5"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020"
|
||||
integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==
|
||||
|
||||
for-each@^0.3.3:
|
||||
version "0.3.3"
|
||||
@ -6666,23 +6615,6 @@ html-webpack-plugin@^5.1.0:
|
||||
pretty-error "^4.0.0"
|
||||
tapable "^2.0.0"
|
||||
|
||||
html2canvas@^1.0.0, html2canvas@^1.0.0-rc.5:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543"
|
||||
integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==
|
||||
dependencies:
|
||||
css-line-break "^2.1.0"
|
||||
text-segmentation "^1.0.3"
|
||||
|
||||
html2pdf.js@^0.10.1:
|
||||
version "0.10.1"
|
||||
resolved "https://registry.yarnpkg.com/html2pdf.js/-/html2pdf.js-0.10.1.tgz#9363910cca52a54113633e552a726722209a8eed"
|
||||
integrity sha512-3onwwhOWsZfNjIZwV6YIJ6FVhXk+X9YxHSqzeS6hup+1dGi2DHI+zZYUJ+iFnvtaYcjlhyrILL1fvRCUOa8Fcg==
|
||||
dependencies:
|
||||
es6-promise "^4.2.5"
|
||||
html2canvas "^1.0.0"
|
||||
jspdf "^2.3.1"
|
||||
|
||||
htmlparser2@^6.0.0, htmlparser2@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
|
||||
@ -7531,21 +7463,6 @@ jsonpointer@^5.0.0:
|
||||
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559"
|
||||
integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==
|
||||
|
||||
jspdf@^2.3.1:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/jspdf/-/jspdf-2.5.1.tgz#00c85250abf5447a05f3b32ab9935ab4a56592cc"
|
||||
integrity sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.0"
|
||||
atob "^2.1.2"
|
||||
btoa "^1.2.1"
|
||||
fflate "^0.4.8"
|
||||
optionalDependencies:
|
||||
canvg "^3.0.6"
|
||||
core-js "^3.6.0"
|
||||
dompurify "^2.2.0"
|
||||
html2canvas "^1.0.0-rc.5"
|
||||
|
||||
keyv@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373"
|
||||
@ -8844,11 +8761,6 @@ pend@~1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
|
||||
integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
|
||||
|
||||
performance-now@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
|
||||
integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
|
||||
|
||||
picocolors@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f"
|
||||
@ -8886,10 +8798,10 @@ pify@^4.0.1:
|
||||
resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
|
||||
integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
|
||||
|
||||
pinia@^2.0.30:
|
||||
version "2.1.6"
|
||||
resolved "https://registry.yarnpkg.com/pinia/-/pinia-2.1.6.tgz#e88959f14b61c4debd9c42d0c9944e2875cbe0fa"
|
||||
integrity sha512-bIU6QuE5qZviMmct5XwCesXelb5VavdOWKWaB17ggk++NUwQWWbP5YnsONTk3b752QkW9sACiR81rorpeOMSvQ==
|
||||
pinia@^2.1.7:
|
||||
version "2.1.7"
|
||||
resolved "https://registry.yarnpkg.com/pinia/-/pinia-2.1.7.tgz#4cf5420d9324ca00b7b4984d3fbf693222115bbc"
|
||||
integrity sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==
|
||||
dependencies:
|
||||
"@vue/devtools-api" "^6.5.0"
|
||||
vue-demi ">=0.14.5"
|
||||
@ -9419,13 +9331,6 @@ queue-microtask@^1.2.2:
|
||||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
||||
|
||||
raf@^3.4.1:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
|
||||
integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
|
||||
dependencies:
|
||||
performance-now "^2.1.0"
|
||||
|
||||
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
|
||||
@ -9560,11 +9465,6 @@ regenerator-runtime@^0.11.0:
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
|
||||
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
|
||||
|
||||
regenerator-runtime@^0.13.7:
|
||||
version "0.13.11"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
|
||||
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
|
||||
|
||||
regenerator-runtime@^0.14.0:
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45"
|
||||
@ -9723,11 +9623,6 @@ reusify@^1.0.4:
|
||||
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
||||
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
|
||||
|
||||
rgbcolor@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d"
|
||||
integrity sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==
|
||||
|
||||
rimraf@^2.5.4, rimraf@^2.6.3:
|
||||
version "2.7.1"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
||||
@ -10378,11 +10273,6 @@ stable@^0.1.8:
|
||||
resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
|
||||
integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
|
||||
|
||||
stackblur-canvas@^2.0.0:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/stackblur-canvas/-/stackblur-canvas-2.6.0.tgz#7876bab4ea99bfc97b69ce662614d7a1afb2d71b"
|
||||
integrity sha512-8S1aIA+UoF6erJYnglGPug6MaHYGo1Ot7h5fuXx4fUPvcvQfcdw2o/ppCse63+eZf8PPidSu4v1JnmEVtEDnpg==
|
||||
|
||||
stackframe@^1.3.4:
|
||||
version "1.3.4"
|
||||
resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310"
|
||||
@ -10666,11 +10556,6 @@ supports-preserve-symlinks-flag@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
|
||||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
||||
|
||||
svg-pathdata@^6.0.3:
|
||||
version "6.0.3"
|
||||
resolved "https://registry.yarnpkg.com/svg-pathdata/-/svg-pathdata-6.0.3.tgz#80b0e0283b652ccbafb69ad4f8f73e8d3fbf2cac"
|
||||
integrity sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==
|
||||
|
||||
svg-tags@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
|
||||
@ -10806,13 +10691,6 @@ terser@^5.0.0, terser@^5.10.0, terser@^5.16.8:
|
||||
commander "^2.20.0"
|
||||
source-map-support "~0.5.20"
|
||||
|
||||
text-segmentation@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943"
|
||||
integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==
|
||||
dependencies:
|
||||
utrie "^1.0.2"
|
||||
|
||||
text-table@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||
@ -11104,10 +10982,10 @@ typescript@~4.5.5:
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3"
|
||||
integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==
|
||||
|
||||
typescript@~5.1.6:
|
||||
version "5.1.6"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274"
|
||||
integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==
|
||||
typescript@~5.3.3:
|
||||
version "5.3.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
|
||||
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
|
||||
|
||||
unbox-primitive@^1.0.2:
|
||||
version "1.0.2"
|
||||
@ -11290,13 +11168,6 @@ utils-merge@1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
|
||||
|
||||
utrie@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645"
|
||||
integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==
|
||||
dependencies:
|
||||
base64-arraybuffer "^1.0.2"
|
||||
|
||||
uuid@^8.3.2:
|
||||
version "8.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||
@ -11606,10 +11477,10 @@ webpack-bundle-analyzer@^4.4.0:
|
||||
sirv "^2.0.3"
|
||||
ws "^7.3.1"
|
||||
|
||||
webpack-bundle-tracker@1.8.1:
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/webpack-bundle-tracker/-/webpack-bundle-tracker-1.8.1.tgz#d1cdbd62da622abe1243f099657af86a6ca2656d"
|
||||
integrity sha512-X1qtXG4ue92gjWQO2VhLVq8HDEf9GzUWE0OQyAQObVEZsFB1SUtSQ7o47agF5WZIaHfJUTKak4jEErU0gzoPcQ==
|
||||
webpack-bundle-tracker@3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webpack-bundle-tracker/-/webpack-bundle-tracker-3.0.1.tgz#dd4809cd22b231b296dfef5634353d875b1502f2"
|
||||
integrity sha512-q0/19A1gpP74oBC3rgveDBh09D1RGpLvREEOmen9eonTbcuhNAyLkfmfoQeOm+j4k26f+Q2mJSzEXoPu42gBFg==
|
||||
dependencies:
|
||||
lodash.assign "^4.2.0"
|
||||
lodash.defaults "^4.2.0"
|
||||
@ -11617,7 +11488,6 @@ webpack-bundle-tracker@1.8.1:
|
||||
lodash.frompairs "^4.0.1"
|
||||
lodash.get "^4.4.2"
|
||||
lodash.topairs "^4.3.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
webpack-chain@^6.5.1:
|
||||
version "6.5.1"
|
||||
|
Loading…
Reference in New Issue
Block a user