Merge branch 'develop' into CiPipelineImprovements

This commit is contained in:
Mikhail Epifanov 2024-01-29 09:49:39 +01:00 committed by GitHub
commit 1ad5f4843f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
49 changed files with 781 additions and 426 deletions

View File

@ -21,7 +21,7 @@ jobs:
suffix: ""
continue-on-error: false
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Get version number
id: get_version
@ -43,7 +43,7 @@ jobs:
path: ./recipes/plugins/open_data_plugin
# Build Vue frontend
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: yarn

View File

@ -21,7 +21,7 @@ jobs:
suffix: ""
continue-on-error: false
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Get version number
id: get_version
@ -35,7 +35,7 @@ jobs:
fi
# Build Vue frontend
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: yarn

View File

@ -21,7 +21,7 @@ jobs:
# Setup python & dependencies
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
@ -45,7 +45,7 @@ jobs:
# Build Vue frontend & Dependencies
- name: Set up Node ${{ matrix.node-version }}
if: steps.django_cache.outputs.cache-hit != 'true'
uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'yarn'

View File

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@ -25,7 +25,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
# Override language selection by uncommenting this and choosing your languages
with:
languages: python, javascript
@ -47,6 +47,6 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3
with:
languages: javascript, python

View File

@ -9,8 +9,8 @@ jobs:
if: github.repository_owner == 'TandoorRecipes'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: 3.x
- run: pip install mkdocs-material mkdocs-include-markdown-plugin

View File

@ -96,7 +96,7 @@ Share some information on how you use Tandoor to help me improve the application
Beginning with version 0.10.0 the code in this repository is licensed under the [GNU AGPL v3](https://www.gnu.org/licenses/agpl-3.0.de.html) license with a
[common clause](https://commonsclause.com/) selling exception. See [LICENSE.md](https://github.com/vabene1111/recipes/blob/develop/LICENSE.md) for details.
> NOTE: There appears to be a whole range of legal issues with licensing anything else then the standard completely open licenses.
> NOTE: There appears to be a whole range of legal issues with licensing anything other than the standard completely open licenses.
> I am in the process of getting some professional legal advice to sort out these issues.
> Please also see [Issue 238](https://github.com/vabene1111/recipes/issues/238) for some discussion and **reasoning** regarding the topic.

View File

@ -76,4 +76,4 @@ echo "Done"
chmod -R 755 /opt/recipes/mediafiles
exec gunicorn -b :$TANDOOR_PORT --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --access-logfile - --error-logfile - --log-level $GUNICORN_LOG_LEVEL recipes.wsgi
exec gunicorn -b "[::]:$TANDOOR_PORT" --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --access-logfile - --error-logfile - --log-level $GUNICORN_LOG_LEVEL recipes.wsgi

View File

@ -450,7 +450,7 @@ class SpacePreferenceForm(forms.ModelForm):
class Meta:
model = Space
fields = ('food_inherit', 'reset_food_inherit', 'use_plural')
fields = ('food_inherit', 'reset_food_inherit',)
help_texts = {
'food_inherit': _('Fields on food that should be inherited by default.'),

View File

@ -14,12 +14,14 @@ class IngredientObject(object):
unit = ""
food = ""
note = ""
numeric_amount = 0
def __init__(self, ingredient):
if ingredient.no_amount:
self.amount = ""
else:
self.amount = f"<scalable-number v-bind:number='{bleach.clean(str(ingredient.amount))}' v-bind:factor='ingredient_factor'></scalable-number>"
self.numeric_amount = float(ingredient.amount)
if ingredient.unit:
if ingredient.unit.plural_name in (None, ""):
self.unit = bleach.clean(str(ingredient.unit))
@ -83,9 +85,12 @@ def render_instructions(step): # TODO deduplicate markdown cleanup code
for i in step.ingredients.all():
ingredients.append(IngredientObject(i))
def scale(number):
return f"<scalable-number v-bind:number='{bleach.clean(str(number))}' v-bind:factor='ingredient_factor'></scalable-number>"
try:
template = Template(instructions)
instructions = template.render(ingredients=ingredients)
instructions = template.render(ingredients=ingredients, scale=scale)
except TemplateSyntaxError:
return _('Could not parse template code.') + ' Error: Template Syntax broken'
except UndefinedError:

View File

@ -4,11 +4,6 @@ from django.db import migrations, models
from django_scopes import scopes_disabled
def fix_fdc_ids(apps, schema_editor):
with scopes_disabled():
# in case any food had a non digit fdc ID before this migration, remove it
Food = apps.get_model('cookbook', 'Food')
Food.objects.exclude(fdc_id__regex=r'^\d+$').exclude(fdc_id=None).update(fdc_id=None)
class Migration(migrations.Migration):
@ -17,7 +12,6 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RunPython(fix_fdc_ids),
migrations.AddField(
model_name='propertytype',
name='fdc_id',

View File

@ -1,15 +1,23 @@
# Generated by Django 4.2.7 on 2023-11-29 19:44
from django.db import migrations, models
from django_scopes import scopes_disabled
def fix_fdc_ids(apps, schema_editor):
with scopes_disabled():
# in case any food had a non digit fdc ID before this migration, remove it
Food = apps.get_model('cookbook', 'Food')
Food.objects.exclude(fdc_id__regex=r'^\d+$').exclude(fdc_id=None).update(fdc_id=None)
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0204_propertytype_fdc_id'),
]
operations = [
migrations.RunPython(fix_fdc_ids),
migrations.AlterField(
model_name='food',
name='fdc_id',

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.7 on 2024-01-14 23:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0207_space_logo_color_128_space_logo_color_144_and_more'),
]
operations = [
migrations.AddField(
model_name='space',
name='app_name',
field=models.CharField(blank=True, max_length=40, null=True),
),
migrations.AddField(
model_name='userpreference',
name='max_owned_spaces',
field=models.IntegerField(default=100),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 4.2.7 on 2024-01-28 07:42
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0208_space_app_name_userpreference_max_owned_spaces'),
]
operations = [
migrations.RemoveField(
model_name='space',
name='use_plural',
),
]

View File

@ -24,7 +24,7 @@ from PIL import Image
from treebeard.mp_tree import MP_Node, MP_NodeManager
from recipes.settings import (COMMENT_PREF_DEFAULT, FRACTION_PREF_DEFAULT, KJ_PREF_DEFAULT,
SORT_TREE_BY_NAME, STICKY_NAV_PREF_DEFAULT)
SORT_TREE_BY_NAME, STICKY_NAV_PREF_DEFAULT, MAX_OWNED_SPACES_PREF_DEFAULT)
def get_user_display_name(self):
@ -288,7 +288,7 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
nav_logo = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_nav_logo')
nav_bg_color = models.CharField(max_length=8, default='', blank=True, )
nav_text_color = models.CharField(max_length=16, choices=NAV_TEXT_COLORS, default=BLANK)
app_name = models.CharField(max_length=40, null=True, blank=True, )
logo_color_32 = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_logo_color_32')
logo_color_128 = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_logo_color_128')
logo_color_144 = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_logo_color_144')
@ -303,7 +303,6 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
max_recipes = models.IntegerField(default=0)
max_file_storage_mb = models.IntegerField(default=0, help_text=_('Maximum file storage for space in MB. 0 for unlimited, -1 to disable file upload.'))
max_users = models.IntegerField(default=0)
use_plural = models.BooleanField(default=True)
allow_sharing = models.BooleanField(default=True)
no_sharing_limit = models.BooleanField(default=False)
demo = models.BooleanField(default=False)
@ -409,7 +408,7 @@ class UserPreference(models.Model, PermissionModelMixin):
nav_text_color = models.CharField(max_length=16, choices=NAV_TEXT_COLORS, default=DARK)
nav_show_logo = models.BooleanField(default=True)
nav_sticky = models.BooleanField(default=STICKY_NAV_PREF_DEFAULT)
max_owned_spaces = models.IntegerField(default=MAX_OWNED_SPACES_PREF_DEFAULT)
default_unit = models.CharField(max_length=32, default='g')
use_fractions = models.BooleanField(default=FRACTION_PREF_DEFAULT)
use_kj = models.BooleanField(default=KJ_PREF_DEFAULT)
@ -434,6 +433,15 @@ class UserPreference(models.Model, PermissionModelMixin):
created_at = models.DateTimeField(auto_now_add=True)
objects = ScopedManager(space='space')
def save(self, *args, **kwargs):
if not self.pk:
self.max_owned_spaces = MAX_OWNED_SPACES_PREF_DEFAULT
self.comments = COMMENT_PREF_DEFAULT
self.nav_sticky = STICKY_NAV_PREF_DEFAULT
self.use_kj = KJ_PREF_DEFAULT
self.use_fractions = FRACTION_PREF_DEFAULT
return super().save(*args, **kwargs)
def __str__(self):
return str(self.user)

View File

@ -311,7 +311,7 @@ class SpaceSerializer(WritableNestedModelSerializer):
fields = (
'id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users',
'allow_sharing', 'demo', 'food_inherit', 'user_count', 'recipe_count', 'file_size_mb',
'image', 'nav_logo', 'space_theme', 'custom_space_theme', 'nav_bg_color', 'nav_text_color', 'use_plural',
'image', 'nav_logo', 'space_theme', 'custom_space_theme', 'nav_bg_color', 'nav_text_color',
'logo_color_32', 'logo_color_128', 'logo_color_144', 'logo_color_180', 'logo_color_192', 'logo_color_512', 'logo_color_svg',)
read_only_fields = (
'id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing',

View File

@ -10,7 +10,7 @@
<title>{% block title %}
{% endblock %}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover">
<meta name="robots" content="noindex,nofollow"/>
<link rel="icon" href="{{ theme_values.logo_color_svg }}">
@ -481,6 +481,14 @@
overflow-x: hidden;
}
}
#id_base_container {
padding-bottom: env(safe-area-inset-bottom);
}
.fixed-bottom {
padding-bottom: max(0.5rem, calc(env(safe-area-inset-bottom) - 0.5rem)) !important;
}
</style>
</body>

View File

@ -3,24 +3,27 @@ from django.templatetags.static import static
from django_scopes import scopes_disabled
from cookbook.models import UserPreference, UserFile, Space
from recipes.settings import STICKY_NAV_PREF_DEFAULT, UNAUTHENTICATED_THEME_FROM_SPACE
from recipes.settings import STICKY_NAV_PREF_DEFAULT, UNAUTHENTICATED_THEME_FROM_SPACE, FORCE_THEME_FROM_SPACE
register = template.Library()
@register.simple_tag
def theme_values(request):
return get_theming_values(request)
def get_theming_values(request):
space = None
if request.space:
if getattr(request, 'space', None):
space = request.space
if not request.user.is_authenticated and UNAUTHENTICATED_THEME_FROM_SPACE > 0:
if not request.user.is_authenticated and UNAUTHENTICATED_THEME_FROM_SPACE > 0 and FORCE_THEME_FROM_SPACE == 0:
with scopes_disabled():
space = Space.objects.filter(id=UNAUTHENTICATED_THEME_FROM_SPACE).first()
if FORCE_THEME_FROM_SPACE:
with scopes_disabled():
space = Space.objects.filter(id=FORCE_THEME_FROM_SPACE).first()
return get_theming_values(space, request.user)
def get_theming_values(space, user):
themes = {
UserPreference.BOOTSTRAP: 'themes/bootstrap.min.css',
UserPreference.FLATLY: 'themes/flatly.min.css',
@ -46,17 +49,17 @@ def get_theming_values(space, user):
'nav_bg_color': '#ddbf86',
'nav_text_class': 'navbar-light',
'sticky_nav': 'position: sticky; top: 0; left: 0; z-index: 1000;',
'app_name': 'Tandoor Recipes',
}
if user.is_authenticated:
if user.userpreference.theme in themes:
tv['theme'] = static(themes[user.userpreference.theme])
if user.userpreference.nav_bg_color:
tv['nav_bg_color'] = user.userpreference.nav_bg_color
if user.userpreference.nav_text_color and user.userpreference.nav_text_color in nav_text_type_mapping:
tv['nav_text_class'] = nav_text_type_mapping[user.userpreference.nav_text_color]
if not user.userpreference.nav_sticky:
if request.user.is_authenticated:
if request.user.userpreference.theme in themes:
tv['theme'] = static(themes[request.user.userpreference.theme])
if request.user.userpreference.nav_bg_color:
tv['nav_bg_color'] = request.user.userpreference.nav_bg_color
if request.user.userpreference.nav_text_color and request.user.userpreference.nav_text_color in nav_text_type_mapping:
tv['nav_text_class'] = nav_text_type_mapping[request.user.userpreference.nav_text_color]
if not request.user.userpreference.nav_sticky:
tv['sticky_nav'] = ''
if space:
@ -74,4 +77,6 @@ def get_theming_values(space, user):
tv['nav_bg_color'] = space.nav_bg_color
if space.nav_text_color and space.nav_text_color in nav_text_type_mapping:
tv['nav_text_class'] = nav_text_type_mapping[space.nav_text_color]
if space.app_name:
tv['app_name'] = space.app_name
return tv

View File

@ -1,36 +1,68 @@
from django.contrib import auth
from django.templatetags.static import static
from django.test import RequestFactory
from django_scopes import scopes_disabled
from cookbook.models import Space, UserPreference, UserFile
from cookbook.templatetags.theming_tags import theme_values, get_theming_values
def test_theming_function(space_1, u1_s1):
user = auth.get_user(u1_s1)
# uf = UserFile.objects.create(name='test', space=space_1, created_by=user) #TODO add file tests
user = auth.get_user(u1_s1)
request = RequestFactory()
request.user = auth.get_user(u1_s1)
request.space = space_1
assert get_theming_values(space_1, user)['theme'] == static('themes/tandoor.min.css')
assert get_theming_values(space_1, user)['nav_bg_color'] == '#ddbf86'
assert get_theming_values(space_1, user)['nav_text_class'] == 'navbar-light'
assert get_theming_values(space_1, user)['nav_logo'] == static('assets/brand_logo.png')
assert get_theming_values(space_1, user)['sticky_nav'] == 'position: sticky; top: 0; left: 0; z-index: 1000;'
# defaults apply without setting anything (user preference is automatically created with these defaults)
assert get_theming_values(request)['theme'] == static('themes/tandoor.min.css')
assert get_theming_values(request)['nav_bg_color'] == '#ddbf86'
assert get_theming_values(request)['nav_text_class'] == 'navbar-light'
assert get_theming_values(request)['nav_logo'] == static('assets/brand_logo.png')
assert get_theming_values(request)['sticky_nav'] == 'position: sticky; top: 0; left: 0; z-index: 1000;'
assert get_theming_values(request)['app_name'] == 'Tandoor Recipes'
user.userpreference.theme = UserPreference.TANDOOR_DARK
user.userpreference.nav_bg_color = '#ffffff'
user.userpreference.nav_text_color = UserPreference.LIGHT
user.userpreference.nav_sticky = False
user.userpreference.save()
with scopes_disabled():
up = UserPreference.objects.filter(user=request.user).first()
up.theme = UserPreference.TANDOOR_DARK
up.nav_bg_color = '#ffffff'
up.nav_text_color = UserPreference.LIGHT
up.nav_sticky = False
up.save()
assert get_theming_values(space_1, user)['theme'] == static('themes/tandoor_dark.min.css')
assert get_theming_values(space_1, user)['nav_bg_color'] == '#ffffff'
assert get_theming_values(space_1, user)['nav_text_class'] == 'navbar-dark'
assert get_theming_values(space_1, user)['sticky_nav'] == ''
request = RequestFactory()
request.user = auth.get_user(u1_s1)
request.space = space_1
# user values apply if only those are present
assert get_theming_values(request)['theme'] == static('themes/tandoor_dark.min.css')
assert get_theming_values(request)['nav_bg_color'] == '#ffffff'
assert get_theming_values(request)['nav_text_class'] == 'navbar-dark'
assert get_theming_values(request)['sticky_nav'] == ''
assert get_theming_values(request)['app_name'] == 'Tandoor Recipes'
space_1.space_theme = Space.BOOTSTRAP
space_1.nav_bg_color = '#000000'
space_1.nav_text_color = UserPreference.DARK
space_1.app_name = 'test_app_name'
space_1.save()
assert get_theming_values(space_1, user)['theme'] == static('themes/bootstrap.min.css')
assert get_theming_values(space_1, user)['nav_bg_color'] == '#000000'
assert get_theming_values(space_1, user)['nav_text_class'] == 'navbar-light'
request = RequestFactory()
request.user = auth.get_user(u1_s1)
request.space = space_1
# space settings apply when set
assert get_theming_values(request)['theme'] == static('themes/bootstrap.min.css')
assert get_theming_values(request)['nav_bg_color'] == '#000000'
assert get_theming_values(request)['nav_text_class'] == 'navbar-light'
assert get_theming_values(request)['app_name'] == 'test_app_name'
user.userspace_set.all().delete()
request = RequestFactory()
request.user = auth.get_user(u1_s1)
# default user settings should apply when user has no space
assert get_theming_values(request)['nav_bg_color'] == '#ffffff'
assert get_theming_values(request)['nav_text_class'] == 'navbar-dark'
assert get_theming_values(request)['nav_logo'] == static('assets/brand_logo.png')

View File

@ -1686,10 +1686,10 @@ def get_plan_ical(request, from_date, to_date):
).filter(space=request.user.userspace_set.filter(active=1).first().space).distinct().all()
if from_date is not None:
queryset = queryset.filter(date__gte=from_date)
queryset = queryset.filter(from_date__gte=from_date)
if to_date is not None:
queryset = queryset.filter(date__lte=to_date)
queryset = queryset.filter(to_date__lte=to_date)
cal = Calendar()

View File

@ -31,6 +31,7 @@ from cookbook.helper.permission_helper import (group_required, has_group_permiss
from cookbook.models import (Comment, CookLog, InviteLink, SearchFields, SearchPreference,
ShareLink, Space, UserSpace, ViewLog)
from cookbook.tables import CookLogTable, ViewLogTable
from cookbook.templatetags.theming_tags import get_theming_values
from cookbook.version_info import VERSION_INFO
from recipes.settings import PLUGINS, BASE_DIR
@ -78,6 +79,11 @@ def space_overview(request):
messages.add_message(request, messages.WARNING, _('This feature is not available in the demo version!'))
else:
if create_form.is_valid():
if Space.objects.filter(created_by=request.user).count() >= request.user.userpreference.max_owned_spaces:
messages.add_message(request, messages.ERROR,
_('You have the reached the maximum amount of spaces that can be owned by you.') + f' ({request.user.userpreference.max_owned_spaces})')
return HttpResponseRedirect(reverse('view_space_overview'))
created_space = Space.objects.create(
name=create_form.cleaned_data['name'],
created_by=request.user,
@ -481,29 +487,24 @@ def report_share_abuse(request, token):
def web_manifest(request):
theme_values = get_theming_values(request)
icons = [
{"src": static("/assets/logo_color.svg"), "sizes": "any"},
{"src": static("/assets/logo_color144.png"), "type": "image/png", "sizes": "144x144"},
{"src": static("/assets/logo_color512.png"), "type": "image/png", "sizes": "512x512"}
{"src": theme_values['logo_color_svg'], "sizes": "any"},
{"src": theme_values['logo_color_144'], "type": "image/png", "sizes": "144x144"},
{"src": theme_values['logo_color_512'], "type": "image/png", "sizes": "512x512"}
]
if request.user.is_authenticated and getattr(request.space, 'logo_color_svg') and getattr(request.space, 'logo_color_144') and getattr(request.space, 'logo_color_512'):
icons = [
{"src": request.space.logo_color_svg.file.url, "sizes": "any"},
{"src": request.space.logo_color_144.file.url, "type": "image/png", "sizes": "144x144"},
{"src": request.space.logo_color_512.file.url, "type": "image/png", "sizes": "512x512"}
]
manifest_info = {
"name": "Tandoor Recipes",
"short_name": "Tandoor",
"name": theme_values['app_name'],
"short_name": theme_values['app_name'],
"description": _("Manage recipes, shopping list, meal plans and more."),
"icons": icons,
"start_url": "./search",
"background_color": "#ffcb76",
"background_color": theme_values['nav_bg_color'],
"display": "standalone",
"scope": ".",
"theme_color": "#ffcb76",
"theme_color": theme_values['nav_bg_color'],
"shortcuts": [
{
"name": _("Plan"),

View File

@ -10,9 +10,12 @@ Tandoor can be installed as a progressive web app (PWA) on mobile and desktop de
#### Safari (iPhone/iPad)
Open Tandoor, click Safari's share button, select `Add to Home Screen`
### Chrome/Chromium
#### Chrome/Chromium
Open Tandoor, click the `add Tandoor to the home screen` message that pops up at the bottom of the screen
#### Firefox for Android
Open Tandoor, click on the `⋮` menu icon, then on `Install`
### Desktop browsers
#### Google Chrome
@ -124,13 +127,13 @@ to your dream setup.
## How can I upgrade postgres (major versions)?
Postgres requires manual intervention when updating from one major version to another. The steps are roughly
1. use `pg_dumpall` to dump your database into SQL (for Docker `docker-compose exec -T <database_container_name> pg_dumpall -U <postgres_user_name> -f /path/to/dump.sql`)
1. use `pg_dumpall` to dump your database into SQL (for Docker `docker-compose exec -T <postgres_container_name> pg_dumpall -U <postgres_user_name> -f /path/to/dump.sql`)
2. stop the DB / down the container
3. move your postgres directory in order to keep it as a backup (e.g. `mv postgres postgres_old`)
4. update postgres to the new major version (for Docker just change the version number and pull)
5. start the db / up the container (do not start tandoor as it will automatically perform the database migrations which will conflict with loading the dump)
6. if not using docker, you might need to create the same postgres user you had in the old database
7. load the postgres dump (for Docker `'/usr/local/bin/docker-compose exec -T <database_container_name> psql -U <postgres_user_name> postgres < /path/to/dump.sql`)
7. load the postgres dump (for Docker `'/usr/local/bin/docker-compose exec -T <postgres_container_name> psql -U <postgres_user_name> <postgres_database_name> < /path/to/dump.sql`)
If anything fails, go back to the old postgres version and data directory and try again.

View File

@ -50,7 +50,7 @@ In order to prevent denial of service attacks on the RegEx engine the number of
and the length of the inputs that are processed are limited. Those limits should never be reached
during normal usage.
## Instructtion Replace, Title Replace, Food Replace & Unit Replace
## Instruction Replace, Title Replace, Food Replace & Unit Replace
These work just like the Description Replace automation.
Instruction, Food and Unit Replace will run against every iteration of the object in a recipe during import.

View File

@ -1,5 +1,5 @@
!!! info "WIP"
While being around for a while there are still a lot of features that i plan on adding to the shopping list.
While being around for a while there are still a lot of features that I plan on adding to the shopping list.
You can see an overview of what is still planned on [this](https://github.com/vabene1111/recipes/issues/114) issue.
@ -41,4 +41,4 @@ There are a few more features worth pointing out
1. You can export recipes for use in other applications (Google Keep, etc.) by using the export button
2. In the export popup you can define a prefix to be put before each row in case an external app requires that
3. Marking a shopping list as finished will hide it from the shopping list page
3. Marking a shopping list as finished will hide it from the shopping list page

View File

@ -204,6 +204,22 @@ server {
}
```
Tandoor does not support directly serving of images, as explained in the [Nginx vs Gunicorn"](#nginx-vs-gunicorn) section. If you are already using nginx to serve as a reverse proxy, you can configure it to serve images as well.
Add the following directly after the `location /` context:
```
location /media/ {
root /media/;
index index.html index.htm;
}
```
Make sure you also update your `docker-compose.yml` file to mount the `mediafiles` directory. If you are using the [Plain](#plain) deployment, you do not need to make any changes. If you are using nginx to act as a reverse proxy for other apps, it may not be optimal to have `mediafiles` mounted to `/media`. In that case, adjust the directory declarations as needed, utilizing nginx's [`alias`](https://nginx.org/en/docs/http/ngx_http_core_module.html#alias) if needed.
!!!note
Use `alias` if your mount point directory is not the same as the URL request path. Tandoor media files are requested from `$http_host/media/recipes/xxx.jpg`. This means if you are mounting to a directory that does **NOT** end in `./media`, you will need to use `alias`.
!!!note
Don't forget to [download and configure](#docker-compose) your ```.env``` file!

View File

@ -59,6 +59,155 @@ I used two paths `<sub path>` and `<www path>` for simplicity. In my case I have
I left out the TLS config in this example for simplicity.
## Docker + Apache + Sub-Path
The following could prove to be useful if you are not using Traefik, but instead run Apache as your reverse proxy to route all calls for a shared (sub)domain to a sub path, e.g. https://mydomain.tld/tandoor
As a side note, I am using [Blocky](https://0xerr0r.github.io/blocky/) + [Consul](https://hub.docker.com/r/hashicorp/consul) + [Registrator](https://hub.docker.com/r/gliderlabs/registrator) as a DNS solution.
The relevant Apache config:
```
<Location /tandoor>
# in case you want to restrict access to specific IP addresses:
Require local
Require forward-dns [myhomedomain.useyourdomain.com]
Require ip [anylocalorremoteipyouwanttowhitelist]
# The following assumes that tandoor.service.consul.local resolves to the IP address of the Docker container.
ProxyPass http://tandoor.service.consul.local:8080/tandoor
ProxyPassReverse http://tandoor.service.consul.local:8080/tandoor
RequestHeader add X-Script-Name /tandoor
RequestHeader set X-Forwarded-Proto "https"
ProxyPreserveHost On
</Location>
<Location /tandoor/static>
Require local
Require forward-dns [myhomedomain.useyourdomain.com]
Require ip [anylocalorremoteipyouwanttowhitelist]
ProxyPass http://tandoor.service.consul.local:8080/tandoor/tandoor/static
ProxyPassReverse http://tandoor.service.consul.local:8080/tandoor/static
RequestHeader add X-Script-Name /tandoor
RequestHeader set X-Forwarded-Proto "https"
ProxyPreserveHost On
</Location>
```
and the relevant section from the docker-compose.yml:
```
tandoor:
restart: always
container_name: tandoor
image: vabene1111/recipes
environment:
- SCRIPT_NAME=/tandoor
- JS_REVERSE_SCRIPT_PREFIX=/tandoor
- STATIC_URL=/tandoor/static/
- MEDIA_URL=/tandoor/media/
- GUNICORN_MEDIA=0
- SECRET_KEY=${YOUR_TANDOOR_SECRET_KEY}
- POSTGRES_HOST=postgres.service.consul.local
- POSTGRES_PORT=${POSTGRES_PORT}
- POSTGRES_USER=${YOUR_TANDOOR_POSTGRES_USER}
- POSTGRES_PASSWORD=${YOUR_TANDOOR_POSTGRES_PASSWORD}
- POSTGRES_DB=${YOUR_TANDOOR_POSTGRES_DB}
labels:
# The following is relevant only if you are using Registrator and Consul
- "SERVICE_NAME=tandoor"
volumes:
- ${YOUR_DOCKER_VOLUME_BASE_DIR}/tandoor/static:/opt/recipes/staticfiles:rw
# Do not make this a bind mount, see https://docs.tandoor.dev/install/docker/#volumes- vs-bind-mounts
- tandoor_nginx_config:/opt/recipes/nginx/conf.d
- ${YOUR_DOCKER_VOLUME_BASE_DIR}}/tandoor/media:/opt/recipes/mediafiles:rw
depends_on:
# You will have to set up postgres accordingly
- postgres
```
The relevant docker-compose.yml for Registrator, Consul, and Blocky, and Autoheal:
```
consul:
image: hashicorp/consul
container_name: consul
command: >
agent -server
-domain consul.local
-advertise=${YOUR_DOCKER_HOST_IP_ON_THE_LAN}
-client=0.0.0.0
-encrypt=${SOME_SECRET_KEY}
-datacenter=${YOUR_DC_NAME}
-bootstrap-expect=1
-ui
-log-level=info
environment:
- "CONSUL_LOCAL_CONFIG={\"skip_leave_on_interrupt\": true, \"dns_config\": { \"service_ttl\": { \"*\": \"0s\" } } }"
network_mode: "host"
restart: always
registrator:
image: gliderlabs/registrator:latest
container_name: registrator
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
command: >
-internal
-cleanup=true
-deregister="always"
-resync=60
consul://host.docker.internal:8500
restart: always
blocky:
image: spx01/blocky
container_name: blocky
restart: unless-stopped
healthcheck:
interval: 30s
timeout: 5s
start_period: 1m
labels:
# The following is only relevant if you use autoheal
autoheal: true
# Optional the instance hostname for logging purpose
hostname: blocky
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- "1153:53/tcp"
- "1153:53/udp"
- 4000:4000
environment:
- TZ=YOUR_TIMEZONE # Optional to synchronize the log timestamp with host
volumes:
# Optional to synchronize the log timestamp with host
- /etc/localtime:/etc/localtime:ro
# config file
- ${YOUR_DOCKER_VOLUME_BASE_DIR}/blocky/config.yml:/app/config.yml
networks:
# in case you want to bind Blocky to an IP address
your-docker-network-name:
ipv4_address: 'some-ip-address-in-the-docker-network-subnet'
autoheal:
image: willfarrell/autoheal
volumes:
- '/var/run/docker.sock:/var/run/docker.sock'
environment:
- AUTOHEAL_CONTAINER_LABEL=autoheal
restart: always
container_name: autoheal
```
as well as a snippet of the Blocky configuration:
```
conditional:
fallbackUpstream: false
mapping:
consul.local: tcp+udp:host.docker.internal:8600
```
## WSL
If you want to install Tandoor on the Windows Subsystem for Linux you can find a detailed post here: <https://github.com/TandoorRecipes/recipes/issues/1733>.

View File

@ -45,7 +45,7 @@ To restore:
cat pgdump.sql | sudo docker exec -i docker_db_recipes_1 psql postgres -U djangouser
```
This connects to the postgres table instead of the actual dgangodb table, as the import function needs to delete the table, which can't be dropped off you're connected to it.
This connects to the postgres table instead of the actual djangodb table, as the import function needs to delete the table, which can't be dropped off you're connected to it.
## Backup using export and import
You can now export recipes from Tandoor using the export function. This method requires a working web interface.

View File

@ -524,6 +524,18 @@ The default value for the user preference 'sticky navigation' (always show navba
STICKY_NAV_PREF_DEFAULT=1
```
#### Max owned spaces
> default `100` - options: `0-X`
The default for the number of spaces a user can own. By setting to 0 space creation for users will be disabled.
Superusers can always bypass this limit.
```
MAX_OWNED_SPACES_PREF_DEFAULT=100
```
### Cosmetic / Preferences
#### Timezone
@ -548,6 +560,15 @@ With this setting you can specify the ID of a space of which the appearance sett
UNAUTHENTICATED_THEME_FROM_SPACE=
```
#### Force Theme
> default `0` - options `1-X` (space ID)
Similar to the Default theme but forces the theme upon all users (authenticated/unauthenticated) and all spaces
```
FORCE_THEME_FROM_SPACE=
```
### Rate Limiting / Performance
#### Shopping auto sync

View File

@ -81,7 +81,7 @@ sudo mv -R ~/.docker/compose/postgres ~/.docker/compose/postgres.old
```
8. Install postgres extensions
``` bash
docker exec -it {{database_container}} psql
docker exec -it {{database_container}} psql postgres -U {{djangouser}}
```
then
``` psql

View File

@ -1,5 +1,6 @@
server {
listen 80;
listen [::]:80 ipv6only=on;
server_name localhost;
client_max_body_size 128M;

View File

@ -57,7 +57,9 @@ COMMENT_PREF_DEFAULT = bool(int(os.getenv('COMMENT_PREF_DEFAULT', True)))
FRACTION_PREF_DEFAULT = bool(int(os.getenv('FRACTION_PREF_DEFAULT', False)))
KJ_PREF_DEFAULT = bool(int(os.getenv('KJ_PREF_DEFAULT', False)))
STICKY_NAV_PREF_DEFAULT = bool(int(os.getenv('STICKY_NAV_PREF_DEFAULT', True)))
MAX_OWNED_SPACES_PREF_DEFAULT = int(os.getenv('MAX_OWNED_SPACES_PREF_DEFAULT', 100))
UNAUTHENTICATED_THEME_FROM_SPACE = int(os.getenv('UNAUTHENTICATED_THEME_FROM_SPACE', 0))
FORCE_THEME_FROM_SPACE = int(os.getenv('FORCE_THEME_FROM_SPACE', 0))
# minimum interval that users can set for automatic sync of shopping lists
SHOPPING_MIN_AUTOSYNC_INTERVAL = int(

View File

@ -1,7 +1,7 @@
Django==4.2.7
cryptography===41.0.6
cryptography===41.0.7
django-annoying==0.10.6
django-autocomplete-light==3.9.4
django-autocomplete-light==3.9.7
django-cleanup==8.0.0
django-crispy-forms==2.0
crispy-bootstrap4==2022.1
@ -14,7 +14,7 @@ bleach==6.0.0
gunicorn==20.1.0
lxml==4.9.3
Markdown==3.5.1
Pillow==10.0.1
Pillow==10.2.0
psycopg2-binary==2.9.5
python-dotenv==1.0.0
requests==2.31.0
@ -26,7 +26,7 @@ pyyaml==6.0.1
uritemplate==4.1.1
beautifulsoup4==4.12.2
microdata==0.8.0
Jinja2==3.1.2
Jinja2==3.1.3
django-webpack-loader==1.8.1
git+https://github.com/BITSOLVER/django-js-reverse@071e304fd600107bc64bbde6f2491f1fe049ec82
django-allauth==0.58.1
@ -36,13 +36,13 @@ pytest==7.4.3
pytest-django==4.6.0
django-treebeard==4.7
django-cors-headers==4.2.0
django-storages==1.13.2
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
pytest-factoryboy==2.5.1
pytest-factoryboy==2.6.0
pyppeteer==1.0.2
validators==0.20.0
pytube==15.0.0

View File

@ -45,8 +45,8 @@
"vue-template-compiler": "2.7.14",
"vue2-touch-events": "^3.2.2",
"vuedraggable": "^2.24.3",
"workbox-webpack-plugin": "^6.5.4",
"workbox-window": "^6.5.4"
"workbox-webpack-plugin": "^7.0.0",
"workbox-window": "^7.0.0"
},
"devDependencies": {
"@kazupon/vue-i18n-loader": "^0.5.0",
@ -65,11 +65,11 @@
"typescript": "~5.1.6",
"vue-cli-plugin-i18n": "^2.3.2",
"webpack-bundle-tracker": "1.8.1",
"workbox-background-sync": "^6.5.4",
"workbox-background-sync": "^7.0.0",
"workbox-expiration": "^6.5.4",
"workbox-navigation-preload": "^6.5.4",
"workbox-navigation-preload": "^7.0.0",
"workbox-precaching": "^6.5.4",
"workbox-routing": "^6.5.4",
"workbox-routing": "^7.0.0",
"workbox-strategies": "^6.2.4"
},
"eslintConfig": {

View File

@ -62,7 +62,7 @@
</div>
</div>
<b-modal id="ingredient_edit_modal" :title="$t('Edit')" @hidden="destroyIngredientEditModal">
<b-modal id="ingredient_edit_modal" ref="ingredient_edit_modal" :title="$t('Edit')" @hidden="destroyIngredientEditModal">
<div v-if="current_edit_ingredient !== null">
<b-form-group v-bind:label="$t('Original_Text')" class="mb-3">
<b-form-input v-model="current_edit_ingredient.original_text" type="text" disabled></b-form-input>
@ -88,8 +88,8 @@
<div class="row w-100">
<div class="col-auto justify-content-end">
<b-button class="mx-1" >{{ $t('Ok') }}</b-button>
<b-button class="mx-1" @click="removeIngredient(current_edit_step,current_edit_ingredient);" variant="danger">{{ $t('Delete') }}</b-button>
<b-button class="mx-1" @click="destroyIngredientEditModal()">{{ $t('Ok') }}</b-button>
<b-button class="mx-1" @click="removeIngredient(current_edit_step,current_edit_ingredient);destroyIngredientEditModal()" variant="danger">{{ $t('Delete') }}</b-button>
</div>
</div>
</template>

View File

@ -80,80 +80,73 @@
</div>
</div>
</div>
<div class="row d-block d-lg-none">
<div class="d-block d-lg-none">
<div class="row">
<div class="col">
<div class="">
<div>
<div class="col-12">
<div class="col-12 d-flex justify-content-center mt-2">
<b-button-toolbar key-nav aria-label="Toolbar with button groups">
<b-button-group class="mx-1">
<b-button v-html="'<<'" class="p-2 pr-3 pl-3"
@click="setShowDate($refs.header.headerProps.previousPeriod)"></b-button>
</b-button-group>
<b-button-group class="mx-1">
<b-button @click="setShowDate($refs.header.headerProps.currentPeriod)"><i
class="fas fa-home"></i></b-button>
<b-form-datepicker right button-only button-variant="secondary" @context="datePickerChanged"></b-form-datepicker>
</b-button-group>
<b-button-group class="mx-1">
<b-button v-html="'>>'" class="p-2 pr-3 pl-3"
@click="setShowDate($refs.header.headerProps.nextPeriod)"></b-button>
</b-button-group>
</b-button-toolbar>
<div class="col-12">
<div class="col-12 d-flex justify-content-center mt-2">
<b-button-toolbar key-nav aria-label="Toolbar with button groups">
<b-button-group class="mx-1">
<b-button v-html="'<<'" class="p-2 pr-3 pl-3"
@click="setShowDate($refs.header.headerProps.previousPeriod)"></b-button>
</b-button-group>
<b-button-group class="mx-1">
<b-button @click="setShowDate($refs.header.headerProps.currentPeriod)"><i
class="fas fa-home"></i></b-button>
<b-form-datepicker right button-only button-variant="secondary" @context="datePickerChanged"></b-form-datepicker>
</b-button-group>
<b-button-group class="mx-1">
<b-button v-html="'>>'" class="p-2 pr-3 pl-3"
@click="setShowDate($refs.header.headerProps.nextPeriod)"></b-button>
</b-button-group>
</b-button-toolbar>
</div>
</div>
<div class="col-12 mt-2" style="padding-bottom: 60px">
<div v-for="day in mobileSimpleGrid" v-bind:key="day.day">
<b-list-group>
<b-list-group-item>
<div class="d-flex flex-row align-middle">
<h6 class="mb-0 mt-1 align-middle">{{ day.date_label }}</h6>
<div class="flex-grow-1 text-right">
<b-button class="btn-sm btn-outline-primary" @click="showMealPlanEditModal(null, day.create_default_date)"><i
class="fa fa-plus"></i></b-button>
</div>
</div>
</div>
<div class="col-12 mt-2" style="padding-bottom: 60px">
<div v-for="day in mobileSimpleGrid" v-bind:key="day.day">
<b-list-group>
<b-list-group-item>
<div class="d-flex flex-row align-middle">
<h6 class="mb-0 mt-1 align-middle">{{ day.date_label }}</h6>
<div class="flex-grow-1 text-right">
<b-button class="btn-sm btn-outline-primary" @click="showMealPlanEditModal(null, day.create_default_date)"><i
class="fa fa-plus"></i></b-button>
</div>
</div>
</b-list-group-item>
<b-list-group-item v-for="plan in day.plan_entries" v-bind:key="plan.entry.id">
<div class="d-flex flex-row align-items-center">
<div>
<b-img style="height: 50px; width: 50px; object-fit: cover"
:src="plan.entry.recipe.image" rounded="circle" v-if="plan.entry.recipe?.image"></b-img>
<b-img style="height: 50px; width: 50px; object-fit: cover"
:src="image_placeholder" rounded="circle" v-else></b-img>
</div>
<div class="flex-grow-1 ml-2"
style="text-overflow: ellipsis; overflow-wrap: anywhere;">
</b-list-group-item>
<b-list-group-item v-for="plan in day.plan_entries" v-bind:key="plan.entry.id">
<div class="d-flex flex-row align-items-center">
<div>
<b-img style="height: 50px; width: 50px; object-fit: cover"
:src="plan.entry.recipe.image" rounded="circle" v-if="plan.entry.recipe?.image"></b-img>
<b-img style="height: 50px; width: 50px; object-fit: cover"
:src="image_placeholder" rounded="circle" v-else></b-img>
</div>
<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>
<span v-else>{{ plan.entry.title }}</span> <br/>
</span>
<span v-if="plan.entry.note" class="two-row-text">
<span v-if="plan.entry.note" class="two-row-text">
<small>{{ plan.entry.note }}</small> <br/>
</span>
<small class="text-muted">
<span v-if="plan.entry.shopping" class="font-light"><i class="fas fa-shopping-cart fa-xs "/></span>
{{ plan.entry.meal_type_name }}
<span v-if="plan.entry.recipe">
<small class="text-muted">
<span v-if="plan.entry.shopping" class="font-light"><i class="fas fa-shopping-cart fa-xs "/></span>
{{ plan.entry.meal_type_name }}
<span v-if="plan.entry.recipe">
- <i class="fa fa-clock"></i> {{ plan.entry.recipe.working_time + plan.entry.recipe.waiting_time }} {{ $t('min') }}
</span>
</small>
</div>
<div class="hover-button">
<a class="pr-2" @click.stop="openContextMenu($event, {originalItem: plan})"><i class="fas fa-ellipsis-v"></i></a>
</div>
</div>
</b-list-group-item>
</b-list-group>
</small>
</div>
<div class="hover-button">
<a class="pr-2" @click.stop="openContextMenu($event, {originalItem: plan})"><i class="fas fa-ellipsis-v"></i></a>
</div>
</div>
</div>
</b-list-group-item>
</div>
</b-list-group>
</div>
</div>
</div>
@ -364,7 +357,7 @@ export default {
},
mobileSimpleGrid() {
let grid = [];
let currentDate = moment();
let currentDate = moment(this.showDate);
for (let x = 0; x < 7; x++) {
let moment_date = currentDate.clone().add(x, "d");
grid.push({
@ -483,7 +476,7 @@ export default {
this.setShowDate(ctx.selectedDate)
},
setShowDate(d) {
this.showDate = d
this.showDate = d ?? new Date();
},
createEntryClick(data) {
this.mealplan_default_date = moment(data).format("YYYY-MM-DD")

View File

@ -1321,3 +1321,9 @@ textarea:not(.form-control) {
border: 0 !important;
}
</style>
<style scoped>
.row.fixed-bottom {
margin: 0;
}
</style>

View File

@ -108,4 +108,4 @@ export default {
-o-transform: translate(-50%, 0);
transform: translate(-50%, 0);
}
</style>
</style>

View File

@ -231,7 +231,7 @@ export default {
font-size: 1.25rem;
line-height: 1;
background-color: transparent;
border: 1px solid rgba(46, 46, 46, 0.5);
border: 1px solid rgba(46, 46, 46, 0.1);
border-radius: 0.1875rem;
z-index: 1001;
}

View File

@ -20,7 +20,7 @@
<cookbook-toc :recipes="recipes" v-if="current_page === 1" v-on:switchRecipe="switchRecipe($event)"></cookbook-toc>
</transition>
<transition name="flip" mode="out-in">
<recipe-card :recipe="display_recipes[1].recipe_content" v-if="current_page > 1 && display_recipes.length === 2" :key="display_recipes[1].recipe" :use_plural="use_plural"></recipe-card>
<recipe-card :recipe="display_recipes[1].recipe_content" v-if="current_page > 1 && display_recipes.length === 2" :key="display_recipes[1].recipe" ></recipe-card>
</transition>
</div>
<div class="col-md-1" @click="swipeLeft" style="cursor: pointer"></div>
@ -57,10 +57,7 @@ export default {
}
},
mounted(){
let apiClient = new ApiApiFactory()
apiClient.retrieveSpace(window.ACTIVE_SPACE_ID).then(r => {
this.use_plural = r.data.use_plural
})
},
data() {
return {
@ -69,7 +66,6 @@ export default {
bounce_left: false,
bounce_right: false,
cookbook_editing: false,
use_plural: false,
}
},
methods: {

View File

@ -76,8 +76,7 @@
<div class="col-md-10 offset-md-2">
<generic-horizontal-card v-for="child in item[children]"
v-bind:key="child.id"
:item="child" :model="model"
:use_plural="use_plural"
:item="child" :model="model"
@item-action="$emit('item-action', $event)"></generic-horizontal-card>
</div>
</div>
@ -160,7 +159,6 @@ export default {
recipe_count: { type: String, default: "numrecipe" },
recipes: { type: String, default: "recipes" },
show_context_menu: { type: Boolean, default: true },
use_plural: { type: Boolean, default: false},
},
data() {
return {

View File

@ -7,7 +7,7 @@
</template>
<template v-else>
<td class="d-print-none" v-if="detailed" @click="done">
<td class="d-print-none align-baseline py-2" v-if="detailed" @click="done">
<i class="far fa-check-circle text-success" v-if="ingredient.checked"></i>
<i class="far fa-check-circle text-primary" v-if="!ingredient.checked"></i>
</td>
@ -40,9 +40,9 @@
</template>
</template>
</td>
<td v-if="detailed">
<td v-if="detailed" class="align-baseline">
<template v-if="ingredient.note">
<span v-b-popover.hover="ingredient.note" class="d-print-none touchable py-0 px-2">
<span class="d-print-none touchable py-0 px-2" v-b-popover.hover="ingredient.note">
<i class="far fa-comment"></i>
</span>
@ -106,9 +106,22 @@ export default {
<style scoped>
/* increase size of hover/touchable space without changing spacing */
.touchable {
/* padding-right: 2em;
padding-left: 2em; */
margin-right: -1em;
margin-left: -1em;
--target-increase: 2em;
display: inline-flex;
}
.touchable::after {
content: "";
display: inline-block;
width: var(--target-increase);
margin-right: calc(var(--target-increase) * -1);
}
.touchable::before {
content: "";
display: inline-block;
width: var(--target-increase);
margin-left: calc(var(--target-increase) * -1);
}
</style>

View File

@ -24,7 +24,6 @@
<ingredient-component
:ingredient="i"
:ingredient_factor="ingredient_factor"
:use_plural="use_plural"
:key="i.id"
:detailed="detailed"
@checked-state-changed="$emit('checked-state-changed', $event)"
@ -64,7 +63,6 @@ export default {
recipe: {type: Number},
ingredient_factor: {type: Number, default: 1},
servings: {type: Number, default: 1},
use_plural: {type: Boolean, default: false},
detailed: {type: Boolean, default: true},
header: {type: Boolean, default: false},
recipe_list: {type: Number, default: undefined},

View File

@ -34,7 +34,6 @@
<div v-for="i in r.steps.flatMap((s) => s.ingredients)" v-bind:key="i.id">
<table class="table table-sm mb-0">
<ingredient-component
:use_plural="true"
:key="i.id"
:detailed="true"
:ingredient="i"

View File

@ -2,8 +2,7 @@
<div>
<template v-if="recipe && recipe.loading">
<b-card no-body v-hover style="height: 100%">
<b-card-img-lazy style="height: 15vh; object-fit: cover" class="" :src="placeholder_image"
v-bind:alt="$t('Recipe_Image')" top></b-card-img-lazy>
<b-card-img-lazy style="height: 15vh; object-fit: cover" class="" :src="placeholder_image" v-bind:alt="$t('Recipe_Image')" top></b-card-img-lazy>
<b-card-body class="p-4">
<h6>
@ -20,30 +19,32 @@
</template>
<template v-else>
<b-card no-body v-hover v-if="recipe" style="height: 100%">
<a :href="recipe_link">
<div class="content">
<div class="content-overlay" v-if="recipe.description !== null && recipe.description !== ''"></div>
<b-card-img-lazy style="height: 15vh; object-fit: cover" class="" :src="recipe_image"
v-bind:alt="$t('Recipe_Image')" top></b-card-img-lazy>
<b-card-img-lazy style="height: 15vh; object-fit: cover" class="" :src="recipe_image" v-bind:alt="$t('Recipe_Image')" top></b-card-img-lazy>
<div class="content-details" >
<div class="content-details">
<p class="content-text">
{{ recipe.description }}
{{ recipe.description }}
</p>
</div>
<div class="card-img-overlay d-flex flex-column justify-content-left float-left text-left pt-2" style="width:40%"
v-if="recipe.working_time !== 0 || recipe.waiting_time !== 0">
<b-badge pill variant="light" class="mt-1 font-weight-normal" v-if="recipe.working_time !== 0 && recipe.working_time !== undefined">
<i
class="fa fa-clock"></i> {{ working_time }}
</b-badge>
<b-badge pill variant="secondary" class="mt-1 font-weight-normal"
v-if="recipe.waiting_time !== 0 && recipe.waiting_time !== undefined">
<i class="fa fa-pause"></i> {{ waiting_time }}
</b-badge>
</div>
<b-row class="card-img-overlay pt-1">
<b-col cols="6">
<div v-if="recipe.working_time !== 0 || recipe.waiting_time !== 0">
<b-badge pill variant="light" class="mt-1 font-weight-normal" v-if="recipe.working_time !== 0 && recipe.working_time !== undefined">
<i class="fa fa-clock"></i> {{ working_time }}
</b-badge>
<b-badge pill variant="secondary" class="mt-1 font-weight-normal" v-if="recipe.waiting_time !== 0 && recipe.waiting_time !== undefined">
<i class="fa fa-pause"></i> {{ waiting_time }}
</b-badge>
</div>
</b-col>
<b-col cols="6" class="text-right">
<recipe-rating :recipe="recipe" :pill="true"></recipe-rating>
</b-col>
</b-row>
</div>
</a>
@ -51,18 +52,20 @@
<div class="d-flex flex-row">
<div class="flex-grow-1">
<a :href="recipe_link" class="text-body font-weight-bold two-row-text">
<template v-if="recipe !== null">{{ recipe.name }}</template>
<template v-else>{{ meal_plan.title }}</template>
</a>
<template v-if="recipe !== null">{{ recipe.name }}</template>
<template v-else>{{ meal_plan.title }}</template>
</a>
</div>
<div class="justify-content-end">
<recipe-context-menu :recipe="recipe" class="justify-content-end float-right align-items-end pr-0"
:disabled_options="context_disabled_options"
v-if="recipe !== null && show_context_menu"></recipe-context-menu>
<recipe-context-menu
:recipe="recipe"
class="justify-content-end float-right align-items-end pr-0"
:disabled_options="context_disabled_options"
v-if="recipe !== null && show_context_menu"
></recipe-context-menu>
</div>
</div>
<b-card-text style="text-overflow: ellipsis">
<template v-if="recipe !== null">
<div v-if="show_detail">
@ -71,34 +74,29 @@
<p class="mt-1 mb-1">
<last-cooked :recipe="recipe"></last-cooked>
<keywords-component :recipe="recipe" :limit="3" :enable_keyword_links="enable_keyword_links"
style="margin-top: 4px; position: relative; z-index: 3;"></keywords-component>
<keywords-component
:recipe="recipe"
:limit="3"
:enable_keyword_links="enable_keyword_links"
style="margin-top: 4px; position: relative; z-index: 3"
></keywords-component>
</p>
<transition name="fade" mode="in-out">
<div class="row mt-3" v-if="show_detail">
<div class="col-md-12">
<h6 class="card-title"><i class="fas fa-pepper-hot"></i> {{ $t("Ingredients") }}
</h6>
<h6 class="card-title"><i class="fas fa-pepper-hot"></i> {{ $t("Ingredients") }}</h6>
<ingredients-card
:steps="recipe.steps"
:header="false"
:detailed="false"
:servings="recipe.servings"/>
<ingredients-card :steps="recipe.steps" :header="false" :detailed="false" :servings="recipe.servings" />
</div>
</div>
</transition>
<b-badge pill variant="info" v-if="recipe.internal !== undefined && !recipe.internal">{{ $t("External") }}</b-badge>
</template>
</b-card-text>
</b-card-body>
</b-card>
</template>
</div>
<!--
@ -123,14 +121,14 @@
</template>
<script>
import RecipeContextMenu from "@/components/RecipeContextMenu"
import IngredientsCard from "@/components/IngredientsCard"
import KeywordsComponent from "@/components/KeywordsComponent"
import {resolveDjangoUrl, ResolveUrlMixin, calculateHourMinuteSplit} from "@/utils/utils"
import LastCooked from "@/components/LastCooked"
import RecipeContextMenu from "@/components/RecipeContextMenu"
import RecipeRating from "@/components/RecipeRating"
import { ResolveUrlMixin, calculateHourMinuteSplit, resolveDjangoUrl } from "@/utils/utils"
import moment from "moment/moment"
import Vue from "vue"
import LastCooked from "@/components/LastCooked"
import IngredientsCard from "@/components/IngredientsCard"
Vue.prototype.moment = moment
@ -141,19 +139,19 @@ export default {
LastCooked,
KeywordsComponent,
"recipe-context-menu": RecipeContextMenu,
IngredientsCard
IngredientsCard,
RecipeRating,
},
props: {
recipe: Object,
meal_plan: Object,
use_plural: {type: Boolean, default: false},
footer_text: String,
footer_icon: String,
detailed: {type: Boolean, default: true},
show_context_menu: {type: Boolean, default: true},
detailed: { type: Boolean, default: true },
show_context_menu: { type: Boolean, default: true },
context_disabled_options: Object,
open_recipe_on_click: {type: Boolean, default: true},
enable_keyword_links: {type: Boolean, default: true},
open_recipe_on_click: { type: Boolean, default: true },
enable_keyword_links: { type: Boolean, default: true },
},
data() {
return {
@ -161,8 +159,7 @@ export default {
}
},
mounted() {
},
mounted() {},
computed: {
show_detail: function () {
return this.recipe?.steps !== undefined && this.detailed
@ -180,13 +177,13 @@ export default {
waiting_time: function () {
return calculateHourMinuteSplit(this.recipe.waiting_time)
},
recipe_link: function (){
if(this.open_recipe_on_click){
return this.recipe.id !== undefined ? resolveDjangoUrl('view_recipe', this.recipe.id) : null
recipe_link: function () {
if (this.open_recipe_on_click) {
return this.recipe.id !== undefined ? resolveDjangoUrl("view_recipe", this.recipe.id) : null
} else {
return "#"
}
}
},
},
methods: {},
directives: {
@ -210,8 +207,7 @@ export default {
transition: opacity 0.5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */
{
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
@ -257,9 +253,11 @@ export default {
.content-details {
position: absolute;
text-align: center;
padding-left: 1em;
padding-right: 1em;
padding: 1em 1em 0 1em;
width: 100%;
max-height: 100%;
overflow-y: scroll;
z-index: 1;
top: 50%;
left: 50%;
opacity: 0;
@ -271,6 +269,10 @@ export default {
transition: all 0.3s ease-in-out 0s;
}
.content-details::-webkit-scrollbar {
display: none;
}
.content:hover .content-details {
top: 50%;
left: 50%;

View File

@ -1,36 +1,40 @@
<template>
<div>
<div class="dropdown d-print-none">
<a class="btn shadow-none pr-0 pl-0" href="javascript:void(0);" role="button" id="dropdownMenuLink"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<a class="btn shadow-none pr-0 pl-0" href="javascript:void(0);" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-ellipsis-v fa-lg"></i>
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuLink" >
<a class="dropdown-item" :href="resolveDjangoUrl('edit_recipe', recipe.id)" v-if="!disabled_options.edit"><i
class="fas fa-pencil-alt fa-fw"></i> {{ $t("Edit") }}</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuLink">
<a class="dropdown-item" :href="resolveDjangoUrl('edit_recipe', recipe.id)" v-if="!disabled_options.edit"
><i class="fas fa-pencil-alt fa-fw"></i> {{ $t("Edit") }}</a
>
<a class="dropdown-item" :href="resolveDjangoUrl('view_property_editor', recipe.id)" v-if="!disabled_options.edit">
<i class="fas fa-table"></i> {{ $t("Property_Editor") }}</a>
<i class="fas fa-table"></i> {{ $t("Property_Editor") }}</a
>
<a class="dropdown-item" :href="resolveDjangoUrl('edit_convert_recipe', recipe.id)"
v-if="!recipe.internal && !disabled_options.convert"><i class="fas fa-exchange-alt fa-fw"></i> {{ $t("convert_internal") }}</a>
<a class="dropdown-item" :href="resolveDjangoUrl('edit_convert_recipe', recipe.id)" v-if="!recipe.internal && !disabled_options.convert"
><i class="fas fa-exchange-alt fa-fw"></i> {{ $t("convert_internal") }}</a
>
<a href="javascript:void(0);">
<button class="dropdown-item" @click="$bvModal.show(`id_modal_add_book_${modal_id}`)" v-if="!disabled_options.books"><i
class="fas fa-bookmark fa-fw"></i> {{ $t("Manage_Books") }}
<button class="dropdown-item" @click="$bvModal.show(`id_modal_add_book_${modal_id}`)" v-if="!disabled_options.books">
<i class="fas fa-bookmark fa-fw"></i> {{ $t("Manage_Books") }}
</button>
</a>
<a class="dropdown-item" v-if="recipe.internal && !disabled_options.shopping" @click="addToShopping" href="#" > <i
class="fas fa-shopping-cart fa-fw"></i> {{ $t("Add_to_Shopping") }} </a>
<a class="dropdown-item" v-if="recipe.internal && !disabled_options.shopping" @click="addToShopping" href="#">
<i class="fas fa-shopping-cart fa-fw"></i> {{ $t("Add_to_Shopping") }}
</a>
<a class="dropdown-item" @click="createMealPlan" href="javascript:void(0);" v-if="!disabled_options.plan"><i
class="fas fa-calendar fa-fw"></i> {{ $t("Add_to_Plan") }} </a>
<a class="dropdown-item" @click="createMealPlan" href="javascript:void(0);" v-if="!disabled_options.plan"
><i class="fas fa-calendar fa-fw"></i> {{ $t("Add_to_Plan") }}
</a>
<a href="javascript:void(0);">
<button class="dropdown-item" @click="$bvModal.show(`id_modal_cook_log_${modal_id}`)" v-if="!disabled_options.log"><i
class="fas fa-clipboard-list fa-fw"></i> {{ $t("Log_Cooking") }}
<button class="dropdown-item" @click="$bvModal.show(`id_modal_cook_log_${modal_id}`)" v-if="!disabled_options.log">
<i class="fas fa-clipboard-list fa-fw"></i> {{ $t("Log_Cooking") }}
</button>
</a>
@ -41,56 +45,49 @@
</button>
</a>
<a href="javascript:void(0);">
<button class="dropdown-item" @click="copyToNew" v-if="!disabled_options.copy"><i class="fas fa-copy fa-fw"></i>
<button class="dropdown-item" @click="copyToNew" v-if="!disabled_options.copy">
<i class="fas fa-copy fa-fw"></i>
{{ $t("copy_to_new") }}
</button>
</a>
<a class="dropdown-item" :href="resolveDjangoUrl('view_export') + '?r=' + recipe.id" target="_blank"
rel="noopener noreferrer" v-if="!disabled_options.export"><i class="fas fa-file-export fa-fw"></i> {{ $t("Export") }}</a>
<a class="dropdown-item" :href="resolveDjangoUrl('view_export') + '?r=' + recipe.id" target="_blank" rel="noopener noreferrer" v-if="!disabled_options.export"
><i class="fas fa-file-export fa-fw"></i> {{ $t("Export") }}</a
>
<a href="javascript:void(0);">
<button class="dropdown-item" @click="pinRecipe()" v-if="!disabled_options.pin">
<i class="fas fa-thumbtack fa-fw"></i>
{{ isPinned ? $t("Unpin") : $t("Pin")}}
{{ isPinned ? $t("Unpin") : $t("Pin") }}
</button>
</a>
<a href="javascript:void(0);">
<button class="dropdown-item" @click="createShareLink()" v-if="recipe.internal && !disabled_options.share" ><i
class="fas fa-share-alt fa-fw"></i> {{ $t("Share") }}
<button class="dropdown-item" @click="createShareLink()" v-if="recipe.internal && !disabled_options.share">
<i class="fas fa-share-alt fa-fw"></i> {{ $t("Share") }}
</button>
</a>
</div>
</div>
<cook-log :recipe="recipe" :modal_id="modal_id"></cook-log>
<add-recipe-to-book :recipe="recipe" :modal_id="modal_id"
:entryEditing_inital_servings="servings_value"></add-recipe-to-book>
<shopping-modal :recipe="recipe" :servings="servings_value" :modal_id="modal_id" :mealplan="undefined"/>
<add-recipe-to-book :recipe="recipe" :modal_id="modal_id" :entryEditing_inital_servings="servings_value"></add-recipe-to-book>
<shopping-modal :recipe="recipe" :servings="servings_value" :modal_id="modal_id" :mealplan="undefined" />
<b-modal :id="`modal-share-link_${modal_id}`" v-bind:title="$t('Share')" hide-footer>
<div class="row">
<div class="col col-md-12">
<label v-if="recipe_share_link !== undefined">{{ $t("Public share link") }}</label>
<input ref="share_link_ref" class="form-control" v-model="recipe_share_link"/>
<b-button class="mt-2 mb-3 d-none d-md-inline" variant="secondary"
@click="$bvModal.hide(`modal-share-link_${modal_id}`)">{{ $t("Close") }}
</b-button>
<b-button class="mt-2 mb-3 ml-md-2" variant="primary" @click="copyShareLink()">{{
$t("Copy")
}}
</b-button>
<b-button class="mt-2 mb-3 ml-2 float-right" variant="success" @click="shareIntend()">{{
$t("Share")
}} <i class="fa fa-share-alt"></i></b-button>
<input ref="share_link_ref" class="form-control" v-model="recipe_share_link" />
<b-button class="mt-2 mb-3 d-none d-md-inline" variant="secondary" @click="$bvModal.hide(`modal-share-link_${modal_id}`)">{{ $t("Close") }} </b-button>
<b-button class="mt-2 mb-3 ml-md-2" variant="primary" @click="copyShareLink()">{{ $t("Copy") }} </b-button>
<b-button class="mt-2 mb-3 ml-2 float-right" variant="success" @click="shareIntend()">{{ $t("Share") }} <i class="fa fa-share-alt"></i></b-button>
</div>
</div>
</b-modal>
<meal-plan-edit-modal
:entry="entryEditing"
:entryEditing_inital_servings="servings_value"
@save-entry="saveMealPlan"
:modal_id="`modal-meal-plan_${modal_id}`"
:allow_delete="false"
@ -100,16 +97,16 @@
</template>
<script>
import {makeToast, resolveDjangoUrl, ResolveUrlMixin, StandardToasts} from "@/utils/utils"
import CookLog from "@/components/CookLog"
import axios from "axios"
import AddRecipeToBook from "@/components/Modals/AddRecipeToBook"
import MealPlanEditModal from "@/components/MealPlanEditModal"
import AddRecipeToBook from "@/components/Modals/AddRecipeToBook"
import ShoppingModal from "@/components/Modals/ShoppingModal"
import { useMealPlanStore } from "@/stores/MealPlanStore"
import { ApiApiFactory } from "@/utils/openapi/api"
import { makeToast, resolveDjangoUrl, ResolveUrlMixin, StandardToasts } from "@/utils/utils"
import axios from "axios"
import moment from "moment"
import Vue from "vue"
import {ApiApiFactory} from "@/utils/openapi/api"
import {useMealPlanStore} from "@/stores/MealPlanStore";
Vue.prototype.moment = moment
@ -143,7 +140,7 @@ export default {
},
},
entryEditing: {},
mealplan: undefined
mealplan: undefined,
}
},
props: {
@ -154,19 +151,18 @@ export default {
},
disabled_options: {
type: Object,
default: () => ({print:true}),
default: () => ({ print: true }),
},
},
mounted() {
this.servings_value = this.servings === -1 ? this.recipe.servings : this.servings
let pinnedRecipes = JSON.parse(localStorage.getItem("pinned_recipes")) || []
this.isPinned = pinnedRecipes.some((r) => r.id == this.recipe.id);
this.isPinned = pinnedRecipes.some((r) => r.id == this.recipe.id)
},
watch: {
recipe: {
handler() {
},
handler() {},
deep: true,
},
servings: function (newVal) {
@ -174,14 +170,14 @@ export default {
},
},
methods: {
pinRecipe () {
pinRecipe() {
let pinnedRecipes = JSON.parse(localStorage.getItem("pinned_recipes")) || []
if(this.isPinned) {
if (this.isPinned) {
pinnedRecipes = pinnedRecipes.filter((r) => r.id !== this.recipe.id)
makeToast(this.$t("Unpin"), this.$t("UnpinnedConfirmation", {recipe: this.recipe.name}), "info")
makeToast(this.$t("Unpin"), this.$t("UnpinnedConfirmation", { recipe: this.recipe.name }), "info")
} else {
pinnedRecipes.push({id: this.recipe.id, name: this.recipe.name})
makeToast(this.$t("Pin"), this.$t("PinnedConfirmation", {recipe: this.recipe.name}), "info")
pinnedRecipes.push({ id: this.recipe.id, name: this.recipe.name })
makeToast(this.$t("Pin"), this.$t("PinnedConfirmation", { recipe: this.recipe.name }), "info")
}
this.isPinned = !this.isPinned
localStorage.setItem("pinned_recipes", JSON.stringify(pinnedRecipes))
@ -211,6 +207,7 @@ export default {
createMealPlan(data) {
this.entryEditing = this.options.entryEditing
this.entryEditing.recipe = this.recipe
this.entryEditing.servings = this.recipe.servings
this.entryEditing.from_date = moment(new Date()).format("YYYY-MM-DD")
this.entryEditing.to_date = moment(new Date()).format("YYYY-MM-DD")
this.$nextTick(function () {
@ -218,17 +215,20 @@ export default {
})
},
createShareLink: function () {
console.log('create')
axios.get(resolveDjangoUrl("api_share_link", this.recipe.id)).then((result) => {
console.log('success')
this.$bvModal.show(`modal-share-link_${this.modal_id}`)
this.recipe_share_link = result.data.link
}).catch((err) => {
console.log('fail')
if (err.response.status === 403) {
makeToast(this.$t("Share"), this.$t("Sharing is not enabled for this space or your user account."), "danger")
}
})
console.log("create")
axios
.get(resolveDjangoUrl("api_share_link", this.recipe.id))
.then((result) => {
console.log("success")
this.$bvModal.show(`modal-share-link_${this.modal_id}`)
this.recipe_share_link = result.data.link
})
.catch((err) => {
console.log("fail")
if (err.response.status === 403) {
makeToast(this.$t("Share"), this.$t("Sharing is not enabled for this space or your user account."), "danger")
}
})
},
copyShareLink: function () {
let share_input = this.$refs.share_link_ref
@ -251,21 +251,21 @@ export default {
let apiClient = new ApiApiFactory()
apiClient.retrieveRecipe(this.recipe.id).then((results) => {
let recipe = {...results.data, ...{id: undefined, name: recipe_name}}
let recipe = { ...results.data, ...{ id: undefined, name: recipe_name } }
recipe.steps = recipe.steps.map((step) => {
return {
...step,
...{
id: undefined,
ingredients: step.ingredients.map((ingredient) => {
return {...ingredient, ...{id: undefined}}
return { ...ingredient, ...{ id: undefined } }
}),
},
}
})
recipe.properties = recipe.properties.map(p => {
return { ...p, ...{ id: undefined, } }
recipe.properties = recipe.properties.map((p) => {
return { ...p, ...{ id: undefined } }
})
apiClient

View File

@ -1,24 +1,30 @@
<template>
<div>
<span class="d-inline" v-if="recipe.rating > 0">
<i class="fas fa-star fa-xs text-primary" v-for="i in Math.floor(recipe.rating)" v-bind:key="i"></i>
<i class="fas fa-star-half-alt fa-xs text-primary" v-if="recipe.rating % 1 > 0"></i>
<i class="far fa-star fa-xs text-secondary" v-for="i in (5 - Math.ceil(recipe.rating))" v-bind:key="i + 10"></i>
</span>
</div>
<div>
<span class="d-inline" v-if="recipe.rating > 0">
<div v-if="!pill">
<i class="fas fa-star fa-xs text-primary" v-for="i in Math.floor(recipe.rating)" v-bind:key="i"></i>
<i class="fas fa-star-half-alt fa-xs text-primary" v-if="recipe.rating % 1 > 0"></i>
<i class="far fa-star fa-xs text-secondary" v-for="i in 5 - Math.ceil(recipe.rating)" v-bind:key="i + 10"></i>
</div>
<div v-else>
<b-badge pill variant="light" class="mt-1 font-weight-normal">
<i class="fas fa-star fa-xs text-dark" v-for="i in Math.floor(recipe.rating)" v-bind:key="i"></i>
<i class="fas fa-star-half-alt fa-xs text-dark" v-if="recipe.rating % 1 > 0"></i>
<i class="far fa-star fa-xs text-dark" v-for="i in 5 - Math.ceil(recipe.rating)" v-bind:key="i + 10"></i>
</b-badge>
</div>
</span>
</div>
</template>
<script>
export default {
name: "RecipeRating",
props: {
recipe: Object
}
name: "RecipeRating",
props: {
recipe: Object,
pill: { required: false, type: Boolean, default: false },
},
}
</script>
<style scoped>
</style>
<style scoped></style>

View File

@ -90,7 +90,6 @@
:index="index"
:start_time="start_time"
:force_ingredients="true"
:use_plural="use_plural"
></step-component>
</div>
</div>
@ -147,10 +146,6 @@ export default {
type: Boolean,
default: false,
},
use_plural: {
type: Boolean,
default: false,
},
},
computed: {
step_time: function() {

View File

@ -536,7 +536,7 @@
"Unit_Replace": "Einheit Ersetzen",
"quart": "\"Quart\" [qt] (US, Volumen)",
"imperial_quart": "Engl. \"Quart\" [imp qt] (UK, Volumen)",
"err_importing_recipe": "Beim Importieren des Rezeptes ist ein Fehler aufgetreten!",
"err_importing_recipe": "Es trat ein Fehler auf beim importieren des Rezepts!",
"property_type_fdc_hint": "Nur Nährwerte mit einer FDC ID können automatisch Daten aus der FDC Datenbank beziehen",
"Property_Editor": "Nährwerte bearbeiten",
"CustomTheme": "Benutzerdefiniertes Theme",

View File

@ -534,5 +534,25 @@
"Food_Replace": "Zastąp produkt",
"Unit_Replace": "Zastąp jednostkę",
"Alignment": "Wyrównanie",
"make_now_count": "Najbardziej brakujące składniki"
"make_now_count": "Najbardziej brakujące składniki",
"CustomTheme": "Własny motyw",
"CustomThemeHelp": "Zastąp style wybranego motywu, przesyłając własny plik CSS.",
"CustomLogoHelp": "Prześlij kwadratowe obrazy w różnych rozmiarach, aby zmienić logo w zakładce przeglądarki i zainstalowanej aplikacji internetowej.",
"Logo": "Logo",
"Show_Logo_Help": "Pokaż logo Tandoor lub przestrzeni na pasku nawigacyjnym.",
"Space_Cosmetic_Settings": "Administratorzy przestrzeni mogą zmienić niektóre ustawienia kosmetyczne, które zastąpią ustawienia klienta dla tej przestrzeni.",
"err_importing_recipe": "Wystąpił błąd podczas importowania przepisu!",
"Properties_Food_Amount": "Właściwości ilości żywności",
"Properties_Food_Unit": "Właściwości jednostek żywności",
"FDC_Search": "Wyszukiwanie w FDC",
"property_type_fdc_hint": "Tylko właściwe typy z identyfikatorem FDC mogą automatycznie pobierać dane z bazy danych FDC",
"Property_Editor": "Edytor właściwości",
"FDC_ID": "Identyfikator FDC",
"FDC_ID_help": "Identyfikator bazy FDC",
"CustomImageHelp": "Prześlij obraz, który będzie wyświetlany w przeglądzie przestrzeni.",
"CustomNavLogoHelp": "Prześlij obraz, który będzie używany jako logo paska nawigacyjnego.",
"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."
}

View File

@ -6049,9 +6049,9 @@ flush-write-stream@^1.0.0:
readable-stream "^2.3.6"
follow-redirects@^1.0.0, follow-redirects@^1.15.0:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
version "1.15.4"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
for-each@^0.3.3:
version "0.3.3"
@ -11848,13 +11848,13 @@ wildcard@^2.0.0:
resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67"
integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==
workbox-background-sync@6.6.1, workbox-background-sync@^6.5.4:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-6.6.1.tgz#08d603a33717ce663e718c30cc336f74909aff2f"
integrity sha512-trJd3ovpWCvzu4sW0E8rV3FUyIcC0W8G+AZ+VcqzzA890AsWZlUGOTSxIMmIHVusUw/FDq1HFWfy/kC/WTRqSg==
workbox-background-sync@7.0.0, workbox-background-sync@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-7.0.0.tgz#2b84b96ca35fec976e3bd2794b70e4acec46b3a5"
integrity sha512-S+m1+84gjdueM+jIKZ+I0Lx0BDHkk5Nu6a3kTVxP4fdj3gKouRNmhO8H290ybnJTOPfBDtTMXSQA/QLTvr7PeA==
dependencies:
idb "^7.0.1"
workbox-core "6.6.1"
workbox-core "7.0.0"
workbox-background-sync@^5.1.4:
version "5.1.4"
@ -11863,12 +11863,12 @@ workbox-background-sync@^5.1.4:
dependencies:
workbox-core "^5.1.4"
workbox-broadcast-update@6.6.1:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-6.6.1.tgz#0fad9454cf8e4ace0c293e5617c64c75d8a8c61e"
integrity sha512-fBhffRdaANdeQ1V8s692R9l/gzvjjRtydBOvR6WCSB0BNE2BacA29Z4r9/RHd9KaXCPl6JTdI9q0bR25YKP8TQ==
workbox-broadcast-update@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-7.0.0.tgz#7f611ca1a94ba8ac0aa40fa171c9713e0f937d22"
integrity sha512-oUuh4jzZrLySOo0tC0WoKiSg90bVAcnE98uW7F8GFiSOXnhogfNDGZelPJa+6KpGBO5+Qelv04Hqx2UD+BJqNQ==
dependencies:
workbox-core "6.6.1"
workbox-core "7.0.0"
workbox-broadcast-update@^5.1.4:
version "5.1.4"
@ -11877,10 +11877,10 @@ workbox-broadcast-update@^5.1.4:
dependencies:
workbox-core "^5.1.4"
workbox-build@6.6.1:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-6.6.1.tgz#6010e9ce550910156761448f2dbea8cfcf759cb0"
integrity sha512-INPgDx6aRycAugUixbKgiEQBWD0MPZqU5r0jyr24CehvNuLPSXp/wGOpdRJmts656lNiXwqV7dC2nzyrzWEDnw==
workbox-build@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-7.0.0.tgz#02ab5ef2991b3369b8b9395703f08912212769b4"
integrity sha512-CttE7WCYW9sZC+nUYhQg3WzzGPr4IHmrPnjKiu3AMXsiNQKx+l4hHl63WTrnicLmKEKHScWDH8xsGBdrYgtBzg==
dependencies:
"@apideck/better-ajv-errors" "^0.3.1"
"@babel/core" "^7.11.1"
@ -11904,21 +11904,21 @@ workbox-build@6.6.1:
strip-comments "^2.0.1"
tempy "^0.6.0"
upath "^1.2.0"
workbox-background-sync "6.6.1"
workbox-broadcast-update "6.6.1"
workbox-cacheable-response "6.6.1"
workbox-core "6.6.1"
workbox-expiration "6.6.1"
workbox-google-analytics "6.6.1"
workbox-navigation-preload "6.6.1"
workbox-precaching "6.6.1"
workbox-range-requests "6.6.1"
workbox-recipes "6.6.1"
workbox-routing "6.6.1"
workbox-strategies "6.6.1"
workbox-streams "6.6.1"
workbox-sw "6.6.1"
workbox-window "6.6.1"
workbox-background-sync "7.0.0"
workbox-broadcast-update "7.0.0"
workbox-cacheable-response "7.0.0"
workbox-core "7.0.0"
workbox-expiration "7.0.0"
workbox-google-analytics "7.0.0"
workbox-navigation-preload "7.0.0"
workbox-precaching "7.0.0"
workbox-range-requests "7.0.0"
workbox-recipes "7.0.0"
workbox-routing "7.0.0"
workbox-strategies "7.0.0"
workbox-streams "7.0.0"
workbox-sw "7.0.0"
workbox-window "7.0.0"
workbox-build@^5.1.4:
version "5.1.4"
@ -11962,12 +11962,12 @@ workbox-build@^5.1.4:
workbox-sw "^5.1.4"
workbox-window "^5.1.4"
workbox-cacheable-response@6.6.1:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-6.6.1.tgz#284c2b86be3f4fd191970ace8c8e99797bcf58e9"
integrity sha512-85LY4veT2CnTCDxaVG7ft3NKaFbH6i4urZXgLiU4AiwvKqS2ChL6/eILiGRYXfZ6gAwDnh5RkuDbr/GMS4KSag==
workbox-cacheable-response@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-7.0.0.tgz#ee27c036728189eed69d25a135013053277482d2"
integrity sha512-0lrtyGHn/LH8kKAJVOQfSu3/80WDc9Ma8ng0p2i/5HuUndGttH+mGMSvOskjOdFImLs2XZIimErp7tSOPmu/6g==
dependencies:
workbox-core "6.6.1"
workbox-core "7.0.0"
workbox-cacheable-response@^5.1.4:
version "5.1.4"
@ -11981,18 +11981,23 @@ workbox-core@6.6.1:
resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-6.6.1.tgz#7184776d4134c5ed2f086878c882728fc9084265"
integrity sha512-ZrGBXjjaJLqzVothoE12qTbVnOAjFrHDXpZe7coCb6q65qI/59rDLwuFMO4PcZ7jcbxY+0+NhUVztzR/CbjEFw==
workbox-core@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-7.0.0.tgz#dec114ec923cc2adc967dd9be1b8a0bed50a3545"
integrity sha512-81JkAAZtfVP8darBpfRTovHg8DGAVrKFgHpOArZbdFd78VqHr5Iw65f2guwjE2NlCFbPFDoez3D3/6ZvhI/rwQ==
workbox-core@^5.1.4:
version "5.1.4"
resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-5.1.4.tgz#8bbfb2362ecdff30e25d123c82c79ac65d9264f4"
integrity sha512-+4iRQan/1D8I81nR2L5vcbaaFskZC2CL17TLbvWVzQ4qiF/ytOGF6XeV54pVxAvKUtkLANhk8TyIUMtiMw2oDg==
workbox-expiration@6.6.1, workbox-expiration@^6.5.4:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-6.6.1.tgz#a841fa36676104426dbfb9da1ef6a630b4f93739"
integrity sha512-qFiNeeINndiOxaCrd2DeL1Xh1RFug3JonzjxUHc5WkvkD2u5abY3gZL1xSUNt3vZKsFFGGORItSjVTVnWAZO4A==
workbox-expiration@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-7.0.0.tgz#3d90bcf2a7577241de950f89784f6546b66c2baa"
integrity sha512-MLK+fogW+pC3IWU9SFE+FRStvDVutwJMR5if1g7oBJx3qwmO69BNoJQVaMXq41R0gg3MzxVfwOGKx3i9P6sOLQ==
dependencies:
idb "^7.0.1"
workbox-core "6.6.1"
workbox-core "7.0.0"
workbox-expiration@^5.1.4:
version "5.1.4"
@ -12001,15 +12006,23 @@ workbox-expiration@^5.1.4:
dependencies:
workbox-core "^5.1.4"
workbox-google-analytics@6.6.1:
workbox-expiration@^6.5.4:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-6.6.1.tgz#a07a6655ab33d89d1b0b0a935ffa5dea88618c5d"
integrity sha512-1TjSvbFSLmkpqLcBsF7FuGqqeDsf+uAXO/pjiINQKg3b1GN0nBngnxLcXDYo1n/XxK4N7RaRrpRlkwjY/3ocuA==
resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-6.6.1.tgz#a841fa36676104426dbfb9da1ef6a630b4f93739"
integrity sha512-qFiNeeINndiOxaCrd2DeL1Xh1RFug3JonzjxUHc5WkvkD2u5abY3gZL1xSUNt3vZKsFFGGORItSjVTVnWAZO4A==
dependencies:
workbox-background-sync "6.6.1"
idb "^7.0.1"
workbox-core "6.6.1"
workbox-routing "6.6.1"
workbox-strategies "6.6.1"
workbox-google-analytics@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-7.0.0.tgz#603b2c4244af1e85de0fb26287d4e17d3293452a"
integrity sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==
dependencies:
workbox-background-sync "7.0.0"
workbox-core "7.0.0"
workbox-routing "7.0.0"
workbox-strategies "7.0.0"
workbox-google-analytics@^5.1.4:
version "5.1.4"
@ -12021,12 +12034,12 @@ workbox-google-analytics@^5.1.4:
workbox-routing "^5.1.4"
workbox-strategies "^5.1.4"
workbox-navigation-preload@6.6.1, workbox-navigation-preload@^6.5.4:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-6.6.1.tgz#61a34fe125558dd88cf09237f11bd966504ea059"
integrity sha512-DQCZowCecO+wRoIxJI2V6bXWK6/53ff+hEXLGlQL4Rp9ZaPDLrgV/32nxwWIP7QpWDkVEtllTAK5h6cnhxNxDA==
workbox-navigation-preload@7.0.0, workbox-navigation-preload@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-7.0.0.tgz#4913878dbbd97057181d57baa18d2bbdde085c6c"
integrity sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==
dependencies:
workbox-core "6.6.1"
workbox-core "7.0.0"
workbox-navigation-preload@^5.1.4:
version "5.1.4"
@ -12035,14 +12048,14 @@ workbox-navigation-preload@^5.1.4:
dependencies:
workbox-core "^5.1.4"
workbox-precaching@6.6.1, workbox-precaching@^6.5.4:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-6.6.1.tgz#dedeeba10a2d163d990bf99f1c2066ac0d1a19e2"
integrity sha512-K4znSJ7IKxCnCYEdhNkMr7X1kNh8cz+mFgx9v5jFdz1MfI84pq8C2zG+oAoeE5kFrUf7YkT5x4uLWBNg0DVZ5A==
workbox-precaching@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-7.0.0.tgz#3979ba8033aadf3144b70e9fe631d870d5fbaa03"
integrity sha512-EC0vol623LJqTJo1mkhD9DZmMP604vHqni3EohhQVwhJlTgyKyOkMrZNy5/QHfOby+39xqC01gv4LjOm4HSfnA==
dependencies:
workbox-core "6.6.1"
workbox-routing "6.6.1"
workbox-strategies "6.6.1"
workbox-core "7.0.0"
workbox-routing "7.0.0"
workbox-strategies "7.0.0"
workbox-precaching@^5.1.4:
version "5.1.4"
@ -12051,12 +12064,21 @@ workbox-precaching@^5.1.4:
dependencies:
workbox-core "^5.1.4"
workbox-range-requests@6.6.1:
workbox-precaching@^6.5.4:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-6.6.1.tgz#ddaf7e73af11d362fbb2f136a9063a4c7f507a39"
integrity sha512-4BDzk28govqzg2ZpX0IFkthdRmCKgAKreontYRC5YsAPB2jDtPNxqx3WtTXgHw1NZalXpcH/E4LqUa9+2xbv1g==
resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-6.6.1.tgz#dedeeba10a2d163d990bf99f1c2066ac0d1a19e2"
integrity sha512-K4znSJ7IKxCnCYEdhNkMr7X1kNh8cz+mFgx9v5jFdz1MfI84pq8C2zG+oAoeE5kFrUf7YkT5x4uLWBNg0DVZ5A==
dependencies:
workbox-core "6.6.1"
workbox-routing "6.6.1"
workbox-strategies "6.6.1"
workbox-range-requests@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-7.0.0.tgz#97511901e043df27c1aa422adcc999a7751f52ed"
integrity sha512-SxAzoVl9j/zRU9OT5+IQs7pbJBOUOlriB8Gn9YMvi38BNZRbM+RvkujHMo8FOe9IWrqqwYgDFBfv6sk76I1yaQ==
dependencies:
workbox-core "7.0.0"
workbox-range-requests@^5.1.4:
version "5.1.4"
@ -12065,25 +12087,32 @@ workbox-range-requests@^5.1.4:
dependencies:
workbox-core "^5.1.4"
workbox-recipes@6.6.1:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-recipes/-/workbox-recipes-6.6.1.tgz#ea70d2b2b0b0bce8de0a9d94f274d4a688e69fae"
integrity sha512-/oy8vCSzromXokDA+X+VgpeZJvtuf8SkQ8KL0xmRivMgJZrjwM3c2tpKTJn6PZA6TsbxGs3Sc7KwMoZVamcV2g==
workbox-recipes@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-recipes/-/workbox-recipes-7.0.0.tgz#1a6a01c8c2dfe5a41eef0fed3fe517e8a45c6514"
integrity sha512-DntcK9wuG3rYQOONWC0PejxYYIDHyWWZB/ueTbOUDQgefaeIj1kJ7pdP3LZV2lfrj8XXXBWt+JDRSw1lLLOnww==
dependencies:
workbox-cacheable-response "6.6.1"
workbox-core "6.6.1"
workbox-expiration "6.6.1"
workbox-precaching "6.6.1"
workbox-routing "6.6.1"
workbox-strategies "6.6.1"
workbox-cacheable-response "7.0.0"
workbox-core "7.0.0"
workbox-expiration "7.0.0"
workbox-precaching "7.0.0"
workbox-routing "7.0.0"
workbox-strategies "7.0.0"
workbox-routing@6.6.1, workbox-routing@^6.5.4:
workbox-routing@6.6.1:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-6.6.1.tgz#cba9a1c7e0d1ea11e24b6f8c518840efdc94f581"
integrity sha512-j4ohlQvfpVdoR8vDYxTY9rA9VvxTHogkIDwGdJ+rb2VRZQ5vt1CWwUUZBeD/WGFAni12jD1HlMXvJ8JS7aBWTg==
dependencies:
workbox-core "6.6.1"
workbox-routing@7.0.0, workbox-routing@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-7.0.0.tgz#6668438a06554f60645aedc77244a4fe3a91e302"
integrity sha512-8YxLr3xvqidnbVeGyRGkaV4YdlKkn5qZ1LfEePW3dq+ydE73hUUJJuLmGEykW3fMX8x8mNdL0XrWgotcuZjIvA==
dependencies:
workbox-core "7.0.0"
workbox-routing@^5.1.4:
version "5.1.4"
resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-5.1.4.tgz#3e8cd86bd3b6573488d1a2ce7385e547b547e970"
@ -12098,6 +12127,13 @@ workbox-strategies@6.6.1, workbox-strategies@^6.2.4:
dependencies:
workbox-core "6.6.1"
workbox-strategies@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-7.0.0.tgz#dcba32b3f3074476019049cc490fe1a60ea73382"
integrity sha512-dg3qJU7tR/Gcd/XXOOo7x9QoCI9nk74JopaJaYAQ+ugLi57gPsXycVdBnYbayVj34m6Y8ppPwIuecrzkpBVwbA==
dependencies:
workbox-core "7.0.0"
workbox-strategies@^5.1.4:
version "5.1.4"
resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-5.1.4.tgz#96b1418ccdfde5354612914964074d466c52d08c"
@ -12106,13 +12142,13 @@ workbox-strategies@^5.1.4:
workbox-core "^5.1.4"
workbox-routing "^5.1.4"
workbox-streams@6.6.1:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-6.6.1.tgz#b2f7ba7b315c27a6e3a96a476593f99c5d227d26"
integrity sha512-maKG65FUq9e4BLotSKWSTzeF0sgctQdYyTMq529piEN24Dlu9b6WhrAfRpHdCncRS89Zi2QVpW5V33NX8PgH3Q==
workbox-streams@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-7.0.0.tgz#36722aecd04785f88b6f709e541c094fc658c0f9"
integrity sha512-moVsh+5to//l6IERWceYKGiftc+prNnqOp2sgALJJFbnNVpTXzKISlTIsrWY+ogMqt+x1oMazIdHj25kBSq/HQ==
dependencies:
workbox-core "6.6.1"
workbox-routing "6.6.1"
workbox-core "7.0.0"
workbox-routing "7.0.0"
workbox-streams@^5.1.4:
version "5.1.4"
@ -12122,10 +12158,10 @@ workbox-streams@^5.1.4:
workbox-core "^5.1.4"
workbox-routing "^5.1.4"
workbox-sw@6.6.1:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-6.6.1.tgz#d4c4ca3125088e8b9fd7a748ed537fa0247bd72c"
integrity sha512-R7whwjvU2abHH/lR6kQTTXLHDFU2izht9kJOvBRYK65FbwutT4VvnUAJIgHvfWZ/fokrOPhfoWYoPCMpSgUKHQ==
workbox-sw@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-7.0.0.tgz#7350126411e3de1409f7ec243df8d06bb5b08b86"
integrity sha512-SWfEouQfjRiZ7GNABzHUKUyj8pCoe+RwjfOIajcx6J5mtgKkN+t8UToHnpaJL5UVVOf5YhJh+OHhbVNIHe+LVA==
workbox-sw@^5.1.4:
version "5.1.4"
@ -12144,24 +12180,24 @@ workbox-webpack-plugin@^5.1.3, workbox-webpack-plugin@^6.1.0:
webpack-sources "^1.3.0"
workbox-build "^5.1.4"
workbox-webpack-plugin@^6.5.4:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-webpack-plugin/-/workbox-webpack-plugin-6.6.1.tgz#4f81cc1ad4e5d2cd7477a86ba83c84ee2d187531"
integrity sha512-zpZ+ExFj9NmiI66cFEApyjk7hGsfJ1YMOaLXGXBoZf0v7Iu6hL0ZBe+83mnDq3YYWAfA3fnyFejritjOHkFcrA==
workbox-webpack-plugin@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-webpack-plugin/-/workbox-webpack-plugin-7.0.0.tgz#6c61661a2cacde1239192a5877a041a2943d1a55"
integrity sha512-R1ZzCHPfzeJjLK2/TpKUhxSQ3fFDCxlWxgRhhSjMQLz3G2MlBnyw/XeYb34e7SGgSv0qG22zEhMIzjMNqNeKbw==
dependencies:
fast-json-stable-stringify "^2.1.0"
pretty-bytes "^5.4.1"
upath "^1.2.0"
webpack-sources "^1.4.3"
workbox-build "6.6.1"
workbox-build "7.0.0"
workbox-window@6.6.1, workbox-window@^6.5.4:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-6.6.1.tgz#f22a394cbac36240d0dadcbdebc35f711bb7b89e"
integrity sha512-wil4nwOY58nTdCvif/KEZjQ2NP8uk3gGeRNy2jPBbzypU4BT4D9L8xiwbmDBpZlSgJd2xsT9FvSNU0gsxV51JQ==
workbox-window@7.0.0, workbox-window@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-7.0.0.tgz#a683ab33c896e4f16786794eac7978fc98a25d08"
integrity sha512-j7P/bsAWE/a7sxqTzXo3P2ALb1reTfZdvVp6OJ/uLr/C2kZAMvjeWGm8V4htQhor7DOvYg0sSbFN2+flT5U0qA==
dependencies:
"@types/trusted-types" "^2.0.2"
workbox-core "6.6.1"
workbox-core "7.0.0"
workbox-window@^5.1.4:
version "5.1.4"