mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2025-12-30 13:40:01 -05:00
Compare commits
59 Commits
2.0.0-beta
...
2.0.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad6fe5fa4d | ||
|
|
034d59373f | ||
|
|
d1ad0ade0f | ||
|
|
991089c17a | ||
|
|
54960d8480 | ||
|
|
5fcfe09bb6 | ||
|
|
01c4974507 | ||
|
|
2d57e0dab2 | ||
|
|
d52e5408c0 | ||
|
|
fdce69daf4 | ||
|
|
cb3ffcb12d | ||
|
|
d7342a349b | ||
|
|
794bbed833 | ||
|
|
0b335e80a6 | ||
|
|
2716d72e31 | ||
|
|
ac31c112f3 | ||
|
|
8c849a1077 | ||
|
|
8c1769458d | ||
|
|
2ac6451370 | ||
|
|
7841397b59 | ||
|
|
cd11194ce5 | ||
|
|
be7558f82b | ||
|
|
35a7875f6f | ||
|
|
55f1f834c2 | ||
|
|
f5f32912b1 | ||
|
|
5709435d43 | ||
|
|
1c219dbc3b | ||
|
|
1262982588 | ||
|
|
be8a340a0c | ||
|
|
fb1de15de6 | ||
|
|
2180f11768 | ||
|
|
1083b7521e | ||
|
|
0104b600cc | ||
|
|
70d40f9e70 | ||
|
|
1094cf2d92 | ||
|
|
aaf6e0f197 | ||
|
|
ec59cd6e4f | ||
|
|
5a0a5b09a1 | ||
|
|
e698d14ec3 | ||
|
|
0caf2fe77f | ||
|
|
c079f49d71 | ||
|
|
8490ac01cc | ||
|
|
84477ef52a | ||
|
|
b789573de3 | ||
|
|
d5d8e7ce63 | ||
|
|
c7a49458b9 | ||
|
|
7baad85112 | ||
|
|
4b0bfa9a85 | ||
|
|
5e7c75ef68 | ||
|
|
954a35bea2 | ||
|
|
88347d44c8 | ||
|
|
2c13e76fbb | ||
|
|
362f634828 | ||
|
|
2fb968cfd3 | ||
|
|
4d3dab6edd | ||
|
|
8f1b593ad1 | ||
|
|
1002f0d61f | ||
|
|
20cb218688 | ||
|
|
bba44b0c1e |
@@ -3,6 +3,11 @@ FROM python:3.10-alpine3.18
|
||||
#Install all dependencies.
|
||||
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev openldap git yarn
|
||||
|
||||
# Fix libxml error from xmlsec https://github.com/xmlsec/python-xmlsec/issues/257#issuecomment-1738620862
|
||||
RUN echo "https://dl-cdn.alpinelinux.org/alpine/v3.15/community/" | tee -a /etc/apk/repositories
|
||||
RUN echo "https://dl-cdn.alpinelinux.org/alpine/v3.15/main" | tee -a /etc/apk/repositories
|
||||
RUN apk add --no-cache libxml2-dev=2.9.14-r2 xmlsec-dev=1.2.33-r0
|
||||
|
||||
#Print all logs without buffering it.
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
|
||||
@@ -29,3 +29,4 @@ vue/babel.config*
|
||||
vue/package.json
|
||||
vue/tsconfig.json
|
||||
vue/src/utils/openapi
|
||||
venv
|
||||
1
boot.sh
1
boot.sh
@@ -84,7 +84,6 @@ python manage.py migrate
|
||||
|
||||
echo "Collecting static files, this may take a while..."
|
||||
|
||||
python manage.py collectstatic_js_reverse
|
||||
python manage.py collectstatic --noinput
|
||||
|
||||
echo "Done"
|
||||
|
||||
@@ -109,7 +109,7 @@ class AutomationEngine:
|
||||
Moves a string that should never be treated as a unit to next token and optionally replaced with default unit
|
||||
e.g. NEVER_UNIT: param1: egg, param2: None would modify ['1', 'egg', 'white'] to ['1', '', 'egg', 'white']
|
||||
or NEVER_UNIT: param1: egg, param2: pcs would modify ['1', 'egg', 'yolk'] to ['1', 'pcs', 'egg', 'yolk']
|
||||
:param1 string: string that should never be considered a unit, will be moved to token[2]
|
||||
:param1 tokens: string that should never be considered a unit, will be moved to token[2]
|
||||
:param2 (optional) unit as string: will insert unit string into token[1]
|
||||
:return: unit as string (possibly changed by automation)
|
||||
"""
|
||||
@@ -135,7 +135,7 @@ class AutomationEngine:
|
||||
new_unit = self.never_unit[tokens[1].lower()]
|
||||
never_unit = True
|
||||
except KeyError:
|
||||
return tokens
|
||||
return tokens, never_unit
|
||||
else:
|
||||
if a := Automation.objects.annotate(param_1_lower=Lower('param_1')).filter(space=self.request.space, type=Automation.NEVER_UNIT, param_1_lower__in=[
|
||||
tokens[1].lower(), alt_unit.lower()], disabled=False).order_by('order').first():
|
||||
@@ -144,7 +144,7 @@ class AutomationEngine:
|
||||
|
||||
if never_unit:
|
||||
tokens.insert(1, new_unit)
|
||||
return tokens
|
||||
return tokens, never_unit
|
||||
|
||||
def apply_transpose_automation(self, string):
|
||||
"""
|
||||
|
||||
@@ -84,7 +84,6 @@ def handle_image(request, image_object, filetype):
|
||||
if filetype == '.png':
|
||||
return rescale_image_png(image_object)
|
||||
else:
|
||||
print('STripping image')
|
||||
return strip_image_meta(image_object, file_format)
|
||||
|
||||
# TODO webp and gifs bypass the scaling and metadata checks, fix
|
||||
|
||||
@@ -212,38 +212,43 @@ class IngredientParser:
|
||||
# a fraction for the amount
|
||||
if len(tokens) > 2:
|
||||
if not self.ignore_rules:
|
||||
tokens = self.automation.apply_never_unit_automation(tokens)
|
||||
try:
|
||||
if unit is not None:
|
||||
# a unit is already found, no need to try the second argument for a fraction
|
||||
# probably not the best method to do it, but I didn't want to make an if check and paste the exact same thing in the else as already is in the except
|
||||
raise ValueError
|
||||
# try to parse second argument as amount and add that, in case of '2 1/2' or '2 ½'
|
||||
amount += self.parse_fraction(tokens[1])
|
||||
# assume that units can't end with a comma
|
||||
if len(tokens) > 3 and not tokens[2].endswith(','):
|
||||
# try to use third argument as unit and everything else as food, use everything as food if it fails
|
||||
try:
|
||||
food, note = self.parse_food(tokens[3:])
|
||||
unit = tokens[2]
|
||||
except ValueError:
|
||||
food, note = self.parse_food(tokens[2:])
|
||||
else:
|
||||
tokens, never_unit_applied = self.automation.apply_never_unit_automation(tokens)
|
||||
if never_unit_applied:
|
||||
unit = tokens[1]
|
||||
food, note = self.parse_food(tokens[2:])
|
||||
except ValueError:
|
||||
# assume that units can't end with a comma
|
||||
if not tokens[1].endswith(','):
|
||||
# try to use second argument as unit and everything else as food, use everything as food if it fails
|
||||
try:
|
||||
food, note = self.parse_food(tokens[2:])
|
||||
if unit is None:
|
||||
unit = tokens[1]
|
||||
else:
|
||||
note = tokens[1]
|
||||
except ValueError:
|
||||
food, note = self.parse_food(tokens[1:])
|
||||
else:
|
||||
food, note = self.parse_food(tokens[1:])
|
||||
try:
|
||||
if unit is not None:
|
||||
# a unit is already found, no need to try the second argument for a fraction
|
||||
# probably not the best method to do it, but I didn't want to make an if check and paste the exact same thing in the else as already is in the except
|
||||
raise ValueError
|
||||
# try to parse second argument as amount and add that, in case of '2 1/2' or '2 ½'
|
||||
if tokens[1]:
|
||||
amount += self.parse_fraction(tokens[1])
|
||||
# assume that units can't end with a comma
|
||||
if len(tokens) > 3 and not tokens[2].endswith(','):
|
||||
# try to use third argument as unit and everything else as food, use everything as food if it fails
|
||||
try:
|
||||
food, note = self.parse_food(tokens[3:])
|
||||
unit = tokens[2]
|
||||
except ValueError:
|
||||
food, note = self.parse_food(tokens[2:])
|
||||
else:
|
||||
food, note = self.parse_food(tokens[2:])
|
||||
except ValueError:
|
||||
# assume that units can't end with a comma
|
||||
if not tokens[1].endswith(','):
|
||||
# try to use second argument as unit and everything else as food, use everything as food if it fails
|
||||
try:
|
||||
food, note = self.parse_food(tokens[2:])
|
||||
if unit is None:
|
||||
unit = tokens[1]
|
||||
else:
|
||||
note = tokens[1]
|
||||
except ValueError:
|
||||
food, note = self.parse_food(tokens[1:])
|
||||
else:
|
||||
food, note = self.parse_food(tokens[1:])
|
||||
else:
|
||||
# only two arguments, first one is the amount
|
||||
# which means this is the food
|
||||
@@ -276,4 +281,6 @@ class IngredientParser:
|
||||
if len(food.strip()) == 0:
|
||||
raise ValueError(f'Error parsing string {ingredient}, food cannot be empty')
|
||||
|
||||
print(f'parsed {ingredient} to {amount} - {unit} - {food} - {note}')
|
||||
|
||||
return amount, unit, food, note[:Ingredient._meta.get_field('note').max_length].strip()
|
||||
|
||||
@@ -12,7 +12,7 @@ class ScopeMiddleware:
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
prefix = settings.JS_REVERSE_SCRIPT_PREFIX or ''
|
||||
prefix = settings.SCRIPT_NAME or ''
|
||||
|
||||
# need to disable scopes for writing requests into userpref and enable for loading ?
|
||||
if request.path.startswith(prefix + '/api/user-preference/'):
|
||||
|
||||
@@ -60,14 +60,15 @@ class CookBookApp(Integration):
|
||||
food=f, unit=u, amount=ingredient.get('amount', None), note=ingredient.get('note', None), original_text=ingredient.get('original_text', None), space=self.request.space,
|
||||
))
|
||||
|
||||
if len(images) > 0:
|
||||
try:
|
||||
url = images[0]
|
||||
if validate_import_url(url):
|
||||
try:
|
||||
for url in images:
|
||||
# import the first valid image which is not cookbookapp branding
|
||||
if validate_import_url(url) and not url.startswith("https://media.cookbookmanager.com/brand/"):
|
||||
response = requests.get(url)
|
||||
self.import_recipe_image(recipe, BytesIO(response.content))
|
||||
except Exception as e:
|
||||
print('failed to import image ', str(e))
|
||||
break
|
||||
except Exception as e:
|
||||
print('failed to import image ', str(e))
|
||||
|
||||
recipe.save()
|
||||
return recipe
|
||||
|
||||
@@ -14,7 +14,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-08-01 15:04+0200\n"
|
||||
"PO-Revision-Date: 2025-01-29 13:44+0000\n"
|
||||
"PO-Revision-Date: 2025-06-23 08:28+0000\n"
|
||||
"Last-Translator: Ángel <1024mb@users.noreply.translate.tandoor.dev>\n"
|
||||
"Language-Team: Spanish <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/es/>\n"
|
||||
@@ -284,14 +284,12 @@ msgid "You have more users than allowed in your space."
|
||||
msgstr "Tenés mas usuarios que los permitidos en tu espacio"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:310
|
||||
#, fuzzy
|
||||
#| msgid "Use fractions"
|
||||
msgid "reverse rotation"
|
||||
msgstr "Usar fracciones"
|
||||
msgstr "rotación inversa"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:311
|
||||
msgid "careful rotation"
|
||||
msgstr ""
|
||||
msgstr "rotación cuidadosa"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:312
|
||||
msgid "knead"
|
||||
@@ -398,8 +396,9 @@ msgid "Section"
|
||||
msgstr "Sección"
|
||||
|
||||
#: .\cookbook\management\commands\fix_duplicate_properties.py:15
|
||||
#, fuzzy
|
||||
msgid "Fixes foods with "
|
||||
msgstr ""
|
||||
msgstr "Corrige alimentos con "
|
||||
|
||||
#: .\cookbook\management\commands\rebuildindex.py:14
|
||||
msgid "Rebuilds full text search index on Recipe"
|
||||
@@ -436,16 +435,14 @@ msgid "Other"
|
||||
msgstr "Otro"
|
||||
|
||||
#: .\cookbook\migrations\0190_auto_20230525_1506.py:17
|
||||
#, fuzzy
|
||||
#| msgid "Fats"
|
||||
msgid "Fat"
|
||||
msgstr "Grasas"
|
||||
msgstr "Grasa"
|
||||
|
||||
#: .\cookbook\migrations\0190_auto_20230525_1506.py:17
|
||||
#: .\cookbook\migrations\0190_auto_20230525_1506.py:18
|
||||
#: .\cookbook\migrations\0190_auto_20230525_1506.py:19
|
||||
msgid "g"
|
||||
msgstr ""
|
||||
msgstr "gr."
|
||||
|
||||
#: .\cookbook\migrations\0190_auto_20230525_1506.py:18
|
||||
msgid "Carbohydrates"
|
||||
@@ -468,6 +465,8 @@ msgid ""
|
||||
"Maximum file storage for space in MB. 0 for unlimited, -1 to disable file "
|
||||
"upload."
|
||||
msgstr ""
|
||||
"Almacenamiento máximo de archivos para el espacio en MB. 0 para ilimitado, -"
|
||||
"1 para desactivar la carga de archivos."
|
||||
|
||||
#: .\cookbook\models.py:454 .\cookbook\templates\search.html:7
|
||||
#: .\cookbook\templates\settings.html:18
|
||||
@@ -498,18 +497,16 @@ msgid "Nutrition"
|
||||
msgstr "Información Nutricional"
|
||||
|
||||
#: .\cookbook\models.py:918
|
||||
#, fuzzy
|
||||
#| msgid "Merge"
|
||||
msgid "Allergen"
|
||||
msgstr "Combinar"
|
||||
msgstr "Alérgeno"
|
||||
|
||||
#: .\cookbook\models.py:919
|
||||
msgid "Price"
|
||||
msgstr ""
|
||||
msgstr "Precio"
|
||||
|
||||
#: .\cookbook\models.py:919
|
||||
msgid "Goal"
|
||||
msgstr ""
|
||||
msgstr "Objetivo"
|
||||
|
||||
#: .\cookbook\models.py:1408 .\cookbook\templates\search_info.html:28
|
||||
msgid "Simple"
|
||||
@@ -532,54 +529,40 @@ msgid "Food Alias"
|
||||
msgstr "Alias de la Comida"
|
||||
|
||||
#: .\cookbook\models.py:1468
|
||||
#, fuzzy
|
||||
#| msgid "Units"
|
||||
msgid "Unit Alias"
|
||||
msgstr "Unidades"
|
||||
msgstr "Alias de unidad"
|
||||
|
||||
#: .\cookbook\models.py:1469
|
||||
#, fuzzy
|
||||
#| msgid "Keywords"
|
||||
msgid "Keyword Alias"
|
||||
msgstr "Palabras clave"
|
||||
msgstr "Alias de palabra clave"
|
||||
|
||||
#: .\cookbook\models.py:1470
|
||||
#, fuzzy
|
||||
#| msgid "Description"
|
||||
msgid "Description Replace"
|
||||
msgstr "Descripción"
|
||||
msgstr "Reemplazo de descripción"
|
||||
|
||||
#: .\cookbook\models.py:1471
|
||||
#, fuzzy
|
||||
#| msgid "Instructions"
|
||||
msgid "Instruction Replace"
|
||||
msgstr "Instrucciones"
|
||||
msgstr "Reemplazo de instrucciones"
|
||||
|
||||
#: .\cookbook\models.py:1472
|
||||
#, fuzzy
|
||||
#| msgid "New Unit"
|
||||
msgid "Never Unit"
|
||||
msgstr "Nueva Unidad"
|
||||
msgstr "Unidad prohibida"
|
||||
|
||||
#: .\cookbook\models.py:1473
|
||||
msgid "Transpose Words"
|
||||
msgstr ""
|
||||
msgstr "Transponer palabras"
|
||||
|
||||
#: .\cookbook\models.py:1474
|
||||
#, fuzzy
|
||||
#| msgid "Food Alias"
|
||||
msgid "Food Replace"
|
||||
msgstr "Alias de la Comida"
|
||||
msgstr "Reemplazo de alimento"
|
||||
|
||||
#: .\cookbook\models.py:1475
|
||||
#, fuzzy
|
||||
#| msgid "Description"
|
||||
msgid "Unit Replace"
|
||||
msgstr "Descripción"
|
||||
msgstr "Reemplazo de unidad"
|
||||
|
||||
#: .\cookbook\models.py:1476
|
||||
msgid "Name Replace"
|
||||
msgstr ""
|
||||
msgstr "Reemplazo de nombre"
|
||||
|
||||
#: .\cookbook\models.py:1503 .\cookbook\views\delete.py:40
|
||||
#: .\cookbook\views\edit.py:210 .\cookbook\views\new.py:39
|
||||
@@ -587,10 +570,8 @@ msgid "Recipe"
|
||||
msgstr "Receta"
|
||||
|
||||
#: .\cookbook\models.py:1504
|
||||
#, fuzzy
|
||||
#| msgid "Food"
|
||||
msgid "Food"
|
||||
msgstr "Comida"
|
||||
msgstr "Alimento"
|
||||
|
||||
#: .\cookbook\models.py:1505 .\cookbook\templates\base.html:149
|
||||
msgid "Keyword"
|
||||
@@ -648,22 +629,26 @@ msgstr "Invitación para Tandoor Recipes"
|
||||
|
||||
#: .\cookbook\serializer.py:1426
|
||||
msgid "Existing shopping list to update"
|
||||
msgstr ""
|
||||
msgstr "Lista de compras existente para actualizar"
|
||||
|
||||
#: .\cookbook\serializer.py:1428
|
||||
msgid ""
|
||||
"List of ingredient IDs from the recipe to add, if not provided all "
|
||||
"ingredients will be added."
|
||||
msgstr ""
|
||||
"Lista de IDs de ingredientes de la receta para agregar; si no se "
|
||||
"proporciona, se agregarán todos los ingredientes."
|
||||
|
||||
#: .\cookbook\serializer.py:1430
|
||||
msgid ""
|
||||
"Providing a list_recipe ID and servings of 0 will delete that shopping list."
|
||||
msgstr ""
|
||||
"Proporcionar un ID list_recipe y porciones igual a 0 eliminará esa lista de "
|
||||
"compras."
|
||||
|
||||
#: .\cookbook\serializer.py:1439
|
||||
msgid "Amount of food to add to the shopping list"
|
||||
msgstr ""
|
||||
msgstr "Cantidad de alimento a agregar a la lista de compras"
|
||||
|
||||
#: .\cookbook\serializer.py:1441
|
||||
msgid "ID of unit to use for the shopping list"
|
||||
|
||||
@@ -41,15 +41,6 @@
|
||||
<script src="{% static 'js/popper.min.js' %}"></script>
|
||||
<script src="{% static 'js/bootstrap.min.js' %}"></script>
|
||||
|
||||
<!-- Select2 for use with django autocomplete light -->
|
||||
<link href="{% static 'css/select2.min.css' %}" rel="stylesheet"/>
|
||||
<script src="{% static 'js/select2.min.js' %}"></script>
|
||||
|
||||
<!-- Bootstrap theme for select2 -->
|
||||
<link rel="stylesheet" href="{% static 'css/select2-bootstrap.css' %}"/>
|
||||
|
||||
<link rel="stylesheet" href="{% static 'themes/select2-bootstrap-theme.css' %}"/>
|
||||
|
||||
<!-- Fontawesome icons -->
|
||||
<link rel="stylesheet" href="{% static "fontawesome/fontawesome_all.min.css" %}">
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<div class="container">
|
||||
|
||||
|
||||
<h1 >{% trans 'System' %}</h1>
|
||||
<h1>{% trans 'System' %}</h1>
|
||||
{% blocktrans %}
|
||||
Tandoor Recipes is an open source free software application. It can be found on
|
||||
<a href="https://github.com/TandoorRecipes/recipes">GitHub</a>.
|
||||
@@ -213,6 +213,14 @@
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
<h4 class="mt-3">Cache Test</h4>
|
||||
On first load this should be None, on second load it should be the time of the first load. Expiration is set to 10 seconds after that it should be None again. <br/>
|
||||
{% if cache_response %}
|
||||
|
||||
<span class="badge text-bg-success">Cache entry from {{ cache_response|date:" d m Y H:i:s" }}</span>
|
||||
{% else %}
|
||||
<span class="badge text-bg-info">No cache entry before load</span>
|
||||
{% endif %}
|
||||
<h4 class="mt-3">Debug</h4>
|
||||
<textarea class="form-control" rows="20">
|
||||
Gunicorn Media: {{ gunicorn_media }}
|
||||
|
||||
@@ -1 +1,157 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><testsuites><testsuite name="pytest" errors="0" failures="0" skipped="0" tests="2" time="38.353" timestamp="2025-03-31T09:44:57.025358" hostname="vabene-pc"><testcase classname="cookbook.tests.other.test_recipe_full_text_search" name="test_search_count[found_recipe0-rating]" time="29.368" /><testcase classname="cookbook.tests.other.test_recipe_full_text_search" name="test_search_count[found_recipe1-timescooked]" time="29.371" /></testsuite></testsuites>
|
||||
<?xml version="1.0" encoding="utf-8"?><testsuites name="pytest tests"><testsuite name="pytest" errors="4" failures="0" skipped="0" tests="4" time="34.750" timestamp="2025-07-17T12:15:15.274960+02:00" hostname="DESKTOP-RM10LP5"><testcase classname="cookbook.tests.other.test_automations" name="test_never_unit_automation[arg0]" time="22.035"><error message="failed on setup with "TypeError: type 'Factory' is not subscriptable"">args = ()
|
||||
kwargs = {'request': <SubRequest 'space_1' for <Function test_never_unit_automation[arg0]>>}
|
||||
k = 'space_1__name'
|
||||
|
||||
@functools.wraps(fixture_function)
|
||||
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
||||
for k in set(kwargs.keys()) - function_args:
|
||||
del kwargs[k]
|
||||
> return fixture_function(*args, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
..\..\..\venv\Lib\site-packages\pytest_factoryboy\fixturegen.py:88:
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
..\..\..\venv\Lib\site-packages\pytest_factoryboy\fixturegen.py:49: in fn
|
||||
return function(*args, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
|
||||
request = <SubRequest 'space_1' for <Function test_never_unit_automation[arg0]>>
|
||||
factory_name = 'space_factory'
|
||||
|
||||
def model_fixture(request: SubRequest, factory_name: str) -> object:
|
||||
"""Model fixture implementation."""
|
||||
factoryboy_request: FactoryboyRequest = request.getfixturevalue("factoryboy_request")
|
||||
|
||||
# Try to evaluate as much post-generation dependencies as possible
|
||||
factoryboy_request.evaluate(request)
|
||||
|
||||
assert request.fixturename # NOTE: satisfy mypy
|
||||
fixture_name = request.fixturename
|
||||
prefix = "".join((fixture_name, SEPARATOR))
|
||||
|
||||
factory_class: type[Factory[object]] = request.getfixturevalue(factory_name)
|
||||
|
||||
# Create model fixture instance
|
||||
> NewFactory: type[Factory[object]] = cast(type[Factory[object]], type("Factory", (factory_class,), {}))
|
||||
^^^^^^^^^^^^^^^
|
||||
E TypeError: type 'Factory' is not subscriptable
|
||||
|
||||
..\..\..\venv\Lib\site-packages\pytest_factoryboy\fixture.py:360: TypeError</error></testcase><testcase classname="cookbook.tests.other.test_automations" name="test_never_unit_automation[arg2]" time="22.047"><error message="failed on setup with "TypeError: type 'Factory' is not subscriptable"">args = ()
|
||||
kwargs = {'request': <SubRequest 'space_1' for <Function test_never_unit_automation[arg2]>>}
|
||||
k = 'space_1__name'
|
||||
|
||||
@functools.wraps(fixture_function)
|
||||
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
||||
for k in set(kwargs.keys()) - function_args:
|
||||
del kwargs[k]
|
||||
> return fixture_function(*args, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
..\..\..\venv\Lib\site-packages\pytest_factoryboy\fixturegen.py:88:
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
..\..\..\venv\Lib\site-packages\pytest_factoryboy\fixturegen.py:49: in fn
|
||||
return function(*args, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
|
||||
request = <SubRequest 'space_1' for <Function test_never_unit_automation[arg2]>>
|
||||
factory_name = 'space_factory'
|
||||
|
||||
def model_fixture(request: SubRequest, factory_name: str) -> object:
|
||||
"""Model fixture implementation."""
|
||||
factoryboy_request: FactoryboyRequest = request.getfixturevalue("factoryboy_request")
|
||||
|
||||
# Try to evaluate as much post-generation dependencies as possible
|
||||
factoryboy_request.evaluate(request)
|
||||
|
||||
assert request.fixturename # NOTE: satisfy mypy
|
||||
fixture_name = request.fixturename
|
||||
prefix = "".join((fixture_name, SEPARATOR))
|
||||
|
||||
factory_class: type[Factory[object]] = request.getfixturevalue(factory_name)
|
||||
|
||||
# Create model fixture instance
|
||||
> NewFactory: type[Factory[object]] = cast(type[Factory[object]], type("Factory", (factory_class,), {}))
|
||||
^^^^^^^^^^^^^^^
|
||||
E TypeError: type 'Factory' is not subscriptable
|
||||
|
||||
..\..\..\venv\Lib\site-packages\pytest_factoryboy\fixture.py:360: TypeError</error></testcase><testcase classname="cookbook.tests.other.test_automations" name="test_never_unit_automation[arg3]" time="22.106"><error message="failed on setup with "TypeError: type 'Factory' is not subscriptable"">args = ()
|
||||
kwargs = {'request': <SubRequest 'space_1' for <Function test_never_unit_automation[arg3]>>}
|
||||
k = 'space_1__name'
|
||||
|
||||
@functools.wraps(fixture_function)
|
||||
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
||||
for k in set(kwargs.keys()) - function_args:
|
||||
del kwargs[k]
|
||||
> return fixture_function(*args, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
..\..\..\venv\Lib\site-packages\pytest_factoryboy\fixturegen.py:88:
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
..\..\..\venv\Lib\site-packages\pytest_factoryboy\fixturegen.py:49: in fn
|
||||
return function(*args, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
|
||||
request = <SubRequest 'space_1' for <Function test_never_unit_automation[arg3]>>
|
||||
factory_name = 'space_factory'
|
||||
|
||||
def model_fixture(request: SubRequest, factory_name: str) -> object:
|
||||
"""Model fixture implementation."""
|
||||
factoryboy_request: FactoryboyRequest = request.getfixturevalue("factoryboy_request")
|
||||
|
||||
# Try to evaluate as much post-generation dependencies as possible
|
||||
factoryboy_request.evaluate(request)
|
||||
|
||||
assert request.fixturename # NOTE: satisfy mypy
|
||||
fixture_name = request.fixturename
|
||||
prefix = "".join((fixture_name, SEPARATOR))
|
||||
|
||||
factory_class: type[Factory[object]] = request.getfixturevalue(factory_name)
|
||||
|
||||
# Create model fixture instance
|
||||
> NewFactory: type[Factory[object]] = cast(type[Factory[object]], type("Factory", (factory_class,), {}))
|
||||
^^^^^^^^^^^^^^^
|
||||
E TypeError: type 'Factory' is not subscriptable
|
||||
|
||||
..\..\..\venv\Lib\site-packages\pytest_factoryboy\fixture.py:360: TypeError</error></testcase><testcase classname="cookbook.tests.other.test_automations" name="test_never_unit_automation[arg1]" time="22.143"><error message="failed on setup with "TypeError: type 'Factory' is not subscriptable"">args = ()
|
||||
kwargs = {'request': <SubRequest 'space_1' for <Function test_never_unit_automation[arg1]>>}
|
||||
k = 'space_1__name'
|
||||
|
||||
@functools.wraps(fixture_function)
|
||||
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
||||
for k in set(kwargs.keys()) - function_args:
|
||||
del kwargs[k]
|
||||
> return fixture_function(*args, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
..\..\..\venv\Lib\site-packages\pytest_factoryboy\fixturegen.py:88:
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
..\..\..\venv\Lib\site-packages\pytest_factoryboy\fixturegen.py:49: in fn
|
||||
return function(*args, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
|
||||
request = <SubRequest 'space_1' for <Function test_never_unit_automation[arg1]>>
|
||||
factory_name = 'space_factory'
|
||||
|
||||
def model_fixture(request: SubRequest, factory_name: str) -> object:
|
||||
"""Model fixture implementation."""
|
||||
factoryboy_request: FactoryboyRequest = request.getfixturevalue("factoryboy_request")
|
||||
|
||||
# Try to evaluate as much post-generation dependencies as possible
|
||||
factoryboy_request.evaluate(request)
|
||||
|
||||
assert request.fixturename # NOTE: satisfy mypy
|
||||
fixture_name = request.fixturename
|
||||
prefix = "".join((fixture_name, SEPARATOR))
|
||||
|
||||
factory_class: type[Factory[object]] = request.getfixturevalue(factory_name)
|
||||
|
||||
# Create model fixture instance
|
||||
> NewFactory: type[Factory[object]] = cast(type[Factory[object]], type("Factory", (factory_class,), {}))
|
||||
^^^^^^^^^^^^^^^
|
||||
E TypeError: type 'Factory' is not subscriptable
|
||||
|
||||
..\..\..\venv\Lib\site-packages\pytest_factoryboy\fixture.py:360: TypeError</error></testcase></testsuite></testsuites>
|
||||
File diff suppressed because one or more lines are too long
@@ -91,7 +91,8 @@ def test_never_unit_automation(u1_s1, arg):
|
||||
|
||||
with scope(space=space):
|
||||
Automation.objects.get_or_create(name='never unit test', type=Automation.NEVER_UNIT, param_1='egg', param_2=arg[1], created_by=user, space=space)
|
||||
assert automation.apply_never_unit_automation(arg[0]) == arg[2]
|
||||
tokens, automation_applied = automation.apply_never_unit_automation(arg[0])
|
||||
assert tokens == arg[2]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source", [
|
||||
|
||||
@@ -120,7 +120,7 @@ urlpatterns = [
|
||||
path('api-token-auth/', CustomAuthToken.as_view()),
|
||||
|
||||
path('offline/', views.offline, name='view_offline'),
|
||||
path('service-worker.js', (TemplateView.as_view(template_name="sw.js", content_type='application/javascript')), name='service_worker'),
|
||||
#path('service-worker.js', (TemplateView.as_view(template_name="sw.js", content_type='application/javascript')), name='service_worker'),
|
||||
path('manifest.json', views.web_manifest, name='web_manifest'),
|
||||
|
||||
]
|
||||
|
||||
@@ -83,6 +83,8 @@ def get_integration(request, export_type):
|
||||
return Rezeptsuitede(request, export_type)
|
||||
if export_type == ImportExportBase.GOURMET:
|
||||
return Gourmet(request, export_type)
|
||||
|
||||
|
||||
@group_required('user')
|
||||
def export_file(request, pk):
|
||||
el = get_object_or_404(ExportLog, pk=pk, space=request.space)
|
||||
@@ -92,7 +94,7 @@ def export_file(request, pk):
|
||||
if cacheData is None:
|
||||
el.possibly_not_expired = False
|
||||
el.save()
|
||||
return render(request, 'export_response.html', {'pk': pk})
|
||||
return JsonResponse({'msg': 'Export Expired or not found'}, status=404)
|
||||
|
||||
response = HttpResponse(cacheData['file'], content_type='application/force-download')
|
||||
response['Content-Disposition'] = 'attachment; filename="' + cacheData['filename'] + '"'
|
||||
|
||||
@@ -12,6 +12,7 @@ from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.auth.password_validation import validate_password
|
||||
from django.core.cache import caches
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.management import call_command
|
||||
from django.db import models
|
||||
@@ -242,6 +243,10 @@ def system(request):
|
||||
space_stats.append(r.zscore(f'api:space-request-count:{d}', s))
|
||||
api_space_stats.append(space_stats)
|
||||
|
||||
cache_response = caches['default'].get(f'system_view_test_cache_entry', None)
|
||||
if not cache_response:
|
||||
caches['default'].set(f'system_view_test_cache_entry', datetime.now(), 10)
|
||||
|
||||
return render(
|
||||
request, 'system.html', {
|
||||
'gunicorn_media': settings.GUNICORN_MEDIA,
|
||||
@@ -256,6 +261,7 @@ def system(request):
|
||||
'orphans': orphans,
|
||||
'migration_info': migration_info,
|
||||
'missing_migration': missing_migration,
|
||||
'cache_response': cache_response,
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ Add a new language to the long list of existing translations.
|
||||
- Spanish
|
||||
- Swedish
|
||||
|
||||
See [here](/contribute/translations) for further information on how to contribute translation to Tandoor.
|
||||
See [here](/docs/contribute/translations) for further information on how to contribute translation to Tandoor.
|
||||
|
||||
## Issues and Feature Requests
|
||||
|
||||
@@ -46,12 +46,12 @@ Helping improve the documentation for Tandoor is one of the easiest ways to give
|
||||
You can write guides on how to install and configure Tandoor expanding our repository of non-standard configuations.
|
||||
Or you can write how-to guides using some of Tandoor's advanced features such as authentication or automation.
|
||||
|
||||
See [here](/contribute/documentation) for more information on how to add documentation to Tandoor.
|
||||
See [here](/docs/contribute/documentation) for more information on how to add documentation to Tandoor.
|
||||
|
||||
## Contributing Code
|
||||
|
||||
For the truly ambitious, you can help write code to fix issues, add additional features, or write your own scripts using
|
||||
Tandoor's extensive API and share your work with the community.
|
||||
|
||||
Before writing any code, please make sure that you review [contribution guidelines](/contribute/guidelines) and
|
||||
[VSCode](/contribute/vscode) or [PyCharm](/contribute/pycharm) specific configurations.
|
||||
Before writing any code, please make sure that you review [contribution guidelines](/docs/contribute/guidelines) and
|
||||
[VSCode](/docs/contribute/vscode) or [PyCharm](/docs/contribute/pycharm) specific configurations.
|
||||
|
||||
@@ -67,5 +67,5 @@ Generate the schema using `openapi-generator-cli generate -g typescript-fetch -i
|
||||
|
||||
## Install and Configuration
|
||||
|
||||
Instructions for [VSCode](/contribute/vscode)
|
||||
Instructions for [PyCharm](/contribute/pycharm)
|
||||
Instructions for [VSCode](/docs/contribute/vscode)
|
||||
Instructions for [PyCharm](/docs/contribute/pycharm)
|
||||
|
||||
@@ -35,7 +35,6 @@ docker-compose:
|
||||
environment:
|
||||
# all the other env
|
||||
- SCRIPT_NAME=/<sub path>
|
||||
- JS_REVERSE_SCRIPT_PREFIX=/<sub path>/
|
||||
- STATIC_URL=/<www path>/static/
|
||||
- MEDIA_URL=/<www path>/media/
|
||||
labels:
|
||||
@@ -100,7 +99,6 @@ and the relevant section from the docker-compose.yml:
|
||||
image: vabene1111/recipes
|
||||
environment:
|
||||
- SCRIPT_NAME=/tandoor
|
||||
- JS_REVERSE_SCRIPT_PREFIX=/tandoor
|
||||
- STATIC_URL=/tandoor/static/
|
||||
- MEDIA_URL=/tandoor/media/
|
||||
- GUNICORN_MEDIA=0
|
||||
|
||||
@@ -40,9 +40,6 @@ def extract_comma_list(env_key, default=None):
|
||||
load_dotenv()
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
SCRIPT_NAME = os.getenv('SCRIPT_NAME', '')
|
||||
# path for django_js_reverse to generate the javascript file containing all urls. Only done because the default command (collectstatic_js_reverse) fails to update the manifest
|
||||
JS_REVERSE_OUTPUT_PATH = os.path.join(BASE_DIR, "cookbook/static/django_js_reverse")
|
||||
JS_REVERSE_SCRIPT_PREFIX = os.getenv('JS_REVERSE_SCRIPT_PREFIX', SCRIPT_NAME)
|
||||
|
||||
STATIC_URL = os.getenv('STATIC_URL', '/static/')
|
||||
STATIC_ROOT = os.getenv('STATIC_ROOT', os.path.join(BASE_DIR, "staticfiles"))
|
||||
@@ -86,6 +83,10 @@ LOGGING = {
|
||||
'handlers': ['console'],
|
||||
'level': LOG_LEVEL,
|
||||
},
|
||||
'django': {
|
||||
'handlers': ['console'],
|
||||
'level': LOG_LEVEL,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -245,7 +246,6 @@ ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 90
|
||||
ACCOUNT_LOGOUT_ON_GET = True
|
||||
|
||||
USERSESSIONS_TRACK_ACTIVITY = True
|
||||
|
||||
HEADLESS_SERVE_SPECIFICATION = True
|
||||
|
||||
try:
|
||||
@@ -521,6 +521,16 @@ CACHES = {
|
||||
}
|
||||
}
|
||||
|
||||
if REDIS_HOST:
|
||||
CACHES['default'] = {
|
||||
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
|
||||
'LOCATION': f'redis://{REDIS_HOST}:{REDIS_PORT}',
|
||||
}
|
||||
if REDIS_USERNAME and not REDIS_PASSWORD:
|
||||
CACHES['default']['LOCATION'] = f'redis://{REDIS_USERNAME}@{REDIS_HOST}:{REDIS_PORT}'
|
||||
if REDIS_USERNAME and REDIS_PASSWORD:
|
||||
CACHES['default']['LOCATION'] = f'redis://{REDIS_USERNAME}:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}'
|
||||
|
||||
# Vue webpack settings
|
||||
VUE_DIR = os.path.join(BASE_DIR, 'vue')
|
||||
|
||||
@@ -674,17 +684,5 @@ ACCOUNT_RATE_LIMITS = {
|
||||
DISABLE_EXTERNAL_CONNECTORS = extract_bool('DISABLE_EXTERNAL_CONNECTORS', False)
|
||||
EXTERNAL_CONNECTORS_QUEUE_SIZE = int(os.getenv('EXTERNAL_CONNECTORS_QUEUE_SIZE', 100))
|
||||
|
||||
# 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",
|
||||
}
|
||||
|
||||
mimetypes.add_type("text/javascript", ".js", True)
|
||||
mimetypes.add_type("text/javascript", ".mjs", True)
|
||||
|
||||
@@ -41,9 +41,10 @@ python-ldap==3.4.4
|
||||
django-auth-ldap==4.6.0
|
||||
pyppeteer==2.0.0
|
||||
pytubefix==9.2.2
|
||||
aiohttp==3.10.11
|
||||
aiohttp==3.12.14
|
||||
inflection==0.5.1
|
||||
redis==5.2.1
|
||||
hiredis==3.2.1
|
||||
requests-oauthlib==2.0.0
|
||||
pyjwt==2.10.1
|
||||
python3-openid==3.2.0
|
||||
|
||||
@@ -18,12 +18,12 @@
|
||||
"pinia": "^3.0.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-draggable-plus": "^0.6.0",
|
||||
"vue-i18n": "^11.1.7",
|
||||
"vue-i18n": "^11.1.10",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue-simple-calendar": "7.1.0",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"vuetify": "^3.8.12"
|
||||
"vuetify": "^3.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
|
||||
@@ -3,92 +3,45 @@
|
||||
<v-app-bar color="tandoor" flat density="comfortable" v-if="!useUserPreferenceStore().isAuthenticated">
|
||||
|
||||
</v-app-bar>
|
||||
<v-app-bar :color="useUserPreferenceStore().activeSpace.navBgColor ? useUserPreferenceStore().activeSpace.navBgColor : useUserPreferenceStore().userSettings.navBgColor" flat density="comfortable" v-if="useUserPreferenceStore().isAuthenticated" :scroll-behavior="useUserPreferenceStore().userSettings.navSticky ? '' : 'hide'">
|
||||
<v-app-bar :color="useUserPreferenceStore().activeSpace.navBgColor ? useUserPreferenceStore().activeSpace.navBgColor : useUserPreferenceStore().userSettings.navBgColor"
|
||||
flat density="comfortable" v-if="useUserPreferenceStore().isAuthenticated" :scroll-behavior="useUserPreferenceStore().userSettings.navSticky ? '' : 'hide'">
|
||||
<router-link :to="{ name: 'StartPage', params: {} }">
|
||||
<v-img src="../../assets/brand_logo.svg" width="140px" class="ms-2" v-if="useUserPreferenceStore().userSettings.navShowLogo && !useUserPreferenceStore().activeSpace.navLogo"></v-img>
|
||||
<v-img :src="useUserPreferenceStore().activeSpace.navLogo.preview" width="140px" class="ms-2" v-if="useUserPreferenceStore().userSettings.navShowLogo && useUserPreferenceStore().activeSpace.navLogo != undefined"></v-img>
|
||||
<v-img src="../../assets/brand_logo.svg" width="140px" class="ms-2"
|
||||
v-if="useUserPreferenceStore().userSettings.navShowLogo && !useUserPreferenceStore().activeSpace.navLogo"></v-img>
|
||||
<v-img :src="useUserPreferenceStore().activeSpace.navLogo.preview" width="140px" class="ms-2"
|
||||
v-if="useUserPreferenceStore().userSettings.navShowLogo && useUserPreferenceStore().activeSpace.navLogo != undefined"></v-img>
|
||||
</router-link>
|
||||
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<global-search-dialog ></global-search-dialog>
|
||||
<v-btn icon="$add" class="d-print-none">
|
||||
<v-icon icon="$add" class="fa-fw"></v-icon>
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list-item prepend-icon="$add" :to="{ name: 'ModelEditPage', params: {model: 'recipe'} }">{{ $t('Create Recipe') }}</v-list-item>
|
||||
<v-list-item prepend-icon="fa-solid fa-globe" :to="{ name: 'RecipeImportPage', params: {} }">{{ $t('Import Recipe') }}</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<global-search-dialog></global-search-dialog>
|
||||
<v-btn icon="$add" class="d-print-none">
|
||||
<v-icon icon="$add" class="fa-fw"></v-icon>
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list-item prepend-icon="$add" :to="{ name: 'ModelEditPage', params: {model: 'recipe'} }">{{ $t('Create Recipe') }}</v-list-item>
|
||||
<v-list-item prepend-icon="fa-solid fa-globe" :to="{ name: 'RecipeImportPage', params: {} }">{{ $t('Import Recipe') }}</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
|
||||
<v-avatar color="primary" class="me-2 cursor-pointer d-print-none">{{ useUserPreferenceStore().userSettings.user.displayName.charAt(0) }}
|
||||
<v-menu activator="parent">
|
||||
<v-avatar color="primary" class="me-2 cursor-pointer d-print-none">{{ useUserPreferenceStore().userSettings.user.displayName.charAt(0) }}
|
||||
<v-menu activator="parent">
|
||||
|
||||
<v-list density="compact">
|
||||
<v-list-item class="mb-1">
|
||||
<template #prepend>
|
||||
<v-avatar color="primary">{{ useUserPreferenceStore().userSettings.user.displayName.charAt(0) }}</v-avatar>
|
||||
</template>
|
||||
<v-list-item-title>{{ useUserPreferenceStore().userSettings.user.displayName }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ useUserPreferenceStore().activeSpace.name }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
<v-list-item :to="{ name: 'SettingsPage', params: {} }">
|
||||
<template #prepend>
|
||||
<v-icon icon="fa-solid fa-sliders"></v-icon>
|
||||
</template>
|
||||
{{ $t('Settings') }}
|
||||
</v-list-item>
|
||||
<v-list-item :to="{ name: 'DatabasePage', params: {} }">
|
||||
<template #prepend>
|
||||
<v-icon icon="fa-solid fa-folder-tree"></v-icon>
|
||||
</template>
|
||||
{{ $t('Database') }}
|
||||
</v-list-item>
|
||||
<v-list-item :to="{ name: 'HelpPage' }">
|
||||
<template #prepend>
|
||||
<v-icon icon="fa-solid fa-question"></v-icon>
|
||||
</template>
|
||||
{{ $t('Help') }}
|
||||
</v-list-item>
|
||||
<!-- <v-list-item><template #prepend><v-icon icon="fa-solid fa-user-shield"></v-icon></template>Admin</v-list-item>-->
|
||||
<!-- <v-list-item><template #prepend><v-icon icon="fa-solid fa-question"></v-icon></template>Help</v-list-item>-->
|
||||
<template v-if="useUserPreferenceStore().spaces.length > 1">
|
||||
<v-divider></v-divider>
|
||||
<v-list-subheader>{{ $t('YourSpaces') }}</v-list-subheader>
|
||||
<v-list-item v-for="s in useUserPreferenceStore().spaces" :key="s.id" @click="useUserPreferenceStore().switchSpace(s)">
|
||||
<template #prepend>
|
||||
<v-icon icon="fa-solid fa-circle-dot" v-if="s.id == useUserPreferenceStore().activeSpace.id"></v-icon>
|
||||
<v-icon icon="fa-solid fa-circle" v-else></v-icon>
|
||||
</template>
|
||||
{{ s.name }}
|
||||
</v-list-item>
|
||||
<v-list density="compact">
|
||||
<v-list-item class="mb-1">
|
||||
<template #prepend>
|
||||
<v-avatar color="primary">{{ useUserPreferenceStore().userSettings.user.displayName.charAt(0) }}</v-avatar>
|
||||
</template>
|
||||
<v-list-item-title>{{ useUserPreferenceStore().userSettings.user.displayName }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ useUserPreferenceStore().activeSpace.name }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-divider></v-divider>
|
||||
<v-list-item link>
|
||||
<template #prepend>
|
||||
<v-icon icon="fa-solid fa-database"></v-icon>
|
||||
</template>
|
||||
{{ $t('Messages') }}
|
||||
<message-list-dialog></message-list-dialog>
|
||||
</v-list-item>
|
||||
<v-list-item :href="getDjangoUrl('admin')" target="_blank" v-if="useUserPreferenceStore().userSettings.user.isSuperuser">
|
||||
<template #prepend>
|
||||
<v-icon icon="fa-solid fa-shield"></v-icon>
|
||||
</template>
|
||||
{{ $t('Admin') }}
|
||||
</v-list-item>
|
||||
<v-list-item :href="getDjangoUrl('accounts/logout')" link>
|
||||
<template #prepend>
|
||||
<v-icon icon="fa-solid fa-arrow-right-from-bracket"></v-icon>
|
||||
</template>
|
||||
{{ $t('Logout') }}
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-avatar>
|
||||
<component :is="item.component" :="item" v-for="item in useNavigation().getUserNavigation()"></component>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-avatar>
|
||||
|
||||
</v-app-bar>
|
||||
<v-app-bar color="info" density="compact"
|
||||
@@ -126,23 +79,17 @@
|
||||
<v-list-item-subtitle>{{ useUserPreferenceStore().activeSpace.name }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
<v-list-item prepend-icon="$recipes" title="Home" :to="{ name: 'StartPage', params: {} }"></v-list-item>
|
||||
<v-list-item prepend-icon="$search" :title="$t('Search')" :to="{ name: 'SearchPage' }"></v-list-item>
|
||||
<v-list-item prepend-icon="$mealplan" :title="$t('Meal_Plan')" :to="{ name: 'MealPlanPage', params: {} }"></v-list-item>
|
||||
<v-list-item prepend-icon="$shopping" :title="$t('Shopping_list')" :to="{ name: 'ShoppingListPage', params: {} }"></v-list-item>
|
||||
<v-list-item prepend-icon="fas fa-globe" :title="$t('Import')" :to="{ name: 'RecipeImportPage', params: {} }"></v-list-item>
|
||||
<v-list-item prepend-icon="$books" :title="$t('Books')" :to="{ name: 'BooksPage', params: {} }"></v-list-item>
|
||||
<v-list-item prepend-icon="fa-solid fa-folder-tree" :title="$t('Database')" :to="{ name: 'DatabasePage' }"></v-list-item>
|
||||
<component :is="item.component" :="item" v-for="item in useNavigation().NAVIGATION_DRAWER"></component>
|
||||
|
||||
<navigation-drawer-context-menu></navigation-drawer-context-menu>
|
||||
</v-list>
|
||||
|
||||
|
||||
<template #append>
|
||||
<v-list nav>
|
||||
<v-list-item prepend-icon="fas fa-sliders" :title="$t('Settings')" :to="{ name: 'SettingsPage', params: {} }"></v-list-item>
|
||||
<v-list-item prepend-icon="fa-solid fa-heart" href="https://tandoor.dev" target="_blank">
|
||||
<v-list-item prepend-icon="fa-solid fa-heart" link>
|
||||
Tandoor {{ useUserPreferenceStore().serverSettings.version }}
|
||||
<help-dialog></help-dialog>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
@@ -166,10 +113,7 @@
|
||||
<v-icon icon="fa-fw fas fa-bars"></v-icon>
|
||||
<v-bottom-sheet activator="parent" close-on-content-click>
|
||||
<v-list nav>
|
||||
<v-list-item prepend-icon="fa-solid fa-sliders" :to="{ name: 'SettingsPage', params: {} }" :title="$t('Settings')"></v-list-item>
|
||||
<v-list-item prepend-icon="fas fa-globe" :title="$t('Import')" :to="{ name: 'RecipeImportPage', params: {} }"></v-list-item>
|
||||
<v-list-item prepend-icon="fa-solid fa-folder-tree" :to="{ name: 'DatabasePage' }" :title="$t('Database')"></v-list-item>
|
||||
<v-list-item prepend-icon="$books" :title="$t('Books')" :to="{ name: 'BooksPage', params: {} }"></v-list-item>
|
||||
<component :is="item.component" :="item" v-for="item in useNavigation().BOTTOM_NAVIGATION"></component>
|
||||
</v-list>
|
||||
</v-bottom-sheet>
|
||||
</v-btn>
|
||||
@@ -187,16 +131,18 @@
|
||||
<script lang="ts" setup>
|
||||
import GlobalSearchDialog from "@/components/inputs/GlobalSearchDialog.vue"
|
||||
|
||||
import {useDisplay, useTheme} from "vuetify"
|
||||
import {useDisplay} from "vuetify"
|
||||
import VSnackbarQueued from "@/components/display/VSnackbarQueued.vue";
|
||||
import MessageListDialog from "@/components/dialogs/MessageListDialog.vue";
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
||||
import NavigationDrawerContextMenu from "@/components/display/NavigationDrawerContextMenu.vue";
|
||||
import {useDjangoUrls} from "@/composables/useDjangoUrls";
|
||||
import {onMounted, ref} from "vue";
|
||||
import {onMounted} from "vue";
|
||||
import {isSpaceAboveLimit} from "@/utils/logic_utils";
|
||||
import '@/assets/tandoor_light.css'
|
||||
import {useMediaQuery} from "@vueuse/core";
|
||||
import HelpDialog from "@/components/dialogs/HelpDialog.vue";
|
||||
import {NAVIGATION_DRAWER} from "@/utils/navigation.ts";
|
||||
import {useNavigation} from "@/composables/useNavigation.ts";
|
||||
|
||||
const {lgAndUp} = useDisplay()
|
||||
const {getDjangoUrl} = useDjangoUrls()
|
||||
@@ -209,6 +155,99 @@ onMounted(() => {
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style>
|
||||
|
||||
.v-theme--dark {
|
||||
|
||||
a:not([class]) {
|
||||
color: #b98766;
|
||||
text-decoration: none;
|
||||
background-color: transparent
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #fff;
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]), a:not([href]):not([tabindex]):focus, a:not([href]):not([tabindex]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]):focus {
|
||||
outline: 0
|
||||
}
|
||||
|
||||
/* Meal-Plan */
|
||||
|
||||
.cv-header {
|
||||
background-color: #303030 !important;
|
||||
}
|
||||
|
||||
.cv-weeknumber, .cv-header-day {
|
||||
background-color: #303030 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.cv-day.past {
|
||||
background-color: #333333 !important;
|
||||
}
|
||||
|
||||
.cv-day.today {
|
||||
background-color: var(--primary) !important;
|
||||
}
|
||||
|
||||
.cv-day.outsideOfMonth {
|
||||
background-color: #0d0d0d !important;
|
||||
}
|
||||
|
||||
.cv-item {
|
||||
background-color: #4E4E4E !important;
|
||||
}
|
||||
|
||||
.d01 .cv-day-number {
|
||||
background-color: #b98766 !important;
|
||||
}
|
||||
|
||||
/* vueform/multiselect */
|
||||
|
||||
.multiselect-dropdown {
|
||||
background: #212121 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.v-theme--light {
|
||||
a:not([class]) {
|
||||
color: #b98766;
|
||||
text-decoration: none;
|
||||
background-color: transparent
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #000;
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]), a:not([href]):not([tabindex]):focus, a:not([href]):not([tabindex]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]):focus {
|
||||
outline: 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* vueform/multiselect */
|
||||
|
||||
.multiselect-option.is-pointed {
|
||||
background: #b98766 !important;
|
||||
}
|
||||
|
||||
.multiselect-option.is-selected {
|
||||
background: #b55e4f !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {createApp} from "vue";
|
||||
import {createRouter, createWebHashHistory, createWebHistory} from 'vue-router'
|
||||
import {createRouter, createWebHistory} from 'vue-router'
|
||||
import {createPinia} from 'pinia'
|
||||
// @ts-ignore
|
||||
import App from './Tandoor.vue'
|
||||
@@ -12,8 +12,9 @@ import { createRulesPlugin } from 'vuetify/labs/rules'
|
||||
|
||||
import {setupI18n} from "@/i18n";
|
||||
import MealPlanPage from "@/pages/MealPlanPage.vue";
|
||||
import {TandoorPlugin} from "@/types/Plugins.ts";
|
||||
|
||||
const routes = [
|
||||
let routes = [
|
||||
{path: '/', component: () => import("@/pages/StartPage.vue"), name: 'StartPage'},
|
||||
{path: '/search', redirect: {name: 'StartPage'}},
|
||||
{path: '/test', component: () => import("@/pages/TestPage.vue"), name: 'view_test'},
|
||||
@@ -55,6 +56,13 @@ const routes = [
|
||||
{path: '/space-setup', component: () => import("@/pages/SpaceSetupPage.vue"), name: 'SpaceSetupPage'},
|
||||
]
|
||||
|
||||
const pluginModules = import.meta.glob('@/plugins/*/plugin.ts', { eager: true })
|
||||
const tandoorPlugins = [] as TandoorPlugin[]
|
||||
Object.values(pluginModules).forEach(module => {
|
||||
tandoorPlugins.push(module.plugin)
|
||||
routes = routes.concat(module.plugin.routes)
|
||||
})
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
a {
|
||||
color: #b98766;
|
||||
text-decoration: none;
|
||||
background-color: transparent
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #fff;
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]), a:not([href]):not([tabindex]):focus, a:not([href]):not([tabindex]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]):focus {
|
||||
outline: 0
|
||||
}
|
||||
|
||||
/* Meal-Plan */
|
||||
|
||||
.cv-header {
|
||||
background-color: #303030 !important;
|
||||
}
|
||||
|
||||
.cv-weeknumber, .cv-header-day {
|
||||
background-color: #303030 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.cv-day.past {
|
||||
background-color: #333333 !important;
|
||||
}
|
||||
|
||||
.cv-day.today {
|
||||
background-color: var(--primary) !important;
|
||||
}
|
||||
|
||||
.cv-day.outsideOfMonth {
|
||||
background-color: #0d0d0d !important;
|
||||
}
|
||||
|
||||
.cv-item {
|
||||
background-color: #4E4E4E !important;
|
||||
}
|
||||
|
||||
.d01 .cv-day-number {
|
||||
background-color: #b98766!important;
|
||||
}
|
||||
|
||||
/* vueform/multiselect */
|
||||
|
||||
.multiselect-dropdown {
|
||||
background: #212121!important;
|
||||
}
|
||||
|
||||
.multiselect-option.is-pointed {
|
||||
background: #b98766!important;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
a {
|
||||
color: #b98766;
|
||||
text-decoration: none;
|
||||
background-color: transparent
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #000;
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]), a:not([href]):not([tabindex]):focus, a:not([href]):not([tabindex]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]):focus {
|
||||
outline: 0
|
||||
}
|
||||
|
||||
@@ -27,10 +27,10 @@
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
<v-number-input v-model="servings" class="mt-3" control-variant="split" :label="$t('Servings')" :precision="2"></v-number-input>
|
||||
<v-number-input v-model="servings" class="mt-3" control-variant="split" :label="$t('Servings')" :precision="2" :disabled="loading"></v-number-input>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn class="float-right" prepend-icon="$create" color="create" @click="createShoppingListRecipe()">{{ $t('Add_to_Shopping') }}</v-btn>
|
||||
<v-btn class="float-right" prepend-icon="$create" color="create" @click="createShoppingListRecipe()" :disabled="loading">{{ $t('Add_to_Shopping') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
@@ -40,15 +40,18 @@
|
||||
|
||||
import {computed, onMounted, PropType, ref} from "vue";
|
||||
import VClosableCardTitle from "@/components/dialogs/VClosableCardTitle.vue";
|
||||
import {ApiApi, Recipe, RecipeFlat, RecipeOverview, type ShoppingListEntryBulkCreate, ShoppingListRecipe} from "@/openapi";
|
||||
import {ApiApi, MealPlan, Recipe, RecipeFlat, RecipeOverview, type ShoppingListEntryBulkCreate, ShoppingListRecipe} from "@/openapi";
|
||||
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
|
||||
import {ShoppingDialogRecipe, ShoppingDialogRecipeEntry} from "@/types/Shopping";
|
||||
import {calculateFoodAmount} from "@/utils/number_utils";
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
||||
import {ingredientToUnitString, ingredientToFoodString} from "@/utils/model_utils.ts";
|
||||
|
||||
const emit = defineEmits(['created'])
|
||||
|
||||
const props = defineProps({
|
||||
recipe: {type: Object as PropType<Recipe | RecipeFlat | RecipeOverview>, required: true},
|
||||
mealPlan: {type: Object as PropType<MealPlan>, required: false},
|
||||
})
|
||||
|
||||
const dialog = ref(false)
|
||||
@@ -75,6 +78,7 @@ onMounted(() => {
|
||||
function loadRecipeData() {
|
||||
let api = new ApiApi()
|
||||
let promises: Promise<any>[] = []
|
||||
loading.value = true
|
||||
|
||||
let recipeRequest = api.apiRecipeRetrieve({id: props.recipe.id!}).then(r => {
|
||||
recipe.value = r
|
||||
@@ -106,7 +110,7 @@ function loadRecipeData() {
|
||||
|
||||
recipe.steps.forEach(step => {
|
||||
step.ingredients.forEach(ingredient => {
|
||||
if(!ingredient.isHeader){
|
||||
if (!ingredient.isHeader) {
|
||||
dialogRecipe.entries.push({
|
||||
amount: ingredient.amount,
|
||||
food: ingredient.food,
|
||||
@@ -136,6 +140,10 @@ function createShoppingListRecipe() {
|
||||
servings: servings.value,
|
||||
} as ShoppingListRecipe
|
||||
|
||||
if (props.mealPlan && props.mealPlan.id) {
|
||||
shoppingListRecipe.mealplan = props.mealPlan.id!
|
||||
}
|
||||
|
||||
let shoppingListEntries = {
|
||||
entries: []
|
||||
} as ShoppingListEntryBulkCreate
|
||||
@@ -157,6 +165,7 @@ function createShoppingListRecipe() {
|
||||
api.apiShoppingListRecipeBulkCreateEntriesCreate({id: slr.id!, shoppingListEntryBulkCreate: shoppingListEntries}).then(r => {
|
||||
useMessageStore().addPreparedMessage(PreparedMessage.CREATE_SUCCESS)
|
||||
dialog.value = false
|
||||
emit('created')
|
||||
}).catch(err => {
|
||||
useMessageStore().addError(ErrorMessageType.CREATE_ERROR, err)
|
||||
}).finally(() => {
|
||||
|
||||
98
vue3/src/components/dialogs/HelpDialog.vue
Normal file
98
vue3/src/components/dialogs/HelpDialog.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
|
||||
<v-dialog height="70vh" activator="parent" v-model="dialog">
|
||||
<v-card>
|
||||
<v-closable-card-title v-model="dialog" :title="$t('Help')" icon="fa-solid fa-question">
|
||||
<template #content>
|
||||
<div class="d-flex align-center">
|
||||
|
||||
<v-btn variant="text" icon="fa-solid fa-bars" @click.stop="drawer = !drawer"></v-btn>
|
||||
<span>{{ $t('Help') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</v-closable-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text class="pa-0">
|
||||
<v-layout style="height: 100%">
|
||||
|
||||
<v-navigation-drawer style="height: calc(100% + 0px)" v-model="drawer">
|
||||
<v-list>
|
||||
|
||||
<v-list-item>
|
||||
<v-text-field density="compact" variant="outlined" class="pt-2 pb-2" :label="$t('Search')" hide-details clearable></v-text-field>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
<v-list-item link title="Start" @click="window = 'start'"></v-list-item>
|
||||
<v-list-item link title="Space" @click="window = 'space'"></v-list-item>
|
||||
</v-list>
|
||||
|
||||
</v-navigation-drawer>
|
||||
|
||||
<v-main>
|
||||
<v-container>
|
||||
<v-window v-model="window">
|
||||
<v-window-item value="start">
|
||||
<h2>Welcome to Tandoor 2</h2>
|
||||
<p class="mt-3">Tandoor is one of the most most powerful recipe management suits available. It has constantly been improved since its first
|
||||
version in 2018.
|
||||
This knowledgebase explains all important features and concepts. Explore it to find out how Tandoor can help you improve your daily cooking
|
||||
routine or search
|
||||
for specific features to help you understand them.</p>
|
||||
|
||||
<p class="mt-3">Some of the most important concepts are Spaces, Recipes, Foods and Units.</p>
|
||||
|
||||
<v-alert class="mt-3" border="start" variant="tonal" color="success">
|
||||
<v-alert-title>Did you know?</v-alert-title>
|
||||
Tandoor is Open Source and available to anyone for free to host on their own server. Thousands of hours have been spend
|
||||
making Tandoor what it is today. You can help make Tandoor even better by contributing or helping financing the effort.
|
||||
<br/>
|
||||
<v-btn class="mt-2" href="https://docs.tandoor.dev/contribute/contribute/" target="_blank" prepend-icon="fa-solid fa-code-branch">
|
||||
Contribute
|
||||
</v-btn>
|
||||
<v-btn class="mt-2 ms-2" href="https://github.com/sponsors/vabene1111" target="_blank" prepend-icon="fa-solid fa-dollar-sign">Sponsor
|
||||
</v-btn>
|
||||
</v-alert>
|
||||
|
||||
</v-window-item>
|
||||
<v-window-item value="space">
|
||||
<p class="mt-3">All your data is stored in a Space where you can invite other people to collaborate on your recipe database. Typcially the members of a space
|
||||
belong to one family/household/organization.</p>
|
||||
|
||||
<p class="mt-3">While everyone can access all recipes by default, Books, Shopping Lists and Mealplans are not shared by default. You can share them with other members of your space
|
||||
using the settings.
|
||||
</p>
|
||||
<p class="mt-3">You can create and be a member of multiple spaces. Switch between them freely using the navigation or space settings. Depending
|
||||
on the permission configured by the space owner you might not have access to all features of a space.</p>
|
||||
<p class="mt-3"></p>
|
||||
|
||||
<v-btn color="primary" variant="tonal" prepend-icon="fa-solid fa-database" class="me-2" :to="{name: 'UserSpaceSettings'}">{{ $t('YourSpaces') }}</v-btn>
|
||||
<v-btn color="primary" variant="tonal" prepend-icon="$settings" class="me-2" :to="{name: 'SpaceSettings'}">{{ $t('SpaceSettings') }}</v-btn>
|
||||
<v-btn color="primary" variant="tonal" prepend-icon="fa-solid fa-users" class="me-2" :to="{name: 'SpaceMemberSettings'}">{{ $t('Invites') }}</v-btn>
|
||||
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-container>
|
||||
</v-main>
|
||||
</v-layout>
|
||||
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import VClosableCardTitle from "@/components/dialogs/VClosableCardTitle.vue";
|
||||
import {ref} from "vue";
|
||||
|
||||
const dialog = ref(false)
|
||||
const window = ref('start')
|
||||
const drawer = ref(true)
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -28,7 +28,7 @@ const props = defineProps({
|
||||
closeAfterDelete: {default: true},
|
||||
})
|
||||
|
||||
const editorComponent = shallowRef(defineAsyncComponent(() => import(`@/components/model_editors/${getGenericModelFromString(props.model, t).model.name}Editor.vue`)))
|
||||
const editorComponent = shallowRef(getGenericModelFromString(props.model, t).model.editorComponent)
|
||||
|
||||
const dialog = defineModel<Boolean|undefined>({default: undefined})
|
||||
const dialogActivator = (dialog.value !== undefined) ? undefined : props.activator
|
||||
@@ -40,7 +40,7 @@ const editingObjChangedState = ref(false)
|
||||
* because of this watch prop changes and update manually if prop is changed
|
||||
*/
|
||||
watch(() => props.model, () => {
|
||||
editorComponent.value = defineAsyncComponent(() => import(`@/components/model_editors/${getGenericModelFromString(props.model, t).model.name}Editor.vue`))
|
||||
editorComponent.value = getGenericModelFromString(props.model, t).model.editorComponent
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<v-card-text class="pt-0 pr-4 pl-4">
|
||||
|
||||
<v-label>{{ $t('Choose_Category') }}</v-label>
|
||||
<model-select model="SupermarketCategory" @update:modelValue="categoryUpdate"></model-select>
|
||||
<model-select model="SupermarketCategory" @update:modelValue="categoryUpdate" allow-create></model-select>
|
||||
|
||||
<v-row>
|
||||
<v-col class="pr-0">
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<template>
|
||||
<v-card-title class="pb-0">
|
||||
<v-row align="center">
|
||||
<v-col cols="10" md="11" class="text-truncate">
|
||||
<i :class="props.icon" v-if="props.icon != ''"></i>
|
||||
{{ props.title }}
|
||||
<v-card-subtitle class="pa-0" v-if="props.subTitle != ''">{{ props.subTitle }}</v-card-subtitle>
|
||||
<v-card-title class="pb-1 pt-1 pl-1 pr-1">
|
||||
<v-row no-gutters align="center">
|
||||
<v-col cols="10" md="11" class="text-truncate pt-0 pb-0 pl-2">
|
||||
<slot name="content">
|
||||
<i :class="props.icon" v-if="props.icon != ''"></i>
|
||||
{{ props.title }}
|
||||
<v-card-subtitle class="pa-0" v-if="props.subTitle != ''">{{ props.subTitle }}</v-card-subtitle>
|
||||
</slot>
|
||||
</v-col>
|
||||
<v-col cols="2" md="1">
|
||||
<v-btn class="float-right pr-2" icon="$close" variant="plain" @click="model = false; emit('close')" v-if="!props.hideClose"></v-btn>
|
||||
|
||||
@@ -82,7 +82,7 @@ const planItems = computed(() => {
|
||||
startDate.setHours(hour, minutes, seconds)
|
||||
endDate.setHours(hour, minutes, seconds)
|
||||
}
|
||||
console.log(startDate, endDate)
|
||||
|
||||
items.push({
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
|
||||
@@ -29,10 +29,6 @@ const image = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.recipe, () => {
|
||||
console.log('changed')
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<span class="ps-2 text-h5 flex-grow-1 pa-1" :class="{'text-truncate': !showFullRecipeName}" @click="showFullRecipeName = !showFullRecipeName">
|
||||
{{ recipe.name }}
|
||||
</span>
|
||||
<recipe-context-menu :recipe="recipe"></recipe-context-menu>
|
||||
<recipe-context-menu :recipe="recipe" v-if="useUserPreferenceStore().isAuthenticated"></recipe-context-menu>
|
||||
</v-sheet>
|
||||
<keywords-component variant="flat" class="ms-1 mb-2" :keywords="recipe.keywords"></keywords-component>
|
||||
<v-sheet class="ps-2 text-disabled">
|
||||
@@ -75,7 +75,7 @@
|
||||
<v-card-text class="flex-grow-1">
|
||||
<div class="d-flex">
|
||||
<h1 class="flex-column flex-grow-1">{{ recipe.name }}</h1>
|
||||
<recipe-context-menu :recipe="recipe" class="flex-column mb-auto mt-2 float-right"></recipe-context-menu>
|
||||
<recipe-context-menu :recipe="recipe" v-if="useUserPreferenceStore().isAuthenticated" class="flex-column mb-auto mt-2 float-right"></recipe-context-menu>
|
||||
</div>
|
||||
<p>
|
||||
{{ $t('created_by') }} {{ recipe.createdBy.displayName }} ({{ DateTime.fromJSDate(recipe.createdAt).toLocaleString(DateTime.DATE_SHORT) }})
|
||||
|
||||
@@ -81,8 +81,8 @@
|
||||
<v-container>
|
||||
<v-row class="pa-0" dense>
|
||||
<v-col class="pa-0">
|
||||
<v-chip-group v-model="useUserPreferenceStore().deviceSettings.shopping_selected_supermarket" v-if="supermarkets.length > 0" >
|
||||
<v-chip v-for="s in supermarkets" :value="s" :key="s.id" label density="compact" variant="outlined" color="primary">{{s.name}}</v-chip>
|
||||
<v-chip-group v-model="useUserPreferenceStore().deviceSettings.shopping_selected_supermarket" v-if="supermarkets.length > 0">
|
||||
<v-chip v-for="s in supermarkets" :value="s" :key="s.id" label density="compact" variant="outlined" color="primary">{{ s.name }}</v-chip>
|
||||
</v-chip-group>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -176,6 +176,15 @@
|
||||
<v-card>
|
||||
<v-card-title>{{ $t('Recipes') }} / {{ $t('Meal_Plan') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<ModelSelect model="Recipe" v-model="manualAddRecipe" append-to-body>
|
||||
<template #append>
|
||||
<v-btn icon="$create" color="create" :disabled="manualAddRecipe == undefined">
|
||||
<v-icon icon="$create"></v-icon>
|
||||
<add-to-shopping-dialog :recipe="manualAddRecipe" v-if="manualAddRecipe != undefined" @created="useShoppingStore().refreshFromAPI(); manualAddRecipe = undefined"></add-to-shopping-dialog>
|
||||
</v-btn>
|
||||
</template>
|
||||
</ModelSelect>
|
||||
|
||||
<v-list>
|
||||
<v-list-item v-for="r in useShoppingStore().getAssociatedRecipes()">
|
||||
<template #prepend>
|
||||
@@ -208,6 +217,8 @@
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
@@ -234,7 +245,7 @@
|
||||
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {useShoppingStore} from "@/stores/ShoppingStore";
|
||||
import {ApiApi, ResponseError, ShoppingListEntry, ShoppingListRecipe, Supermarket} from "@/openapi";
|
||||
import {ApiApi, Recipe, ResponseError, ShoppingListEntry, ShoppingListRecipe, Supermarket} from "@/openapi";
|
||||
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
|
||||
import ShoppingLineItem from "@/components/display/ShoppingLineItem.vue";
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
||||
@@ -250,11 +261,13 @@ import ModelEditDialog from "@/components/dialogs/ModelEditDialog.vue";
|
||||
import {onBeforeRouteLeave} from "vue-router";
|
||||
import {isShoppingCategoryVisible} from "@/utils/logic_utils.ts";
|
||||
import ShoppingExportDialog from "@/components/dialogs/ShoppingExportDialog.vue";
|
||||
import AddToShoppingDialog from "@/components/dialogs/AddToShoppingDialog.vue";
|
||||
|
||||
const {t} = useI18n()
|
||||
|
||||
const currentTab = ref("shopping")
|
||||
const supermarkets = ref([] as Supermarket[])
|
||||
const manualAddRecipe = ref<undefined | Recipe>(undefined)
|
||||
|
||||
/**
|
||||
* VSelect items for shopping list grouping options with localized names
|
||||
@@ -351,7 +364,7 @@ function deleteListRecipe(slr: ShoppingListRecipe) {
|
||||
/**
|
||||
* load a list of supermarkets
|
||||
*/
|
||||
function loadSupermarkets(){
|
||||
function loadSupermarkets() {
|
||||
let api = new ApiApi()
|
||||
|
||||
api.apiSupermarketList().then(r => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<!-- TODO label is not showing for some reason, for now in placeholder -->
|
||||
|
||||
<v-label class="mt-2" v-if="props.label" >{{props.label}}</v-label>
|
||||
<v-label class="mt-2" v-if="props.label">{{ props.label }}</v-label>
|
||||
<v-input :hint="props.hint" persistent-hint :label="props.label" :hide-details="props.hideDetails">
|
||||
<template #prepend v-if="$slots.prepend">
|
||||
<slot name="prepend"></slot>
|
||||
@@ -92,7 +92,7 @@ const props = defineProps({
|
||||
|
||||
mode: {type: String as PropType<'single' | 'multiple' | 'tags'>, default: 'single'},
|
||||
appendToBody: {type: Boolean, default: false},
|
||||
object: {type: Boolean, default: true},
|
||||
object: {type: Boolean, default: true}, // TODO broken either fix or finally get other multiselect working
|
||||
|
||||
allowCreate: {type: Boolean, default: false},
|
||||
placeholder: {type: String, default: undefined},
|
||||
@@ -150,9 +150,7 @@ function search(query: string) {
|
||||
loading.value = true
|
||||
return modelClass.value.list({query: query, page: 1, pageSize: props.limit}).then((r: any) => {
|
||||
if (modelClass.value.model.isPaginated) {
|
||||
if (r.next) {
|
||||
hasMoreItems.value = true
|
||||
}
|
||||
hasMoreItems.value = !!r.next
|
||||
return r.results
|
||||
} else {
|
||||
hasMoreItems.value = false
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</v-btn-group>
|
||||
|
||||
<v-row class="d-none d-md-flex mt-2" v-for="p in properties" dense>
|
||||
<v-col cols="0" md="5">
|
||||
<v-col cols="0" md="6">
|
||||
<v-number-input :step="10" v-model="p.propertyAmount" control-variant="stacked" :precision="2">
|
||||
<template #append-inner v-if="p.propertyType">
|
||||
<v-chip class="me-4">{{ p.propertyType.unit }} / {{ props.amountFor }}
|
||||
@@ -14,13 +14,13 @@
|
||||
</v-number-input>
|
||||
</v-col>
|
||||
<v-col cols="0" md="6">
|
||||
<!-- TODO fix card overflow invisible, overflow-visible class is not working -->
|
||||
<model-select :label="$t('Property')" v-model="p.propertyType" model="PropertyType"></model-select>
|
||||
</v-col>
|
||||
<v-col cols="0" md="1">
|
||||
<v-btn color="delete" @click="deleteProperty(p)">
|
||||
<v-icon icon="$delete"></v-icon>
|
||||
</v-btn>
|
||||
<model-select v-model="p.propertyType" model="PropertyType">
|
||||
<template #append>
|
||||
<v-btn color="delete" icon @click="deleteProperty(p)">
|
||||
<v-icon icon="$delete"></v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</model-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-list class="d-md-none">
|
||||
|
||||
@@ -47,10 +47,10 @@
|
||||
<v-number-input :label="$t('Time')" v-model="step.time" :min="0" :step="5" control-variant="split"></v-number-input>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6" v-if="showRecipe || step.stepRecipe != null">
|
||||
<model-select model="Recipe" v-model="step.stepRecipe" :object="false" append-to-body></model-select>
|
||||
<model-select model="Recipe" v-model="step.stepRecipeData" @update:modelValue="step.stepRecipe = (step.stepRecipeData != null) ? step.stepRecipeData.id! : null"></model-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6" v-if="showFile || step.file != null">
|
||||
<model-select model="UserFile" v-model="step.file" append-to-body></model-select>
|
||||
<model-select model="UserFile" v-model="step.file"></model-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {VDateInput} from 'vuetify/labs/VDateInput' //TODO remove once component is out of labs
|
||||
import {onMounted, PropType} from "vue";
|
||||
import {AccessToken} from "@/openapi";
|
||||
import {onMounted, PropType, watch} from "vue";
|
||||
import {AccessToken, ApiApi} from "@/openapi";
|
||||
|
||||
import {DateTime} from "luxon";
|
||||
import BtnCopy from "@/components/buttons/BtnCopy.vue";
|
||||
@@ -49,8 +49,22 @@ const props = defineProps({
|
||||
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
|
||||
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<AccessToken>('AccessToken', emit)
|
||||
|
||||
/**
|
||||
* watch prop changes and re-initialize editor
|
||||
* required to embed editor directly into pages and be able to change item from the outside
|
||||
*/
|
||||
watch([() => props.item, () => props.itemId], () => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor(){
|
||||
setupState(props.item, props.itemId, {
|
||||
newItemFunction: () => {
|
||||
editingObj.value.expires = DateTime.now().plus({year: 1}).toJSDate()
|
||||
@@ -58,7 +72,7 @@ onMounted(() => {
|
||||
},
|
||||
itemDefaults: props.itemDefaults
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<v-number-input :label="$t('Order')" :step="10" v-model="editingObj.order" :hint="$t('OrderInformation')" control-variant="stacked"></v-number-input>
|
||||
<v-checkbox :label="$t('Disabled')" v-model="editingObj.disabled"></v-checkbox>
|
||||
|
||||
<a href="https://docs.tandoor.dev/features/automation/" target="_blank">{{$t('Learn_More')}}</a>
|
||||
<a href="https://docs.tandoor.dev/features/automation/" target="_blank">{{ $t('Learn_More') }}</a>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</model-editor-base>
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, PropType} from "vue";
|
||||
import {onMounted, PropType, watch} from "vue";
|
||||
import {Automation} from "@/openapi";
|
||||
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
||||
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
||||
@@ -47,7 +47,26 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
|
||||
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<Automation>('Automation', emit)
|
||||
const {
|
||||
setupState,
|
||||
deleteObject,
|
||||
saveObject,
|
||||
isUpdate,
|
||||
editingObjName,
|
||||
loading,
|
||||
editingObj,
|
||||
editingObjChanged,
|
||||
modelClass,
|
||||
applyItemDefaults
|
||||
} = useModelEditorFunctions<Automation>('Automation', emit)
|
||||
|
||||
/**
|
||||
* watch prop changes and re-initialize editor
|
||||
* required to embed editor directly into pages and be able to change item from the outside
|
||||
*/
|
||||
watch([() => props.item, () => props.itemId], () => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
// object specific data (for selects/display)
|
||||
|
||||
@@ -65,13 +84,22 @@ const AUTOMATION_TYPES = [
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor(){
|
||||
setupState(props.item, props.itemId, {
|
||||
newItemFunction: () => {
|
||||
editingObj.value.order = 0
|
||||
|
||||
applyItemDefaults(props.itemDefaults)
|
||||
},
|
||||
itemDefaults: props.itemDefaults
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, PropType} from "vue";
|
||||
import {onMounted, PropType, watch} from "vue";
|
||||
import {ConnectorConfig} from "@/openapi";
|
||||
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
||||
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
||||
@@ -61,12 +61,27 @@ const {
|
||||
modelClass
|
||||
} = useModelEditorFunctions<ConnectorConfig>('ConnectorConfig', emit)
|
||||
|
||||
/**
|
||||
* watch prop changes and re-initialize editor
|
||||
* required to embed editor directly into pages and be able to change item from the outside
|
||||
*/
|
||||
watch([() => props.item, () => props.itemId], () => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
// object specific data (for selects/display)
|
||||
|
||||
onMounted(() => {
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor(){
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, PropType} from "vue";
|
||||
import {onMounted, PropType, watch} from "vue";
|
||||
import {CustomFilter} from "@/openapi";
|
||||
|
||||
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
||||
@@ -35,11 +35,27 @@ const props = defineProps({
|
||||
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
|
||||
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<CustomFilter>('CustomFilter', emit)
|
||||
|
||||
/**
|
||||
* watch prop changes and re-initialize editor
|
||||
* required to embed editor directly into pages and be able to change item from the outside
|
||||
*/
|
||||
watch([() => props.item, () => props.itemId], () => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
// object specific data (for selects/display)
|
||||
|
||||
onMounted(() => {
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor(){
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -34,6 +34,19 @@
|
||||
<v-tabs-window-item value="properties">
|
||||
<v-alert icon="$help">{{ $t('PropertiesFoodHelp') }}</v-alert>
|
||||
<v-form :disabled="loading" class="mt-5">
|
||||
<v-number-input :label="$t('FDC_ID')" v-model="editingObj.fdcId" :precision="0" control-variant="hidden" clearable>
|
||||
<template #append-inner>
|
||||
<v-btn icon="$search" size="small" density="compact" variant="plain" v-if="editingObj.fdcId == undefined"
|
||||
@click="fdcDialog = true"></v-btn>
|
||||
<v-btn @click="updateFoodFdcData()" icon="fa-solid fa-arrows-rotate" size="small" density="compact" variant="plain"
|
||||
v-if="editingObj.fdcId"></v-btn>
|
||||
<v-btn @click="openFdcPage(editingObj.fdcId)" :href="`https://fdc.nal.usda.gov/food-details/${editingObj.fdcId}/nutrients`" target="_blank"
|
||||
icon="fa-solid fa-arrow-up-right-from-square"
|
||||
size="small" variant="plain" v-if="editingObj.fdcId"></v-btn>
|
||||
</template>
|
||||
|
||||
</v-number-input>
|
||||
|
||||
<v-number-input :label="$t('Properties_Food_Amount')" v-model="editingObj.propertiesFoodAmount" :precision="2"></v-number-input>
|
||||
<model-select :label="$t('Properties_Food_Unit')" v-model="editingObj.propertiesFoodUnit" model="Unit"></model-select>
|
||||
|
||||
@@ -61,22 +74,27 @@
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text class="d-none d-md-block">
|
||||
<v-row>
|
||||
<v-row dense>
|
||||
<v-col md="6">
|
||||
<v-number-input :label="$t('Amount')" :step="10" v-model="uc.baseAmount" control-variant="stacked" :precision="3"></v-number-input>
|
||||
<v-number-input :label="$t('Amount')" :step="10" v-model="uc.baseAmount" control-variant="stacked" :precision="3" hide-details></v-number-input>
|
||||
</v-col>
|
||||
<v-col md="6">
|
||||
<!-- TODO fix card overflow invisible, overflow-visible class is not working -->
|
||||
<model-select :label="$t('Unit')" v-model="uc.baseUnit" model="Unit"></model-select>
|
||||
<model-select v-model="uc.baseUnit" model="Unit" hide-details></model-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-row dense>
|
||||
<v-col cols="12" class="text-center">
|
||||
<v-icon icon="fa-solid fa-arrows-up-down" class="mt-4 mb-4"></v-icon>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row dense>
|
||||
<v-col md="6">
|
||||
<v-number-input :label="$t('Amount')" :step="10" v-model="uc.convertedAmount" control-variant="stacked" :precision="3"></v-number-input>
|
||||
</v-col>
|
||||
<v-col md="6">
|
||||
<!-- TODO fix card overflow invisible, overflow-visible class is not working -->
|
||||
<model-select :label="$t('Unit')" v-model="uc.convertedUnit" model="Unit"></model-select>
|
||||
<model-select v-model="uc.convertedUnit" model="Unit"></model-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
@@ -110,6 +128,8 @@
|
||||
|
||||
</v-card-text>
|
||||
|
||||
<fdc-search-dialog v-model="fdcDialog"
|
||||
@selected="(fdcId:number) => {editingObj.fdcId = fdcId;}"></fdc-search-dialog>
|
||||
|
||||
</model-editor-base>
|
||||
|
||||
@@ -118,7 +138,7 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {computed, onMounted, PropType, ref, watch} from "vue";
|
||||
import {ApiApi, Food, Unit, UnitConversion} from "@/openapi";
|
||||
import {ApiApi, Food, Unit, UnitConversion} from "@/openapi";
|
||||
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
|
||||
import ModelSelect from "@/components/inputs/ModelSelect.vue";
|
||||
import ModelEditDialog from "@/components/dialogs/ModelEditDialog.vue";
|
||||
@@ -126,6 +146,9 @@ import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
||||
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
||||
import PropertiesEditor from "@/components/inputs/PropertiesEditor.vue";
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
||||
import FdcSearchDialog from "@/components/dialogs/FdcSearchDialog.vue";
|
||||
import {openFdcPage} from "@/utils/fdc.ts";
|
||||
import {DateTime} from "luxon";
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
@@ -138,6 +161,13 @@ const props = defineProps({
|
||||
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
|
||||
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<Food>('Food', emit)
|
||||
|
||||
/**
|
||||
* watch prop changes and re-initialize editor
|
||||
* required to embed editor directly into pages and be able to change item from the outside
|
||||
*/
|
||||
watch([() => props.item, () => props.itemId], () => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
// object specific data (for selects/display)
|
||||
|
||||
@@ -147,10 +177,10 @@ const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading,
|
||||
*/
|
||||
const propertiesAmountFor = computed(() => {
|
||||
let amountFor = ''
|
||||
if(editingObj.value.propertiesFoodAmount){
|
||||
if (editingObj.value.propertiesFoodAmount) {
|
||||
amountFor += editingObj.value.propertiesFoodAmount
|
||||
}
|
||||
if(editingObj.value.propertiesFoodUnit){
|
||||
if (editingObj.value.propertiesFoodUnit) {
|
||||
amountFor += " " + editingObj.value.propertiesFoodUnit.name
|
||||
}
|
||||
return amountFor
|
||||
@@ -160,6 +190,8 @@ const tab = ref("food")
|
||||
|
||||
const unitConversions = ref([] as UnitConversion[])
|
||||
|
||||
const fdcDialog = ref(false)
|
||||
|
||||
// load conversions the first time the conversions tab is opened
|
||||
const stopConversionsWatcher = watch(tab, (value, oldValue, onCleanup) => {
|
||||
if (value == 'conversions') {
|
||||
@@ -168,8 +200,14 @@ const stopConversionsWatcher = watch(tab, (value, oldValue, onCleanup) => {
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor(){
|
||||
setupState(props.item, props.itemId, {
|
||||
newItemFunction: () => {
|
||||
editingObj.value.propertiesFoodAmount = 100
|
||||
@@ -177,7 +215,7 @@ onMounted(() => {
|
||||
},
|
||||
itemDefaults: props.itemDefaults,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@@ -229,6 +267,27 @@ function deleteUnitConversion(unitConversion: UnitConversion, database = false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the food FDC data on the server and update the editing object
|
||||
*/
|
||||
function updateFoodFdcData() {
|
||||
let api = new ApiApi()
|
||||
if (editingObj.value.fdcId) {
|
||||
saveObject().then(() => {
|
||||
|
||||
loading.value = true
|
||||
api.apiFoodFdcCreate({id: editingObj.value.id!, food: editingObj.value}).then(r => {
|
||||
editingObj.value = r
|
||||
}).catch(err => {
|
||||
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
editingObjChanged.value = false
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {VDateInput} from 'vuetify/labs/VDateInput' //TODO remove once component is out of labs
|
||||
import {onMounted, PropType, ref} from "vue";
|
||||
import {onMounted, PropType, ref, watch} from "vue";
|
||||
import {ApiApi, Group, InviteLink} from "@/openapi";
|
||||
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
|
||||
import {DateTime} from "luxon";
|
||||
@@ -43,11 +43,26 @@ const props = defineProps({
|
||||
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
|
||||
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<InviteLink>('InviteLink', emit)
|
||||
|
||||
/**
|
||||
* watch prop changes and re-initialize editor
|
||||
* required to embed editor directly into pages and be able to change item from the outside
|
||||
*/
|
||||
watch([() => props.item, () => props.itemId], () => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
// object specific data (for selects/display)
|
||||
const groups = ref([] as Group[])
|
||||
|
||||
onMounted(() => {
|
||||
const api = new ApiApi()
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor(){
|
||||
const api = new ApiApi()
|
||||
|
||||
api.apiGroupList().then(r => {
|
||||
groups.value = r
|
||||
@@ -63,8 +78,7 @@ onMounted(() => {
|
||||
}).catch(err => {
|
||||
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, PropType} from "vue";
|
||||
import {onMounted, PropType, watch} from "vue";
|
||||
import {Keyword} from "@/openapi";
|
||||
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
||||
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
||||
@@ -38,12 +38,27 @@ const props = defineProps({
|
||||
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
|
||||
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<Keyword>('Keyword', emit)
|
||||
|
||||
/**
|
||||
* watch prop changes and re-initialize editor
|
||||
* required to embed editor directly into pages and be able to change item from the outside
|
||||
*/
|
||||
watch([() => props.item, () => props.itemId], () => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
// object specific data (for selects/display)
|
||||
|
||||
onMounted(() => {
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor(){
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -23,6 +23,19 @@
|
||||
<v-form :disabled="loading">
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<ModelSelect model="Recipe" v-model="editingObj.recipe"
|
||||
@update:modelValue="editingObj.servings = editingObj.recipe ? editingObj.recipe.servings : 1"></ModelSelect>
|
||||
<!-- <v-number-input label="Days" control-variant="split" :min="1"></v-number-input>-->
|
||||
<!--TODO create days input with +/- synced to date -->
|
||||
<recipe-card :recipe="editingObj.recipe" v-if="editingObj && editingObj.recipe"></recipe-card>
|
||||
<v-btn prepend-icon="$shopping" color="create" class="mt-1" v-if="!editingObj.shopping && editingObj.recipe && isUpdate()">
|
||||
{{$t('Add')}}
|
||||
<add-to-shopping-dialog :recipe="editingObj.recipe" :meal-plan="editingObj" @created="loadShoppingListEntries(); editingObj.shopping = true;"></add-to-shopping-dialog>
|
||||
</v-btn>
|
||||
|
||||
<v-checkbox :label="$t('AddToShopping')" v-model="editingObj.addshopping" hide-details v-if="editingObj.recipe && !isUpdate()"></v-checkbox>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field :label="$t('Title')" v-model="editingObj.title"></v-text-field>
|
||||
<v-date-input
|
||||
@@ -51,16 +64,7 @@
|
||||
<v-number-input control-variant="split" :min="0" v-model="editingObj.servings" :label="$t('Servings')" :precision="2"></v-number-input>
|
||||
<ModelSelect model="User" :allow-create="false" v-model="editingObj.shared" item-label="displayName" mode="tags"></ModelSelect>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<ModelSelect model="Recipe" v-model="editingObj.recipe"
|
||||
@update:modelValue="editingObj.servings = editingObj.recipe ? editingObj.recipe.servings : 1"></ModelSelect>
|
||||
<!-- <v-number-input label="Days" control-variant="split" :min="1"></v-number-input>-->
|
||||
<!--TODO create days input with +/- synced to date -->
|
||||
<recipe-card :recipe="editingObj.recipe" v-if="editingObj && editingObj.recipe"></recipe-card>
|
||||
|
||||
<v-checkbox :label="$t('AddToShopping')" v-model="editingObj.addshopping" hide-details v-if="editingObj.recipe && !isUpdate()"></v-checkbox>
|
||||
<!-- TODO review shopping before add -->
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row dense>
|
||||
<v-col cols="12">
|
||||
@@ -96,7 +100,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {nextTick, onMounted, PropType, ref, toRaw} from "vue";
|
||||
import {nextTick, onMounted, PropType, ref, toRaw, watch} from "vue";
|
||||
import {ApiApi, MealPlan, MealType, ShoppingListRecipe} from "@/openapi";
|
||||
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
||||
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
||||
@@ -112,6 +116,7 @@ import {useShoppingStore} from "@/stores/ShoppingStore";
|
||||
import ShoppingListEntryInput from "@/components/inputs/ShoppingListEntryInput.vue";
|
||||
import ClosableHelpAlert from "@/components/display/ClosableHelpAlert.vue";
|
||||
import {useMealPlanStore} from "@/stores/MealPlanStore";
|
||||
import AddToShoppingDialog from "@/components/dialogs/AddToShoppingDialog.vue";
|
||||
|
||||
const props = defineProps({
|
||||
item: {type: {} as PropType<MealPlan>, required: false, default: null},
|
||||
@@ -134,12 +139,27 @@ const {
|
||||
modelClass
|
||||
} = useModelEditorFunctions<MealPlan>('MealPlan', emit)
|
||||
|
||||
/**
|
||||
* watch prop changes and re-initialize editor
|
||||
* required to embed editor directly into pages and be able to change item from the outside
|
||||
*/
|
||||
watch([() => props.item, () => props.itemId], () => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
// object specific data (for selects/display)
|
||||
const tab = ref('plan')
|
||||
|
||||
const dateRangeValue = ref([] as Date[])
|
||||
|
||||
onMounted(() => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor(){
|
||||
const api = new ApiApi()
|
||||
|
||||
// load meal types and create new object based on default type when initially loading
|
||||
@@ -185,7 +205,7 @@ onMounted(() => {
|
||||
}
|
||||
},)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* update the editing object with data from the date range selector whenever its changed (could probably be a watcher)
|
||||
|
||||
@@ -41,13 +41,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, PropType, ref} from "vue";
|
||||
import {onMounted, PropType, ref, watch} from "vue";
|
||||
import {MealType} from "@/openapi";
|
||||
import {VTimePicker} from 'vuetify/labs/VTimePicker'; // TODO remove once out of labs
|
||||
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
||||
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
||||
import {DateTime} from "luxon";
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
item: {type: {} as PropType<MealType>, required: false, default: null},
|
||||
@@ -59,14 +56,29 @@ const props = defineProps({
|
||||
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
|
||||
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<MealType>('MealType', emit)
|
||||
|
||||
/**
|
||||
* watch prop changes and re-initialize editor
|
||||
* required to embed editor directly into pages and be able to change item from the outside
|
||||
*/
|
||||
watch([() => props.item, () => props.itemId], () => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
|
||||
// object specific data (for selects/display)
|
||||
const timePickerMenu = ref(false)
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor(){
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, PropType} from "vue";
|
||||
import {onMounted, PropType, watch} from "vue";
|
||||
import {Property} from "@/openapi";
|
||||
|
||||
import ModelSelect from "@/components/inputs/ModelSelect.vue";
|
||||
@@ -44,11 +44,27 @@ const props = defineProps({
|
||||
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
|
||||
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<Property>('Property', emit)
|
||||
|
||||
/**
|
||||
* watch prop changes and re-initialize editor
|
||||
* required to embed editor directly into pages and be able to change item from the outside
|
||||
*/
|
||||
watch([() => props.item, () => props.itemId], () => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
// object specific data (for selects/display)
|
||||
|
||||
onMounted(() => {
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor(){
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, PropType, ref} from "vue";
|
||||
import {onMounted, PropType, ref, watch} from "vue";
|
||||
import {PropertyType} from "@/openapi";
|
||||
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
||||
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
||||
@@ -53,12 +53,27 @@ const {
|
||||
modelClass
|
||||
} = useModelEditorFunctions<PropertyType>('PropertyType', emit)
|
||||
|
||||
/**
|
||||
* watch prop changes and re-initialize editor
|
||||
* required to embed editor directly into pages and be able to change item from the outside
|
||||
*/
|
||||
watch([() => props.item, () => props.itemId], () => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
// object specific data (for selects/display)
|
||||
|
||||
onMounted(() => {
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor(){
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, PropType, ref} from "vue";
|
||||
import {onMounted, PropType, ref, watch} from "vue";
|
||||
import {ApiApi, Recipe, RecipeBook, RecipeBookEntry, User} from "@/openapi";
|
||||
import {VDataTableUpdateOptions} from "@/vuetify";
|
||||
|
||||
@@ -79,6 +79,14 @@ const props = defineProps({
|
||||
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
|
||||
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<RecipeBook>('RecipeBook', emit)
|
||||
|
||||
/**
|
||||
* watch prop changes and re-initialize editor
|
||||
* required to embed editor directly into pages and be able to change item from the outside
|
||||
*/
|
||||
watch([() => props.item, () => props.itemId], () => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
const {t} = useI18n()
|
||||
const tab = ref("book")
|
||||
const recipeBookEntries = ref([] as RecipeBookEntry[])
|
||||
@@ -94,6 +102,13 @@ const tableHeaders = [
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor() {
|
||||
setupState(props.item, props.itemId, {
|
||||
newItemFunction: () => {
|
||||
editingObj.value.shared = [] as User[]
|
||||
@@ -104,7 +119,7 @@ onMounted(() => {
|
||||
},
|
||||
itemDefaults: props.itemDefaults
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* add selected recipe into the book and client list
|
||||
@@ -116,12 +131,12 @@ function addRecipeToBook() {
|
||||
let duplicateFound = false
|
||||
|
||||
recipeBookEntries.value.forEach(rBE => {
|
||||
if (rBE.recipe == selectedRecipe.value.id){
|
||||
if (rBE.recipe == selectedRecipe.value.id) {
|
||||
duplicateFound = true
|
||||
}
|
||||
})
|
||||
|
||||
if (!duplicateFound){
|
||||
if (!duplicateFound) {
|
||||
api.apiRecipeBookEntryCreate({recipeBookEntry: {book: editingObj.value.id!, recipe: selectedRecipe.value.id!}}).then(r => {
|
||||
recipeBookEntries.value.push(r)
|
||||
selectedRecipe.value = {} as Recipe
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, PropType, ref, shallowRef} from "vue";
|
||||
import {onMounted, PropType, ref, shallowRef, watch} from "vue";
|
||||
import {Ingredient, Recipe, Step} from "@/openapi";
|
||||
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
||||
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
||||
@@ -164,6 +164,14 @@ const props = defineProps({
|
||||
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
|
||||
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<Recipe>('Recipe', emit)
|
||||
|
||||
/**
|
||||
* watch prop changes and re-initialize editor
|
||||
* required to embed editor directly into pages and be able to change item from the outside
|
||||
*/
|
||||
watch([() => props.item, () => props.itemId], () => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
// object specific data (for selects/display)
|
||||
const {mobile} = useDisplay()
|
||||
|
||||
@@ -174,6 +182,13 @@ const {fileApiLoading, updateRecipeImage} = useFileApi()
|
||||
const file = shallowRef<File | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor(){
|
||||
setupState(props.item, props.itemId, {
|
||||
newItemFunction: () => {
|
||||
editingObj.value.steps = [] as Step[]
|
||||
@@ -181,7 +196,7 @@ onMounted(() => {
|
||||
},
|
||||
itemDefaults: props.itemDefaults,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* save recipe via normal saveMethod and update image afterward if it was changed
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, PropType} from "vue";
|
||||
import {onMounted, PropType, watch} from "vue";
|
||||
import {ShoppingListEntry} from "@/openapi";
|
||||
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
||||
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
||||
@@ -49,12 +49,27 @@ const props = defineProps({
|
||||
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
|
||||
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<ShoppingListEntry>('ShoppingListEntry', emit)
|
||||
|
||||
/**
|
||||
* watch prop changes and re-initialize editor
|
||||
* required to embed editor directly into pages and be able to change item from the outside
|
||||
*/
|
||||
watch([() => props.item, () => props.itemId], () => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
// object specific data (for selects/display)
|
||||
|
||||
onMounted(() => {
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor(){
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, PropType} from "vue";
|
||||
import {onMounted, PropType, watch} from "vue";
|
||||
import { Storage } from "@/openapi";
|
||||
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
||||
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
||||
@@ -47,12 +47,27 @@ const props = defineProps({
|
||||
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
|
||||
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<Storage>('Storage', emit)
|
||||
|
||||
/**
|
||||
* watch prop changes and re-initialize editor
|
||||
* required to embed editor directly into pages and be able to change item from the outside
|
||||
*/
|
||||
watch([() => props.item, () => props.itemId], () => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
// object specific data (for selects/display)
|
||||
|
||||
onMounted(() => {
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor(){
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, PropType} from "vue";
|
||||
import {onMounted, PropType, watch} from "vue";
|
||||
import {SupermarketCategory} from "@/openapi";
|
||||
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
||||
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
||||
@@ -40,12 +40,27 @@ const props = defineProps({
|
||||
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
|
||||
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<SupermarketCategory>('SupermarketCategory', emit)
|
||||
|
||||
/**
|
||||
* watch prop changes and re-initialize editor
|
||||
* required to embed editor directly into pages and be able to change item from the outside
|
||||
*/
|
||||
watch([() => props.item, () => props.itemId], () => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
// object specific data (for selects/display)
|
||||
|
||||
onMounted(() => {
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor(){
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {computed, onMounted, PropType, ref} from "vue";
|
||||
import {computed, onMounted, PropType, ref, watch} from "vue";
|
||||
import {ApiApi, Supermarket, SupermarketCategory, SupermarketCategoryRelation} from "@/openapi";
|
||||
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
||||
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
||||
@@ -121,6 +121,14 @@ const props = defineProps({
|
||||
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
|
||||
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<Supermarket>('Supermarket', emit)
|
||||
|
||||
/**
|
||||
* watch prop changes and re-initialize editor
|
||||
* required to embed editor directly into pages and be able to change item from the outside
|
||||
*/
|
||||
watch([() => props.item, () => props.itemId], () => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
// object specific data (for selects/display)
|
||||
const tab = ref("supermarket")
|
||||
|
||||
@@ -148,6 +156,13 @@ const unusedSupermarketCategories = computed(() => {
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor(){
|
||||
const api = new ApiApi()
|
||||
|
||||
api.apiSupermarketCategoryList({pageSize: 100}).then(r => {
|
||||
@@ -161,7 +176,7 @@ onMounted(() => {
|
||||
itemDefaults: props.itemDefaults,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* called whenever something in the list is moved to track the last moved element (to be used in add/remove functions)
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, PropType} from "vue";
|
||||
import {onMounted, PropType, watch} from "vue";
|
||||
import { Sync} from "@/openapi";
|
||||
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
||||
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
||||
@@ -42,12 +42,27 @@ const props = defineProps({
|
||||
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
|
||||
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<Sync>('Sync', emit)
|
||||
|
||||
/**
|
||||
* watch prop changes and re-initialize editor
|
||||
* required to embed editor directly into pages and be able to change item from the outside
|
||||
*/
|
||||
watch([() => props.item, () => props.itemId], () => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
// object specific data (for selects/display)
|
||||
|
||||
onMounted(() => {
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor(){
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, PropType} from "vue";
|
||||
import {onMounted, PropType, watch} from "vue";
|
||||
import {UnitConversion} from "@/openapi";
|
||||
|
||||
import ModelSelect from "@/components/inputs/ModelSelect.vue";
|
||||
@@ -64,11 +64,27 @@ const props = defineProps({
|
||||
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
|
||||
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<UnitConversion>('UnitConversion', emit)
|
||||
|
||||
/**
|
||||
* watch prop changes and re-initialize editor
|
||||
* required to embed editor directly into pages and be able to change item from the outside
|
||||
*/
|
||||
watch([() => props.item, () => props.itemId], () => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
// object specific data (for selects/display)
|
||||
|
||||
onMounted(() => {
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor(){
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, PropType} from "vue";
|
||||
import {onMounted, PropType, watch} from "vue";
|
||||
import {Unit} from "@/openapi";
|
||||
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
||||
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
||||
@@ -41,6 +41,13 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
|
||||
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<Unit>('Unit', emit)
|
||||
/**
|
||||
* watch prop changes and re-initialize editor
|
||||
* required to embed editor directly into pages and be able to change item from the outside
|
||||
*/
|
||||
watch([() => props.item, () => props.itemId], () => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
// object specific data (for selects/display)
|
||||
|
||||
@@ -67,9 +74,16 @@ const BASE_UNITS = [
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor(){
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, PropType, shallowRef} from "vue";
|
||||
import {onMounted, PropType, shallowRef, watch} from "vue";
|
||||
import {UserFile, UserSpace} from "@/openapi";
|
||||
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
||||
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
||||
@@ -66,15 +66,30 @@ const props = defineProps({
|
||||
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
|
||||
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<UserFile>('UserFile', emit)
|
||||
|
||||
/**
|
||||
* watch prop changes and re-initialize editor
|
||||
* required to embed editor directly into pages and be able to change item from the outside
|
||||
*/
|
||||
watch([() => props.item, () => props.itemId], () => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
// object specific data (for selects/display)
|
||||
|
||||
const {fileApiLoading, createOrUpdateUserFile} = useFileApi()
|
||||
const file = shallowRef<File | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor(){
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
}
|
||||
|
||||
/**
|
||||
* save file to database via fileApi composable
|
||||
*/
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, PropType, ref} from "vue";
|
||||
import {onMounted, PropType, ref, watch} from "vue";
|
||||
import {ApiApi, Group, UserSpace} from "@/openapi";
|
||||
|
||||
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
|
||||
@@ -39,10 +39,25 @@ const props = defineProps({
|
||||
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
|
||||
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<UserSpace>('UserSpace', emit)
|
||||
|
||||
/**
|
||||
* watch prop changes and re-initialize editor
|
||||
* required to embed editor directly into pages and be able to change item from the outside
|
||||
*/
|
||||
watch([() => props.item, () => props.itemId], () => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
// object specific data (for selects/display)
|
||||
const groups = ref([] as Group[])
|
||||
|
||||
onMounted(() => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor(){
|
||||
const api = new ApiApi()
|
||||
api.apiGroupList().then(r => {
|
||||
groups.value = r
|
||||
@@ -51,8 +66,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
81
vue3/src/composables/useNavigation.ts
Normal file
81
vue3/src/composables/useNavigation.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {VDivider, VListItem} from "vuetify/components";
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
|
||||
import {useDjangoUrls} from "@/composables/useDjangoUrls.ts";
|
||||
|
||||
/**
|
||||
* manages configuration and loading of navigation entries for tandoor main app and plugins
|
||||
*/
|
||||
export function useNavigation() {
|
||||
const {t} = useI18n()
|
||||
|
||||
let NAVIGATION_DRAWER = [
|
||||
{component: VListItem, prependIcon: '$recipes', title: 'Home', to: {name: 'StartPage', params: {}}},
|
||||
{component: VListItem, prependIcon: '$search', title: t('Search'), to: {name: 'SearchPage', params: {}}},
|
||||
{component: VListItem, prependIcon: '$mealplan', title: t('Meal_Plan'), to: {name: 'MealPlanPage', params: {}}},
|
||||
{component: VListItem, prependIcon: '$shopping', title: t('Shopping_list'), to: {name: 'ShoppingListPage', params: {}}},
|
||||
{component: VListItem, prependIcon: 'fas fa-globe', title: t('Import'), to: {name: 'RecipeImportPage', params: {}}},
|
||||
{component: VListItem, prependIcon: '$books', title: t('Books'), to: {name: 'BooksPage', params: {}}},
|
||||
{component: VListItem, prependIcon: 'fa-solid fa-folder-tree', title: t('DatabasePage'), to: {name: 'SearchPage', params: {}}},
|
||||
]
|
||||
|
||||
let BOTTOM_NAVIGATION = [
|
||||
{component: VListItem, prependIcon: 'fa-solid fa-sliders', title: t('Settings'), to: {name: 'SettingsPage', params: {}}},
|
||||
{component: VListItem, prependIcon: 'fas fa-globe', title: t('Import'), to: {name: 'RecipeImportPage', params: {}}},
|
||||
{component: VListItem, prependIcon: 'fa-solid fa-folder-tree', title: t('Database'), to: {name: 'DatabasePage', params: {}}},
|
||||
{component: VListItem, prependIcon: '$books', title: t('Books'), to: {name: 'BooksPage', params: {}}},
|
||||
]
|
||||
|
||||
let USER_NAVIGATION = [
|
||||
{component: VListItem, prependIcon: 'fa-solid fa-sliders', title: t('Settings'), to: {name: 'SettingsPage', params: {}}},
|
||||
{component: VListItem, prependIcon: 'fa-solid fa-question', title: t('Settings'), to: {name: 'HelpPage', params: {}}},
|
||||
]
|
||||
|
||||
function getUserNavigation() {
|
||||
let navigation = []
|
||||
|
||||
navigation.push({component: VListItem, prependIcon: 'fa-solid fa-sliders', title: t('Settings'), to: {name: 'SettingsPage', params: {}}})
|
||||
navigation.push({component: VListItem, prependIcon: 'fa-solid fa-question', title: t('Help'), to: {name: 'HelpPage', params: {}}})
|
||||
|
||||
if (useUserPreferenceStore().userSettings.user.isSuperuser) {
|
||||
navigation.push({component: VListItem, prependIcon: 'fa-solid fa-shield', title: t('Admin'), href: useDjangoUrls().getDjangoUrl('admin')})
|
||||
}
|
||||
|
||||
if (useUserPreferenceStore().spaces.length > 1) {
|
||||
navigation.push({component: VDivider})
|
||||
useUserPreferenceStore().spaces.forEach(space => {
|
||||
navigation.push({
|
||||
component: VListItem,
|
||||
prependIcon: (useUserPreferenceStore().activeSpace.id == space.id) ? 'fa-solid fa-circle-dot' : 'fa-solid fa-circle',
|
||||
title: space.name,
|
||||
onClick: () => {
|
||||
useUserPreferenceStore().switchSpace(space)
|
||||
}
|
||||
})
|
||||
})
|
||||
navigation.push({component: VDivider})
|
||||
}
|
||||
|
||||
navigation.push({component: VListItem, prependIcon: 'fa-solid fa-arrow-right-from-bracket', title: t('Logout'), href: useDjangoUrls().getDjangoUrl('accounts/logout')})
|
||||
|
||||
return navigation
|
||||
}
|
||||
|
||||
return {NAVIGATION_DRAWER, BOTTOM_NAVIGATION, USER_NAVIGATION, getUserNavigation}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
|
||||
// <v-list-item :href="getDjangoUrl('admin')" target="_blank" v-if="useUserPreferenceStore().userSettings.user.isSuperuser">
|
||||
// <template #prepend>
|
||||
// <v-icon icon="fa-solid fa-shield"></v-icon>
|
||||
// </template>
|
||||
// {{ $t('Admin') }}
|
||||
// </v-list-item>
|
||||
// <v-list-item :href="getDjangoUrl('accounts/logout')" link>
|
||||
// <template #prepend>
|
||||
// <v-icon icon="fa-solid fa-arrow-right-from-bracket"></v-icon>
|
||||
// </template>
|
||||
// {{ $t('Logout') }}
|
||||
// </v-list-item>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -90,6 +90,7 @@
|
||||
|
||||
import {TKeyword, TRecipe, TUnit, TUserSpace} from "@/types/Models";
|
||||
import {useUserPreferenceStore} from "../stores/UserPreferenceStore";
|
||||
import HelpDialog from "@/components/dialogs/HelpDialog.vue";
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -29,6 +29,12 @@
|
||||
<model-merge-dialog :source="selectedFood" model="Food"
|
||||
@change="(obj: Food) => {selectedFood = obj;refreshPage()} "></model-merge-dialog>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link prepend-icon="$automation" :disabled="!selectedFood">
|
||||
{{ $t('Automate') }}
|
||||
<model-edit-dialog model="Automation" activator="parent" :item-defaults="{param1: selectedFood.name, type: 'FOOD_ALIAS'}" v-if="selectedFood"></model-edit-dialog>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link prepend-icon="$delete" :disabled="!selectedFood">
|
||||
{{ $t('Delete') }}
|
||||
<delete-confirm-dialog :model-name="$t('Food')" :object-name="selectedFood.name" v-if="selectedFood"
|
||||
@@ -58,6 +64,10 @@
|
||||
<model-merge-dialog :source="selectedUnit" model="Unit"
|
||||
@change="(obj: Food) => {selectedUnit = obj;refreshPage()} "></model-merge-dialog>
|
||||
</v-list-item>
|
||||
<v-list-item link prepend-icon="$automation" :disabled="!selectedUnit">
|
||||
{{ $t('Automate') }}
|
||||
<model-edit-dialog model="Automation" activator="parent" :item-defaults="{param1: selectedUnit.name, type: 'UNIT_ALIAS'}" v-if="selectedUnit"></model-edit-dialog>
|
||||
</v-list-item>
|
||||
<v-list-item link prepend-icon="$delete" :disabled="!selectedUnit">
|
||||
{{ $t('Delete') }}
|
||||
<delete-confirm-dialog :model-name="$t('Unit')" :object-name="selectedUnit.name" v-if="selectedUnit"
|
||||
|
||||
@@ -32,7 +32,7 @@ const props = defineProps({
|
||||
id: {type: String, required: false, default: undefined},
|
||||
})
|
||||
|
||||
const editorComponent = shallowRef(defineAsyncComponent(() => import(`@/components/model_editors/${getGenericModelFromString(props.model, t).model.name}Editor.vue`)))
|
||||
const editorComponent = shallowRef(getGenericModelFromString(props.model, t).model.editorComponent)
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
|
||||
@@ -187,6 +187,7 @@ import VClosableCardTitle from "@/components/dialogs/VClosableCardTitle.vue";
|
||||
import {useUrlSearchParams} from "@vueuse/core";
|
||||
import BtnCopy from "@/components/buttons/BtnCopy.vue";
|
||||
import FdcSearchDialog from "@/components/dialogs/FdcSearchDialog.vue";
|
||||
import {openFdcPage} from "@/utils/fdc.ts";
|
||||
|
||||
type IngredientLoading = Ingredient & { loading?: boolean }
|
||||
|
||||
@@ -346,8 +347,8 @@ function updateFood(ingredient: IngredientLoading) {
|
||||
*/
|
||||
function updateFoodFdcData(ingredient: IngredientLoading) {
|
||||
let api = new ApiApi()
|
||||
ingredient.loading = true
|
||||
if (ingredient.food.fdcId) {
|
||||
ingredient.loading = true
|
||||
api.apiFoodFdcCreate({id: ingredient.food.id!, food: ingredient.food}).then(r => {
|
||||
ingredient.food = r
|
||||
ingredients.value.set(r.id!, buildIngredientFoodProperties(ingredient))
|
||||
@@ -381,13 +382,6 @@ function changeAllPropertyFoodAmounts(amount: number) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* for some reason v-btn href does not work in append inner slot of text field so open link with js
|
||||
* @param fdcId
|
||||
*/
|
||||
function openFdcPage(fdcId: number){
|
||||
window.open(`https://fdc.nal.usda.gov/food-details/${fdcId}/nutrients`, '_blank')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import {DateTime} from "luxon";
|
||||
|
||||
export default {
|
||||
install: (app: any) => {
|
||||
// inject a globally available luxon DateTime
|
||||
app.config.globalProperties.$luxon = DateTime
|
||||
}
|
||||
}
|
||||
26
vue3/src/plugins/open_data_plugin/OpenDataPage.vue
Normal file
26
vue3/src/plugins/open_data_plugin/OpenDataPage.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<v-container>
|
||||
Welcome to the OpenData Plugin in Tandoor 2
|
||||
<model-select model="OpenDataFood" allow-create></model-select>
|
||||
|
||||
</v-container>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, ref} from "vue";
|
||||
import ModelSelect from "@/components/inputs/ModelSelect.vue";
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<model-editor-base
|
||||
:loading="loading"
|
||||
:dialog="dialog"
|
||||
@save="saveObject"
|
||||
@delete="deleteObject"
|
||||
@close="emit('close'); editingObjChanged = false"
|
||||
:is-update="isUpdate()"
|
||||
:is-changed="editingObjChanged"
|
||||
:model-class="modelClass"
|
||||
:object-name="editingObjName()">
|
||||
<v-card-text>
|
||||
<v-form :disabled="loading">
|
||||
|
||||
<v-text-field :label="$t('Name')" v-model="editingObj.name"></v-text-field>
|
||||
|
||||
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</model-editor-base>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, PropType} from "vue";
|
||||
import {Keyword, OpenDataFood} from "@/openapi";
|
||||
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
||||
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
||||
|
||||
const props = defineProps({
|
||||
item: {type: {} as PropType<OpenDataFood>, required: false, default: null},
|
||||
itemId: {type: [Number, String], required: false, default: undefined},
|
||||
itemDefaults: {type: {} as PropType<OpenDataFood>, required: false, default: {} as OpenDataFood},
|
||||
dialog: {type: Boolean, default: false}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
|
||||
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<OpenDataFood>('OpenDataFood', emit)
|
||||
|
||||
// object specific data (for selects/display)
|
||||
|
||||
onMounted(() => {
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
32
vue3/src/plugins/open_data_plugin/plugin.ts
Normal file
32
vue3/src/plugins/open_data_plugin/plugin.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {TandoorPlugin} from '@/types/Plugins.ts'
|
||||
import {Model, registerModel} from "@/types/Models.ts";
|
||||
import {defineAsyncComponent} from "vue";
|
||||
|
||||
export const plugin: TandoorPlugin = {
|
||||
name: 'Open Data Plugin',
|
||||
routes: [
|
||||
{path: '/open-data/', component: () => import("@/plugins/open_data_plugin/OpenDataPage.vue"), name: 'OpenDataPage'},
|
||||
]
|
||||
} as TandoorPlugin
|
||||
|
||||
|
||||
// define models below
|
||||
|
||||
const TOpenDataFood = {
|
||||
name: 'OpenDataFood',
|
||||
localizationKey: 'Food',
|
||||
localizationKeyDescription: 'FoodHelp',
|
||||
icon: 'fa-solid fa-carrot',
|
||||
|
||||
editorComponent: defineAsyncComponent(() => import(`@/plugins/open_data_plugin/components/model_editors/OpenDataFoodEditor.vue`)),
|
||||
|
||||
isPaginated: false,
|
||||
isMerge: false,
|
||||
toStringKeys: ['name'],
|
||||
|
||||
tableHeaders: [
|
||||
{title: 'Name', key: 'name'},
|
||||
{title: 'Actions', key: 'action', align: 'end'},
|
||||
]
|
||||
} as Model
|
||||
registerModel(TOpenDataFood)
|
||||
@@ -1,12 +1,11 @@
|
||||
import {acceptHMRUpdate, defineStore} from 'pinia'
|
||||
import {useStorage} from "@vueuse/core";
|
||||
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
|
||||
import {ApiApi, ServerSettings, Space, Supermarket, UserPreference, UserSpace} from "@/openapi";
|
||||
import {ApiApi, ServerSettings, Space, UserPreference, UserSpace} from "@/openapi";
|
||||
import {ShoppingGroupingOptions} from "@/types/Shopping";
|
||||
import {computed, ComputedRef, ref} from "vue";
|
||||
import {DeviceSettings} from "@/types/settings";
|
||||
import {useTheme} from "vuetify";
|
||||
import tandoorDarkCustomCss from '@/assets/tandoor_dark.css?inline'
|
||||
|
||||
const DEVICE_SETTINGS_KEY = 'TANDOOR_DEVICE_SETTINGS'
|
||||
const USER_PREFERENCE_KEY = 'TANDOOR_USER_PREFERENCE'
|
||||
@@ -209,23 +208,10 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
|
||||
* applies user settings regarding themes/styling
|
||||
*/
|
||||
function updateTheme() {
|
||||
let customStyleTag = document.getElementById('id_style_custom_css')
|
||||
|
||||
if (userSettings.value.theme == 'TANDOOR') {
|
||||
theme.global.name.value = 'light'
|
||||
|
||||
if (customStyleTag) {
|
||||
document.head.removeChild(customStyleTag)
|
||||
}
|
||||
theme.change('light')
|
||||
} else if (userSettings.value.theme == 'TANDOOR_DARK') {
|
||||
theme.global.name.value = 'dark'
|
||||
|
||||
if (!customStyleTag) {
|
||||
const styleTag = document.createElement('style')
|
||||
styleTag.id = "id_style_custom_css"
|
||||
styleTag.innerHTML = tandoorDarkCustomCss
|
||||
document.head.appendChild(styleTag)
|
||||
}
|
||||
theme.change('dark')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import {VDataTable} from "vuetify/components";
|
||||
import {getNestedProperty} from "@/utils/utils";
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
||||
import {defineAsyncComponent, shallowRef} from "vue";
|
||||
|
||||
type VDataTableProps = InstanceType<typeof VDataTable>['$props']
|
||||
|
||||
@@ -40,7 +41,7 @@ export function getGenericModelFromString(modelName: EditorSupportedModels, t: a
|
||||
* register a given model instance in the supported models list
|
||||
* @param model model to register
|
||||
*/
|
||||
function registerModel(model: Model) {
|
||||
export function registerModel(model: Model) {
|
||||
SUPPORTED_MODELS.set(model.name.toLowerCase(), model)
|
||||
}
|
||||
|
||||
@@ -90,6 +91,8 @@ export type Model = {
|
||||
icon: string,
|
||||
toStringKeys: Array<string>,
|
||||
|
||||
editorComponent?: any,
|
||||
|
||||
itemValue: string | undefined,
|
||||
itemLabel: string | undefined,
|
||||
|
||||
@@ -184,6 +187,8 @@ export const TFood = {
|
||||
localizationKeyDescription: 'FoodHelp',
|
||||
icon: 'fa-solid fa-carrot',
|
||||
|
||||
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/FoodEditor.vue`)),
|
||||
|
||||
isPaginated: true,
|
||||
isMerge: true,
|
||||
mergeAutomation: 'FOOD_ALIAS',
|
||||
@@ -204,6 +209,8 @@ export const TUnit = {
|
||||
localizationKeyDescription: 'UnitHelp',
|
||||
icon: 'fa-solid fa-scale-balanced',
|
||||
|
||||
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/UnitEditor.vue`)),
|
||||
|
||||
isPaginated: true,
|
||||
isMerge: true,
|
||||
mergeAutomation: 'UNIT_ALIAS',
|
||||
@@ -223,6 +230,8 @@ export const TKeyword = {
|
||||
localizationKeyDescription: 'KeywordHelp',
|
||||
icon: 'fa-solid fa-tags',
|
||||
|
||||
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/KeywordEditor.vue`)),
|
||||
|
||||
isPaginated: true,
|
||||
isMerge: true,
|
||||
mergeAutomation: 'KEYWORD_ALIAS',
|
||||
@@ -241,6 +250,8 @@ export const TRecipe = {
|
||||
localizationKeyDescription: 'RecipeHelp',
|
||||
icon: 'fa-solid fa-book',
|
||||
|
||||
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/RecipeEditor.vue`)),
|
||||
|
||||
isPaginated: true,
|
||||
toStringKeys: ['name'],
|
||||
|
||||
@@ -295,6 +306,8 @@ export const TMealType = {
|
||||
localizationKeyDescription: 'MealTypeHelp',
|
||||
icon: 'fa-solid fa-utensils',
|
||||
|
||||
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/MealTypeEditor.vue`)),
|
||||
|
||||
isPaginated: true,
|
||||
toStringKeys: ['name'],
|
||||
|
||||
@@ -311,6 +324,8 @@ export const TMealPlan = {
|
||||
localizationKeyDescription: 'MealPlanHelp',
|
||||
icon: 'fa-solid fa-calendar-days',
|
||||
|
||||
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/MealPlanEditor.vue`)),
|
||||
|
||||
isPaginated: true,
|
||||
toStringKeys: ['title', 'recipe.name'],
|
||||
|
||||
@@ -330,6 +345,8 @@ export const TRecipeBook = {
|
||||
localizationKeyDescription: 'RecipeBookHelp',
|
||||
icon: 'fa-solid fa-book-bookmark',
|
||||
|
||||
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/RecipeBookEditor.vue`)),
|
||||
|
||||
isPaginated: true,
|
||||
toStringKeys: ['name'],
|
||||
|
||||
@@ -367,6 +384,8 @@ export const TCustomFilter = {
|
||||
localizationKeyDescription: 'SavedSearchHelp',
|
||||
icon: 'fa-solid fa-filter',
|
||||
|
||||
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/CustomFilterEditor.vue`)),
|
||||
|
||||
isPaginated: true,
|
||||
toStringKeys: ['name'],
|
||||
|
||||
@@ -405,6 +424,8 @@ export const TSupermarket = {
|
||||
localizationKeyDescription: 'SupermarketHelp',
|
||||
icon: 'fa-solid fa-store',
|
||||
|
||||
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/SupermarketEditor.vue`)),
|
||||
|
||||
isPaginated: true,
|
||||
toStringKeys: ['name'],
|
||||
|
||||
@@ -421,6 +442,8 @@ export const TSupermarketCategory = {
|
||||
localizationKeyDescription: 'SupermarketCategoryHelp',
|
||||
icon: 'fa-solid fa-boxes-stacked',
|
||||
|
||||
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/SupermarketCategoryEditor.vue`)),
|
||||
|
||||
isPaginated: true,
|
||||
isMerge: true,
|
||||
toStringKeys: ['name'],
|
||||
@@ -438,6 +461,8 @@ export const TShoppingListEntry = {
|
||||
localizationKeyDescription: 'ShoppingListEntryHelp',
|
||||
icon: 'fa-solid fa-list-check',
|
||||
|
||||
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/ShoppingListEntryEditor.vue`)),
|
||||
|
||||
disableListView: true,
|
||||
isPaginated: true,
|
||||
toStringKeys: ['amount', 'unit.name', 'food.name'],
|
||||
@@ -457,6 +482,8 @@ export const TPropertyType = {
|
||||
localizationKeyDescription: 'PropertyTypeHelp',
|
||||
icon: 'fa-solid fa-database',
|
||||
|
||||
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/PropertyTypeEditor.vue`)),
|
||||
|
||||
isPaginated: true,
|
||||
toStringKeys: ['name'],
|
||||
|
||||
@@ -473,6 +500,8 @@ export const TProperty = {
|
||||
localizationKeyDescription: 'PropertyHelp',
|
||||
icon: 'fa-solid fa-database',
|
||||
|
||||
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/PropertyEditor.vue`)),
|
||||
|
||||
disableListView: true,
|
||||
isPaginated: true,
|
||||
toStringKeys: ['propertyAmount', 'propertyType.name'],
|
||||
@@ -491,6 +520,8 @@ export const TUnitConversion = {
|
||||
localizationKeyDescription: 'UnitConversionHelp',
|
||||
icon: 'fa-solid fa-exchange-alt',
|
||||
|
||||
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/UnitConversionEditor.vue`)),
|
||||
|
||||
isPaginated: true,
|
||||
toStringKeys: ['food.name', 'baseUnit.name', 'convertedUnit.name'],
|
||||
|
||||
@@ -511,6 +542,8 @@ export const TUserFile = {
|
||||
localizationKeyDescription: 'UserFileHelp',
|
||||
icon: 'fa-solid fa-file',
|
||||
|
||||
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/UserFileEditor.vue`)),
|
||||
|
||||
isPaginated: true,
|
||||
toStringKeys: ['name'],
|
||||
|
||||
@@ -527,6 +560,8 @@ export const TAutomation = {
|
||||
localizationKeyDescription: 'AutomationHelp',
|
||||
icon: 'fa-solid fa-robot',
|
||||
|
||||
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/AutomationEditor.vue`)),
|
||||
|
||||
isPaginated: true,
|
||||
toStringKeys: ['name'],
|
||||
|
||||
@@ -584,6 +619,8 @@ export const TAccessToken = {
|
||||
localizationKeyDescription: 'AccessTokenHelp',
|
||||
icon: 'fa-solid fa-key',
|
||||
|
||||
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/AccessTokenEditor.vue`)),
|
||||
|
||||
disableListView: true,
|
||||
isPaginated: true,
|
||||
toStringKeys: ['token'],
|
||||
@@ -602,6 +639,8 @@ export const TUserSpace = {
|
||||
localizationKeyDescription: 'SpaceMembersHelp',
|
||||
icon: 'fa-solid fa-users',
|
||||
|
||||
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/UserSpaceEditor.vue`)),
|
||||
|
||||
disableListView: true,
|
||||
isPaginated: true,
|
||||
toStringKeys: ['user.displayName'],
|
||||
@@ -621,6 +660,8 @@ export const TInviteLink = {
|
||||
localizationKeyDescription: 'InviteLinkHelp',
|
||||
icon: 'fa-solid fa-link',
|
||||
|
||||
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/InviteLinkEditor.vue`)),
|
||||
|
||||
disableListView: true,
|
||||
isPaginated: true,
|
||||
toStringKeys: ['email', 'role'],
|
||||
@@ -640,6 +681,8 @@ export const TStorage = {
|
||||
localizationKeyDescription: 'StorageHelp',
|
||||
icon: 'fa-solid fa-cloud',
|
||||
|
||||
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/StorageEditor.vue`)),
|
||||
|
||||
disableListView: false,
|
||||
toStringKeys: ['name'],
|
||||
isPaginated: true,
|
||||
@@ -657,6 +700,8 @@ export const TSync = {
|
||||
localizationKeyDescription: 'SyncedPathHelp',
|
||||
icon: 'fa-solid fa-folder-plus',
|
||||
|
||||
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/SyncEditor.vue`)),
|
||||
|
||||
disableListView: false,
|
||||
toStringKeys: ['path'],
|
||||
isPaginated: true,
|
||||
@@ -722,6 +767,8 @@ export const TConnectorConfig = {
|
||||
localizationKeyDescription: 'ConnectorConfigHelp',
|
||||
icon: 'fa-solid fa-arrows-turn-to-dots',
|
||||
|
||||
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/ConnectorConfigEditor.vue`)),
|
||||
|
||||
disableListView: false,
|
||||
toStringKeys: ['name'],
|
||||
isPaginated: true,
|
||||
@@ -896,7 +943,7 @@ export class GenericModel {
|
||||
throw new Error('Cannot merge on this model!')
|
||||
} else {
|
||||
let mergeRequestParams: any = {id: source.id, target: target.id}
|
||||
mergeRequestParams[this.model.name.toLowerCase()] = {}
|
||||
mergeRequestParams[this.model.name.charAt(0).toLowerCase() + this.model.name.slice(1)] = {}
|
||||
|
||||
return this.api[`api${this.model.name}MergeUpdate`](mergeRequestParams)
|
||||
}
|
||||
|
||||
7
vue3/src/types/Plugins.ts
Normal file
7
vue3/src/types/Plugins.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {RouteRecordRaw} from "vue-router";
|
||||
import {Model, registerModel} from "@/types/Models.ts";
|
||||
|
||||
export type TandoorPlugin = {
|
||||
name: string,
|
||||
routes: RouteRecordRaw[]
|
||||
}
|
||||
@@ -1,4 +1,14 @@
|
||||
/**
|
||||
* for some reason v-btn href does not work in append inner slot of text field so open link with js
|
||||
* @param fdcId
|
||||
*/
|
||||
export function openFdcPage(fdcId: number){
|
||||
window.open(`https://fdc.nal.usda.gov/food-details/${fdcId}/nutrients`, '_blank')
|
||||
}
|
||||
|
||||
/**
|
||||
* different types defined in the FDC Database
|
||||
*/
|
||||
export const FDC_PROPERTY_TYPES = [
|
||||
{value: 1002, text: "Nitrogen [g] (1002)"},
|
||||
{value: 1003, text: "Protein [g] (1003)"},
|
||||
|
||||
@@ -98,6 +98,7 @@ export default createVuetify({
|
||||
menu: 'fa-solid fa-ellipsis-vertical',
|
||||
import: 'fa-solid fa-globe',
|
||||
properties: 'fa-solid fa-database',
|
||||
automation: 'fa-solid fa-robot',
|
||||
ai: 'fa-solid fa-wand-magic-sparkles'
|
||||
},
|
||||
sets: {
|
||||
|
||||
@@ -959,26 +959,26 @@
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz#8249de9b7e22fcb3ceb5e66090c30a1d5492b81a"
|
||||
integrity sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==
|
||||
|
||||
"@intlify/core-base@11.1.7":
|
||||
version "11.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-11.1.7.tgz#497280e4774011cf0d42eaedb20e9cd4594c0a3f"
|
||||
integrity sha512-gYiGnQeJVp3kNBeXQ73m1uFOak0ry4av8pn+IkEWigyyPWEMGzB+xFeQdmGMFn49V+oox6294oGVff8bYOhtOw==
|
||||
"@intlify/core-base@11.1.10":
|
||||
version "11.1.10"
|
||||
resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-11.1.10.tgz#4731748992bc6d8e723ca6c2cc5aa5a4c90cf7a5"
|
||||
integrity sha512-JhRb40hD93Vk0BgMgDc/xMIFtdXPHoytzeK6VafBNOj6bb6oUZrGamXkBKecMsmGvDQQaPRGG2zpa25VCw8pyw==
|
||||
dependencies:
|
||||
"@intlify/message-compiler" "11.1.7"
|
||||
"@intlify/shared" "11.1.7"
|
||||
"@intlify/message-compiler" "11.1.10"
|
||||
"@intlify/shared" "11.1.10"
|
||||
|
||||
"@intlify/message-compiler@11.1.7":
|
||||
version "11.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-11.1.7.tgz#047ba659cfd34b0f630dddf73c3f9224bd3af7f8"
|
||||
integrity sha512-0ezkep1AT30NyuKj8QbRlmvMORCCRlOIIu9v8RNU8SwDjjTiFCZzczCORMns2mCH4HZ1nXgrfkKzYUbfjNRmng==
|
||||
"@intlify/message-compiler@11.1.10":
|
||||
version "11.1.10"
|
||||
resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-11.1.10.tgz#ff5c92c311cd72144126f5c128912adb4e911207"
|
||||
integrity sha512-TABl3c8tSLWbcD+jkQTyBhrnW251dzqW39MPgEUCsd69Ua3ceoimsbIzvkcPzzZvt1QDxNkenMht+5//V3JvLQ==
|
||||
dependencies:
|
||||
"@intlify/shared" "11.1.7"
|
||||
"@intlify/shared" "11.1.10"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
"@intlify/shared@11.1.7":
|
||||
version "11.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-11.1.7.tgz#54e60d52b73fb25019e2689d6531a54928b40194"
|
||||
integrity sha512-4yZeMt2Aa/7n5Ehy4KalUlvt3iRLcg1tq9IBVfOgkyWFArN4oygn6WxgGIFibP3svpaH8DarbNaottq+p0gUZQ==
|
||||
"@intlify/shared@11.1.10":
|
||||
version "11.1.10"
|
||||
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-11.1.10.tgz#d869aa8fbc1aa307f26a58848fea6df3c9785b6f"
|
||||
integrity sha512-6ZW/f3Zzjxfa1Wh0tYQI5pLKUtU+SY7l70pEG+0yd0zjcsYcK0EBt6Fz30Dy0tZhEqemziQQy2aNU3GJzyrMUA==
|
||||
|
||||
"@jridgewell/gen-mapping@^0.3.5":
|
||||
version "0.3.8"
|
||||
@@ -3363,13 +3363,13 @@ vue-draggable-plus@^0.6.0:
|
||||
dependencies:
|
||||
"@types/sortablejs" "^1.15.8"
|
||||
|
||||
vue-i18n@^11.1.7:
|
||||
version "11.1.7"
|
||||
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-11.1.7.tgz#a26c0224d1311ac89b82ff6d0ee45f68b5099237"
|
||||
integrity sha512-CDrU7Cmyh1AxJjerQmipV9nVa//exVBdhTcWGlbfcDCN8bKp/uAe7Le6IoN4//5emIikbsSKe9Uofmf/xXkhOA==
|
||||
vue-i18n@^11.1.10:
|
||||
version "11.1.10"
|
||||
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-11.1.10.tgz#04578ea4213f96c37939a08f516a648a6d0b84a1"
|
||||
integrity sha512-C+IwnSg8QDSOAox0gdFYP5tsKLx5jNWxiawNoiNB/Tw4CReXmM1VJMXbduhbrEzAFLhreqzfDocuSVjGbxQrag==
|
||||
dependencies:
|
||||
"@intlify/core-base" "11.1.7"
|
||||
"@intlify/shared" "11.1.7"
|
||||
"@intlify/core-base" "11.1.10"
|
||||
"@intlify/shared" "11.1.10"
|
||||
"@vue/devtools-api" "^6.5.0"
|
||||
|
||||
vue-router@^4.5.0:
|
||||
@@ -3412,10 +3412,10 @@ vuedraggable@^4.1.0:
|
||||
dependencies:
|
||||
sortablejs "1.14.0"
|
||||
|
||||
vuetify@^3.8.12:
|
||||
version "3.8.12"
|
||||
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.8.12.tgz#7c433b8b036011bb0a800f08f5a53d61067eeae8"
|
||||
integrity sha512-XRX/yRel/V5rlas12ovujVCo8RDb/NwICyef/DVYzybqbYz/UGHZd23sN5q1zw0h9jUN8httXI6ytWN7OFugmA==
|
||||
vuetify@^3.9.0:
|
||||
version "3.9.0"
|
||||
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.9.0.tgz#e9412e9ba662fcd4b9946c589a984d157460943a"
|
||||
integrity sha512-vjqyHP5gBFH4x0BAjdRAcS3FXY5OfHaKnC6Hhgln8tePZtKc3AUhF7BEJtcrD3l6XwL8gaYx/wMt+UP7X5EZJw==
|
||||
|
||||
w3c-xmlserializer@^5.0.0:
|
||||
version "5.0.0"
|
||||
|
||||
Reference in New Issue
Block a user