Compare commits

..

59 Commits

Author SHA1 Message Date
vabene1111
ad6fe5fa4d Merge branch 'develop' into beta 2025-07-18 15:48:02 +02:00
Matjaž T
034d59373f Translated using Weblate (Slovenian)
Currently translated at 100.0% (793 of 793 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/sl/
2025-07-18 08:00:26 +00:00
vabene1111
d1ad0ade0f playing with programmatic navigation creation 2025-07-17 16:21:10 +02:00
vabene1111
991089c17a various little fixes 2025-07-17 16:04:31 +02:00
vabene1111
54960d8480 basics of an open data plugin (tmp in this repo)
temporarily in the main repo while testing and playing around
2025-07-17 15:40:05 +02:00
vabene1111
5fcfe09bb6 added basic plugin support 2025-07-17 15:34:51 +02:00
vabene1111
01c4974507 Merge pull request #3815 from caffeinated-tech/cookbookapp-images-import
Cookbookapp images import
2025-07-17 09:44:27 +02:00
vabene1111
2d57e0dab2 Merge pull request #3832 from TandoorRecipes/dependabot/npm_and_yarn/vue3/vue-i18n-11.1.10
Bump vue-i18n from 11.1.7 to 11.1.10 in /vue3
2025-07-17 08:11:05 +02:00
vabene1111
d52e5408c0 Merge pull request #3825 from TandoorRecipes/dependabot/pip/aiohttp-3.12.14
Bump aiohttp from 3.10.11 to 3.12.14
2025-07-17 08:10:48 +02:00
dependabot[bot]
fdce69daf4 Bump vue-i18n from 11.1.7 to 11.1.10 in /vue3
Bumps [vue-i18n](https://github.com/intlify/vue-i18n/tree/HEAD/packages/vue-i18n) from 11.1.7 to 11.1.10.
- [Release notes](https://github.com/intlify/vue-i18n/releases)
- [Changelog](https://github.com/intlify/vue-i18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/intlify/vue-i18n/commits/v11.1.10/packages/vue-i18n)

---
updated-dependencies:
- dependency-name: vue-i18n
  dependency-version: 11.1.10
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-16 19:36:39 +00:00
Vincenzo Reale
cb3ffcb12d Translated using Weblate (Italian)
Currently translated at 100.0% (793 of 793 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/it/
2025-07-16 03:44:42 +00:00
vabene1111
d7342a349b Merge branch 'develop' of http://translate.tandoor.dev/git/tandoor/recipes-backend into develop
# Conflicts:
#	vue3/src/locales/es.json
#	vue3/src/locales/it.json
#	vue3/src/locales/pt_BR.json
#	vue3/src/locales/sl.json
2025-07-15 12:59:52 +02:00
dependabot[bot]
794bbed833 Bump aiohttp from 3.10.11 to 3.12.14
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.10.11 to 3.12.14.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.10.11...v3.12.14)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-version: 3.12.14
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-15 09:25:01 +00:00
vabene1111
0b335e80a6 first help dialog draft 2025-07-11 22:54:03 +02:00
vabene1111
2716d72e31 fixed redis settings? 2025-07-11 22:22:31 +02:00
vabene1111
ac31c112f3 Merge branch 'develop' into beta 2025-07-11 21:59:18 +02:00
vabene1111
8c849a1077 added redis as cache backend 2025-07-11 21:51:54 +02:00
vabene1111
8c1769458d multiselect CSS for every variant and fix is-selected class 2025-07-11 21:17:55 +02:00
vabene1111
2ac6451370 added FDC import to food editor 2025-07-11 21:08:56 +02:00
vabene1111
7841397b59 fixed step editor recipe selection 2025-07-11 20:51:21 +02:00
vabene1111
cd11194ce5 fixed css 2025-07-11 20:07:25 +02:00
vabene1111
be7558f82b allow automation directly from ingredient editor 2025-07-11 19:59:10 +02:00
vabene1111
35a7875f6f fixed generic model merge for multi word model names 2025-07-11 19:50:17 +02:00
vabene1111
55f1f834c2 improved meal plan editor shopping integration 2025-07-11 19:43:34 +02:00
vabene1111
f5f32912b1 added ability to add recipes to shopping list from shopping list view 2025-07-11 19:16:06 +02:00
vabene1111
5709435d43 removed JS_REVERSE_SCRIPT_PREFIX config variable 2025-07-11 19:04:37 +02:00
vabene1111
1c219dbc3b fixed error on account pages 2025-07-11 19:03:18 +02:00
vabene1111
1262982588 time picker out of labs 2025-07-11 18:46:54 +02:00
vabene1111
be8a340a0c fixed custom CSS problem after build 2025-07-11 18:44:40 +02:00
liam
fb1de15de6 cookbook app: only import the first valid image 2025-07-07 20:39:00 +00:00
liam
2180f11768 ignore venv in dockerfile to prevent my local venv overwriting the alpine one 2025-07-07 20:39:00 +00:00
caffeinated-tech
1083b7521e Merge branch 'TandoorRecipes:develop' into cookbookapp-images-import 2025-07-07 21:33:23 +01:00
vabene1111
0104b600cc Merge branch 'develop' into beta 2025-07-07 18:28:50 +02:00
vabene1111
70d40f9e70 fixed missing template 2025-07-07 17:56:54 +02:00
vabene1111
1094cf2d92 temporarily disabled service worker url 2025-07-07 17:39:18 +02:00
vabene1111
aaf6e0f197 Merge branch 'develop' of https://github.com/TandoorRecipes/recipes into develop 2025-07-06 18:26:50 +02:00
vabene1111
ec59cd6e4f playing around with a help dialog 2025-07-06 18:26:46 +02:00
caffeinated-tech
5a0a5b09a1 Merge branch 'TandoorRecipes:develop' into cookbookapp-images-import 2025-07-06 01:16:27 +01:00
liam
e698d14ec3 fixed documentation links 2025-07-06 00:10:53 +00:00
liam
0caf2fe77f added libxml workaround for building devcontainers 2025-07-06 00:03:19 +00:00
liam
c079f49d71 import all images from cookbookapp, ignoring branding images 2025-07-06 00:02:53 +00:00
Lucas Ortega
8490ac01cc Translated using Weblate (Portuguese (Brazil))
Currently translated at 78.4% (609 of 776 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/pt_BR/
2025-06-23 08:28:59 +00:00
Ángel
84477ef52a Translated using Weblate (Spanish)
Currently translated at 57.3% (280 of 488 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/es/
2025-06-23 08:28:59 +00:00
Matjaž T
b789573de3 Translated using Weblate (Slovenian)
Currently translated at 100.0% (776 of 776 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/sl/
2025-06-23 08:28:59 +00:00
Ángel
d5d8e7ce63 Translated using Weblate (Spanish)
Currently translated at 98.8% (767 of 776 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/es/
2025-06-23 08:28:58 +00:00
Vincenzo Reale
c7a49458b9 Translated using Weblate (Italian)
Currently translated at 100.0% (776 of 776 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/it/
2025-06-23 08:28:58 +00:00
vabene1111
7baad85112 Merge branch 'develop' into beta 2025-06-22 10:35:01 +02:00
vabene1111
4b0bfa9a85 Merge branch 'master' into beta 2025-06-22 10:29:43 +02:00
vabene1111
5e7c75ef68 Merge branch 'develop' into beta 2025-01-18 09:24:08 +01:00
vabene1111
954a35bea2 Merge branch 'develop' into beta 2025-01-01 08:17:32 +01:00
vabene1111
88347d44c8 Merge branch 'beta' of https://github.com/TandoorRecipes/recipes into beta 2024-11-23 21:56:13 +01:00
vabene1111
2c13e76fbb Merge branch 'develop' into beta 2024-03-05 08:54:58 +01:00
vabene1111
362f634828 Merge branch 'develop' into beta 2024-03-02 07:41:28 +01:00
vabene1111
2fb968cfd3 Merge branch 'develop' into beta 2024-03-01 07:42:28 +01:00
vabene1111
4d3dab6edd Merge branch 'develop' into beta 2024-02-28 17:21:22 +01:00
vabene1111
8f1b593ad1 Merge branch 'develop' into beta 2024-02-28 17:19:15 +01:00
vabene1111
1002f0d61f Merge branch 'develop' into beta 2024-02-28 17:12:35 +01:00
vabene1111
20cb218688 Merge branch 'develop' into beta 2024-02-26 16:29:16 +01:00
vabene1111
bba44b0c1e Merge branch 'develop' into beta 2024-02-20 07:54:28 +01:00
80 changed files with 4332 additions and 2837 deletions

View File

@@ -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

View File

@@ -29,3 +29,4 @@ vue/babel.config*
vue/package.json
vue/tsconfig.json
vue/src/utils/openapi
venv

View File

@@ -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"

View File

@@ -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):
"""

View File

@@ -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

View File

@@ -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()

View File

@@ -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/'):

View File

@@ -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

View File

@@ -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"

View File

@@ -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" %}">

View File

@@ -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 }}

View File

@@ -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 &quot;TypeError: type 'Factory' is not subscriptable&quot;">args = ()
kwargs = {'request': &lt;SubRequest 'space_1' for &lt;Function test_never_unit_automation[arg0]&gt;&gt;}
k = 'space_1__name'
@functools.wraps(fixture_function)
def wrapper(*args: P.args, **kwargs: P.kwargs) -&gt; T:
for k in set(kwargs.keys()) - function_args:
del kwargs[k]
&gt; 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 = &lt;SubRequest 'space_1' for &lt;Function test_never_unit_automation[arg0]&gt;&gt;
factory_name = 'space_factory'
def model_fixture(request: SubRequest, factory_name: str) -&gt; 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
&gt; 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 &quot;TypeError: type 'Factory' is not subscriptable&quot;">args = ()
kwargs = {'request': &lt;SubRequest 'space_1' for &lt;Function test_never_unit_automation[arg2]&gt;&gt;}
k = 'space_1__name'
@functools.wraps(fixture_function)
def wrapper(*args: P.args, **kwargs: P.kwargs) -&gt; T:
for k in set(kwargs.keys()) - function_args:
del kwargs[k]
&gt; 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 = &lt;SubRequest 'space_1' for &lt;Function test_never_unit_automation[arg2]&gt;&gt;
factory_name = 'space_factory'
def model_fixture(request: SubRequest, factory_name: str) -&gt; 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
&gt; 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 &quot;TypeError: type 'Factory' is not subscriptable&quot;">args = ()
kwargs = {'request': &lt;SubRequest 'space_1' for &lt;Function test_never_unit_automation[arg3]&gt;&gt;}
k = 'space_1__name'
@functools.wraps(fixture_function)
def wrapper(*args: P.args, **kwargs: P.kwargs) -&gt; T:
for k in set(kwargs.keys()) - function_args:
del kwargs[k]
&gt; 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 = &lt;SubRequest 'space_1' for &lt;Function test_never_unit_automation[arg3]&gt;&gt;
factory_name = 'space_factory'
def model_fixture(request: SubRequest, factory_name: str) -&gt; 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
&gt; 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 &quot;TypeError: type 'Factory' is not subscriptable&quot;">args = ()
kwargs = {'request': &lt;SubRequest 'space_1' for &lt;Function test_never_unit_automation[arg1]&gt;&gt;}
k = 'space_1__name'
@functools.wraps(fixture_function)
def wrapper(*args: P.args, **kwargs: P.kwargs) -&gt; T:
for k in set(kwargs.keys()) - function_args:
del kwargs[k]
&gt; 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 = &lt;SubRequest 'space_1' for &lt;Function test_never_unit_automation[arg1]&gt;&gt;
factory_name = 'space_factory'
def model_fixture(request: SubRequest, factory_name: str) -&gt; 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
&gt; 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

View File

@@ -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", [

View File

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

View File

@@ -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'] + '"'

View File

@@ -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,
})

View File

@@ -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.

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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",

View File

@@ -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>

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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
}

View File

@@ -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(() => {

View 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>

View File

@@ -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
})
/**

View File

@@ -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">

View File

@@ -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>

View File

@@ -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,

View File

@@ -29,10 +29,6 @@ const image = computed(() => {
}
})
watch(() => props.recipe, () => {
console.log('changed')
})
</script>
<style scoped>

View File

@@ -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) }})

View File

@@ -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 => {

View File

@@ -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

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
*/

View File

@@ -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>

View 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

View File

@@ -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>

View File

@@ -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"

View File

@@ -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()

View File

@@ -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>

View File

@@ -1,8 +0,0 @@
import {DateTime} from "luxon";
export default {
install: (app: any) => {
// inject a globally available luxon DateTime
app.config.globalProperties.$luxon = DateTime
}
}

View 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>

View File

@@ -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>

View 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)

View File

@@ -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')
}
}

View File

@@ -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)
}

View File

@@ -0,0 +1,7 @@
import {RouteRecordRaw} from "vue-router";
import {Model, registerModel} from "@/types/Models.ts";
export type TandoorPlugin = {
name: string,
routes: RouteRecordRaw[]
}

View File

@@ -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)"},

View File

@@ -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: {

View File

@@ -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"