Merge branch 'develop' into feature/importer_to_vue
# Conflicts: # vue/src/apps/RecipeEditView/RecipeEditView.vue # vue/src/utils/openapi/api.ts
This commit is contained in:
commit
2ddb0c719a
@ -86,8 +86,10 @@ GUNICORN_MEDIA=0
|
||||
# EMAIL_HOST_PASSWORD=
|
||||
# EMAIL_USE_TLS=0
|
||||
# EMAIL_USE_SSL=0
|
||||
# DEFAULT_FROM_EMAIL= # email sender address (default 'webmaster@localhost')
|
||||
# ACCOUNT_EMAIL_SUBJECT_PREFIX= # prefix used for account related emails (default "[Tandoor Recipes] ")
|
||||
# email sender address (default 'webmaster@localhost')
|
||||
# DEFAULT_FROM_EMAIL=
|
||||
# prefix used for account related emails (default "[Tandoor Recipes] ")
|
||||
# ACCOUNT_EMAIL_SUBJECT_PREFIX=
|
||||
|
||||
# allow authentication via reverse proxy (e.g. authelia), leave off if you dont know what you are doing
|
||||
# see docs for more information https://vabene1111.github.io/recipes/features/authentication/
|
||||
|
1
.github/workflows/docs.yml
vendored
1
.github/workflows/docs.yml
vendored
@ -3,7 +3,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
|
@ -1,7 +1,10 @@
|
||||
# Contributers
|
||||
|
||||
Many thanks to everyone who contributed to this project! If you add something or help out feel free to add yourself
|
||||
to this list.
|
||||
|
||||
## Code/Features
|
||||
|
||||
Please have a look at the [list of pull requests](https://github.com/vabene1111/recipes/pulls) for
|
||||
a complete list of contributions.
|
||||
Below are some of the larger contributions made yet.
|
||||
@ -20,46 +23,61 @@ Below are some of the larger contributions made yet.
|
||||
|
||||
## Translations
|
||||
|
||||
### Catalan
|
||||
### Catalan
|
||||
|
||||
[Rubenix](https://www.transifex.com/user/profile/rubenix/)
|
||||
|
||||
### Dutch
|
||||
[D0T1X](https://www.transifex.com/user/profile/D0T1X/)
|
||||
[ikbenfrank](https://www.transifex.com/user/profile/ikbenfrank/)
|
||||
[kampsj](https://www.transifex.com/user/profile/kampsj/)
|
||||
|
||||
[D0T1X](https://www.transifex.com/user/profile/D0T1X/)
|
||||
[ikbenfrank](https://www.transifex.com/user/profile/ikbenfrank/)
|
||||
[kampsj](https://www.transifex.com/user/profile/kampsj/)
|
||||
|
||||
### French
|
||||
[jt117](https://www.transifex.com/user/profile/jt117/)
|
||||
[nerdinator](https://www.transifex.com/user/profile/nerdinator/)
|
||||
[agaume](https://www.transifex.com/user/profile/agaume/)
|
||||
|
||||
[jt117](https://www.transifex.com/user/profile/jt117/)
|
||||
[nerdinator](https://www.transifex.com/user/profile/nerdinator/)
|
||||
[agaume](https://www.transifex.com/user/profile/agaume/)
|
||||
|
||||
### German
|
||||
|
||||
[eTaurus](https://www.transifex.com/user/profile/eTaurus/)
|
||||
[l0c4lh057](https://www.transifex.com/user/profile/l0c4lh057/)
|
||||
[hyperbit00]
|
||||
[hyperbit00](https://github.com/hyperbit00)
|
||||
|
||||
### Hungarian
|
||||
|
||||
[igazka](https://www.transifex.com/user/profile/igazka/)
|
||||
|
||||
### Italian
|
||||
[SK3LA](https://www.transifex.com/user/profile/SK3LA/)
|
||||
[auanasgheps](https://www.transifex.com/user/profile/auanasgheps/)
|
||||
|
||||
[SK3LA](https://www.transifex.com/user/profile/SK3LA/)
|
||||
[auanasgheps](https://www.transifex.com/user/profile/auanasgheps/)
|
||||
|
||||
### Latvian
|
||||
|
||||
[melkypie](https://github.com/melkypie)
|
||||
|
||||
### Portuguese
|
||||
|
||||
[hds](https://www.transifex.com/user/profile/hds/)
|
||||
[mlopezifu](https://www.transifex.com/user/profile/mlopezifu/)
|
||||
[stormsz](https://www.transifex.com/user/profile/stormsz/)
|
||||
[hds](https://www.transifex.com/user/profile/hds/)
|
||||
[mlopezifu](https://www.transifex.com/user/profile/mlopezifu/)
|
||||
[stormsz](https://www.transifex.com/user/profile/stormsz/)
|
||||
|
||||
### Russian
|
||||
|
||||
[amillerr](https://github.com/amillerr)
|
||||
|
||||
### Spanish
|
||||
|
||||
[albertocp](https://www.transifex.com/user/profile/albertocp/)
|
||||
[alfa5](https://www.transifex.com/user/profile/alfa5/)
|
||||
[mlopezifu](https://www.transifex.com/user/profile/mlopezifu/)
|
||||
[sergio.laya](https://www.transifex.com/user/profile/sergio.laya/)
|
||||
[albertocp](https://www.transifex.com/user/profile/albertocp/)
|
||||
[alfa5](https://www.transifex.com/user/profile/alfa5/)
|
||||
[mlopezifu](https://www.transifex.com/user/profile/mlopezifu/)
|
||||
[sergio.laya](https://www.transifex.com/user/profile/sergio.laya/)
|
||||
|
||||
### Swedish
|
||||
|
||||
[makanz](https://github.com/makanz)
|
||||
|
||||
### Turkish
|
||||
|
||||
|
@ -49,7 +49,7 @@ class UserPreferenceForm(forms.ModelForm):
|
||||
fields = (
|
||||
'default_unit', 'use_fractions', 'use_kj', 'theme', 'nav_color',
|
||||
'sticky_navbar', 'default_page', 'show_recent', 'search_style',
|
||||
'plan_share', 'ingredient_decimals', 'comments',
|
||||
'plan_share', 'ingredient_decimals', 'comments', 'left_handed',
|
||||
)
|
||||
|
||||
labels = {
|
||||
@ -65,7 +65,8 @@ class UserPreferenceForm(forms.ModelForm):
|
||||
'plan_share': _('Plan sharing'),
|
||||
'ingredient_decimals': _('Ingredient decimal places'),
|
||||
'shopping_auto_sync': _('Shopping list auto sync period'),
|
||||
'comments': _('Comments')
|
||||
'comments': _('Comments'),
|
||||
'left_handed': _('Left-handed mode')
|
||||
}
|
||||
|
||||
help_texts = {
|
||||
@ -89,6 +90,7 @@ class UserPreferenceForm(forms.ModelForm):
|
||||
'sticky_navbar': _('Makes the navbar stick to the top of the page.'), # noqa: E501
|
||||
'mealplan_autoadd_shopping': _('Automatically add meal plan ingredients to shopping list.'),
|
||||
'mealplan_autoexclude_onhand': _('Exclude ingredients that are on hand.'),
|
||||
'left_handed': _('Will optimize the UI for use with your left hand.')
|
||||
}
|
||||
|
||||
widgets = {
|
||||
|
@ -43,7 +43,8 @@ class NextcloudCookbook(Integration):
|
||||
if 'keywords' in recipe_json:
|
||||
try:
|
||||
for x in recipe_json['keywords'].split(','):
|
||||
recipe.keywords.add(Keyword.objects.get_or_create(space=self.request.space, name=x)[0])
|
||||
if x.strip() != '':
|
||||
recipe.keywords.add(Keyword.objects.get_or_create(space=self.request.space, name=x)[0])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
@ -77,14 +77,13 @@ class RecipeSage(Integration):
|
||||
}
|
||||
|
||||
for s in recipe.steps.all():
|
||||
if s.type != Step.TIME:
|
||||
data['recipeInstructions'].append({
|
||||
'@type': 'HowToStep',
|
||||
'text': s.instruction
|
||||
})
|
||||
data['recipeInstructions'].append({
|
||||
'@type': 'HowToStep',
|
||||
'text': s.instruction
|
||||
})
|
||||
|
||||
for i in s.ingredients.all():
|
||||
data['recipeIngredient'].append(f'{float(i.amount)} {i.unit} {i.food}')
|
||||
for i in s.ingredients.all():
|
||||
data['recipeIngredient'].append(f'{float(i.amount)} {i.unit} {i.food}')
|
||||
|
||||
return data
|
||||
|
||||
|
@ -12,33 +12,33 @@ class RezKonv(Integration):
|
||||
|
||||
ingredients = []
|
||||
directions = []
|
||||
for line in file.replace('\r', '').split('\n'):
|
||||
for line in file.replace('\r', '').replace('\n\n', '\n').split('\n'):
|
||||
if 'Titel:' in line:
|
||||
title = line.replace('Titel:', '').strip()
|
||||
if 'Kategorien:' in line:
|
||||
tags = line.replace('Kategorien:', '').strip()
|
||||
if ingredient_mode and ('quelle' in line.lower() or 'source' in line.lower()):
|
||||
if ingredient_mode and (
|
||||
'quelle' in line.lower() or 'source' in line.lower() or (line == '' and len(ingredients) > 0)):
|
||||
ingredient_mode = False
|
||||
direction_mode = True
|
||||
if ingredient_mode:
|
||||
if line != '' and '===' not in line and 'Zubereitung' not in line:
|
||||
ingredients.append(line.strip())
|
||||
if direction_mode:
|
||||
if line.strip() != '' and line.strip() != '=====':
|
||||
directions.append(line.strip())
|
||||
if 'Zutaten:' in line:
|
||||
if 'Zutaten:' in line or 'Ingredients' in line or 'Menge:' in line:
|
||||
ingredient_mode = True
|
||||
if 'Zubereitung:' in line:
|
||||
ingredient_mode = False
|
||||
direction_mode = True
|
||||
|
||||
recipe = Recipe.objects.create(name=title, created_by=self.request.user, internal=True, space=self.request.space)
|
||||
recipe = Recipe.objects.create(name=title, created_by=self.request.user, internal=True,
|
||||
space=self.request.space)
|
||||
|
||||
for k in tags.split(','):
|
||||
keyword, created = Keyword.objects.get_or_create(name=k.strip(), space=self.request.space)
|
||||
recipe.keywords.add(keyword)
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction='\n'.join(directions) + '\n\n', space=self.request.space,
|
||||
instruction=' \n'.join(directions) + '\n\n', space=self.request.space,
|
||||
)
|
||||
|
||||
ingredient_parser = IngredientParser(self.request, True)
|
||||
@ -60,7 +60,8 @@ class RezKonv(Integration):
|
||||
def split_recipe_file(self, file):
|
||||
recipe_list = []
|
||||
current_recipe = ''
|
||||
encoding_list = ['windows-1250', 'latin-1'] #TODO build algorithm to try trough encodings and fail if none work, use for all importers
|
||||
encoding_list = ['windows-1250',
|
||||
'latin-1'] # TODO build algorithm to try trough encodings and fail if none work, use for all importers
|
||||
encoding = 'windows-1250'
|
||||
for fl in file.readlines():
|
||||
try:
|
||||
|
@ -71,11 +71,10 @@ class Saffron(Integration):
|
||||
recipeInstructions = []
|
||||
recipeIngredient = []
|
||||
for s in recipe.steps.all():
|
||||
if s.type != Step.TIME:
|
||||
recipeInstructions.append(s.instruction)
|
||||
recipeInstructions.append(s.instruction)
|
||||
|
||||
for i in s.ingredients.all():
|
||||
recipeIngredient.append(f'{float(i.amount)} {i.unit} {i.food}')
|
||||
for i in s.ingredients.all():
|
||||
recipeIngredient.append(f'{float(i.amount)} {i.unit} {i.food}')
|
||||
|
||||
data += "Ingredients: \n"
|
||||
for ingredient in recipeIngredient:
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1469,10 +1469,10 @@ msgid ""
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Markdown ist eine Schreibweise mit der Text einfach formatiert "
|
||||
"werden kann. Diese Seite benutzt <a href=\"https\\://python-markdown.github."
|
||||
"werden kann. Diese Seite benutzt <a href=\"https://python-markdown.github."
|
||||
"io/\" target=\"_blank\">Python Markdown</a>, eine Bibliothek, die reinen "
|
||||
"Text in schönes HTML umwandelt. Die komplette Dokumentation befindet sich <a "
|
||||
"href=\"https\\://daringfireball.net/projects/markdown/syntax\" target="
|
||||
"href=\"https://daringfireball.net/projects/markdown/syntax\" target="
|
||||
"\"_blank\">hier</a>. Die wichtigsten Formatierungszeichen befinden sich hier "
|
||||
"auf dieser Seite.\n"
|
||||
" "
|
||||
|
@ -13,10 +13,10 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-02-11 08:52+0100\n"
|
||||
"PO-Revision-Date: 2022-02-09 01:31+0000\n"
|
||||
"PO-Revision-Date: 2022-04-05 10:31+0000\n"
|
||||
"Last-Translator: Jesse <jesse.kamps@pm.me>\n"
|
||||
"Language-Team: Dutch <http://translate.tandoor.dev/projects/tandoor/recipes-"
|
||||
"backend/nl/>\n"
|
||||
"Language-Team: Dutch <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/nl/>\n"
|
||||
"Language: nl\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@ -291,10 +291,6 @@ msgstr ""
|
||||
"(lage waarden betekenen bijvoorbeeld dat meer typefouten genegeerd worden)."
|
||||
|
||||
#: .\cookbook\forms.py:445
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Select type method of search. Click <a href=\"/docs/search/\">here</a> "
|
||||
#| "for full desciption of choices."
|
||||
msgid ""
|
||||
"Select type method of search. Click <a href=\"/docs/search/\">here</a> for "
|
||||
"full description of choices."
|
||||
@ -368,8 +364,6 @@ msgid "Partial Match"
|
||||
msgstr "Gedeeltelijke overeenkomst"
|
||||
|
||||
#: .\cookbook\forms.py:464
|
||||
#, fuzzy
|
||||
#| msgid "Starts Wtih"
|
||||
msgid "Starts With"
|
||||
msgstr "Begint met"
|
||||
|
||||
@ -529,10 +523,8 @@ msgid "One of queryset or hash_key must be provided"
|
||||
msgstr "Er moet een queryset of hash_key opgegeven worden"
|
||||
|
||||
#: .\cookbook\helper\shopping_helper.py:148
|
||||
#, fuzzy
|
||||
#| msgid "You must supply a created_by"
|
||||
msgid "You must supply a servings size"
|
||||
msgstr "Je moet een created_by aanleveren"
|
||||
msgstr "Je moet een portiegrootte aanleveren"
|
||||
|
||||
#: .\cookbook\helper\template_helper.py:61
|
||||
#: .\cookbook\helper\template_helper.py:63
|
||||
@ -604,11 +596,9 @@ msgid "Rebuilds full text search index on Recipe"
|
||||
msgstr "Herbouwt de volledige tekst zoekindex van Recept"
|
||||
|
||||
#: .\cookbook\management\commands\rebuildindex.py:18
|
||||
#, fuzzy
|
||||
#| msgid "Only Postgress databases use full text search, no index to rebuild"
|
||||
msgid "Only Postgresql databases use full text search, no index to rebuild"
|
||||
msgstr ""
|
||||
"Alleen Postgress databases gebruiken volledige tekst zoekmethoden, geen "
|
||||
"Alleen Postgresql databases gebruiken volledige tekst zoekmethoden, geen "
|
||||
"index aanwezig om te herbouwen"
|
||||
|
||||
#: .\cookbook\management\commands\rebuildindex.py:29
|
||||
@ -732,18 +722,22 @@ msgstr ""
|
||||
msgid ""
|
||||
"Providing a list_recipe ID and servings of 0 will delete that shopping list."
|
||||
msgstr ""
|
||||
"Als je een list_recipe ID en portiegrootte van 0 opgeeft wordt dat "
|
||||
"boodschappenlijstje verwijderd."
|
||||
|
||||
#: .\cookbook\serializer.py:988
|
||||
msgid "Amount of food to add to the shopping list"
|
||||
msgstr ""
|
||||
msgstr "Hoeveelheid eten om aan het boodschappenlijstje toe te voegen"
|
||||
|
||||
#: .\cookbook\serializer.py:989
|
||||
msgid "ID of unit to use for the shopping list"
|
||||
msgstr ""
|
||||
msgstr "ID of eenheid om te gebruik voor het boodschappenlijstje"
|
||||
|
||||
#: .\cookbook\serializer.py:990
|
||||
msgid "When set to true will delete all food from active shopping lists."
|
||||
msgstr ""
|
||||
"Wanneer ingesteld op waar, wordt al het voedsel van actieve "
|
||||
"boodschappenlijstjes verwijderd."
|
||||
|
||||
#: .\cookbook\tables.py:35 .\cookbook\templates\generic\edit_template.html:6
|
||||
#: .\cookbook\templates\generic\edit_template.html:14
|
||||
@ -1480,8 +1474,6 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:57
|
||||
#: .\cookbook\templates\markdown_info.html:73
|
||||
#, fuzzy
|
||||
#| msgid "or by leaving a blank line inbetween."
|
||||
msgid "or by leaving a blank line in between."
|
||||
msgstr "of door een witregel te gebruiken."
|
||||
|
||||
@ -1505,10 +1497,6 @@ msgid "Lists"
|
||||
msgstr "Lijsten"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:85
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Lists can ordered or unorderd. It is <b>important to leave a blank line "
|
||||
#| "before the list!</b>"
|
||||
msgid ""
|
||||
"Lists can ordered or unordered. It is <b>important to leave a blank line "
|
||||
"before the list!</b>"
|
||||
@ -1806,15 +1794,6 @@ msgstr ""
|
||||
" "
|
||||
|
||||
#: .\cookbook\templates\search_info.html:29
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| " \n"
|
||||
#| " Simple searches ignore punctuation and common words such as "
|
||||
#| "'the', 'a', 'and'. And will treat seperate words as required.\n"
|
||||
#| " Searching for 'apple or flour' will return any recipe that "
|
||||
#| "includes both 'apple' and 'flour' anywhere in the fields that have been "
|
||||
#| "selected for a full text search.\n"
|
||||
#| " "
|
||||
msgid ""
|
||||
" \n"
|
||||
" Simple searches ignore punctuation and common words such as "
|
||||
@ -1851,23 +1830,6 @@ msgstr ""
|
||||
" "
|
||||
|
||||
#: .\cookbook\templates\search_info.html:39
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| " \n"
|
||||
#| " Web searches simulate functionality found on many web search "
|
||||
#| "sites supporting special syntax.\n"
|
||||
#| " Placing quotes around several words will convert those words "
|
||||
#| "into a phrase.\n"
|
||||
#| " 'or' is recongized as searching for the word (or phrase) "
|
||||
#| "immediately before 'or' OR the word (or phrase) directly after.\n"
|
||||
#| " '-' is recognized as searching for recipes that do not "
|
||||
#| "include the word (or phrase) that comes immediately after. \n"
|
||||
#| " For example searching for 'apple pie' or cherry -butter will "
|
||||
#| "return any recipe that includes the phrase 'apple pie' or the word "
|
||||
#| "'cherry' \n"
|
||||
#| " in any field included in the full text search but exclude any "
|
||||
#| "recipe that has the word 'butter' in any field included.\n"
|
||||
#| " "
|
||||
msgid ""
|
||||
" \n"
|
||||
" Web searches simulate functionality found on many web search "
|
||||
@ -1913,19 +1875,6 @@ msgstr ""
|
||||
" "
|
||||
|
||||
#: .\cookbook\templates\search_info.html:59
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| " \n"
|
||||
#| " Another approach to searching that also requires Postgresql "
|
||||
#| "is fuzzy search or trigram similarity. A trigram is a group of three "
|
||||
#| "consecutive characters.\n"
|
||||
#| " For example searching for 'apple' will create x trigrams "
|
||||
#| "'app', 'ppl', 'ple' and will create a score of how closely words match "
|
||||
#| "the generated trigrams.\n"
|
||||
#| " One benefit of searching trigams is that a search for "
|
||||
#| "'sandwich' will find mispelled words such as 'sandwhich' that would be "
|
||||
#| "missed by other methods.\n"
|
||||
#| " "
|
||||
msgid ""
|
||||
" \n"
|
||||
" Another approach to searching that also requires Postgresql is "
|
||||
@ -2061,10 +2010,8 @@ msgid "Search-Settings"
|
||||
msgstr "Zoek instellingen"
|
||||
|
||||
#: .\cookbook\templates\settings.html:56
|
||||
#, fuzzy
|
||||
#| msgid "Search-Settings"
|
||||
msgid "Shopping-Settings"
|
||||
msgstr "Zoek instellingen"
|
||||
msgstr "Boodschappen instellingen"
|
||||
|
||||
#: .\cookbook\templates\settings.html:65
|
||||
msgid "Name Settings"
|
||||
@ -2180,10 +2127,8 @@ msgid "Perfect for large Databases"
|
||||
msgstr "Perfect voor grote databases"
|
||||
|
||||
#: .\cookbook\templates\settings.html:207
|
||||
#, fuzzy
|
||||
#| msgid "Shopping List"
|
||||
msgid "Shopping Settings"
|
||||
msgstr "Boodschappenlijst"
|
||||
msgstr "Boodschappen instellingen"
|
||||
|
||||
#: .\cookbook\templates\setup.html:6 .\cookbook\templates\system.html:5
|
||||
msgid "Cookbook Setup"
|
||||
@ -2263,8 +2208,6 @@ msgid "Finished"
|
||||
msgstr "Afgerond"
|
||||
|
||||
#: .\cookbook\templates\shopping_list.html:267
|
||||
#, fuzzy
|
||||
#| msgid "You are offline, shopping list might not syncronize."
|
||||
msgid "You are offline, shopping list might not synchronize."
|
||||
msgstr "Je bent offline, de boodschappenlijst synchroniseert mogelijk niet."
|
||||
|
||||
@ -2804,93 +2747,113 @@ msgstr "{child.name} is succesvol verplaatst naar {parent.name}"
|
||||
#: .\cookbook\views\api.py:474
|
||||
#, python-brace-format
|
||||
msgid "{obj.name} was removed from the shopping list."
|
||||
msgstr ""
|
||||
msgstr "{obj.name} is verwijderd van het boodschappenlijstje."
|
||||
|
||||
#: .\cookbook\views\api.py:479 .\cookbook\views\api.py:729
|
||||
#: .\cookbook\views\api.py:742
|
||||
#, python-brace-format
|
||||
msgid "{obj.name} was added to the shopping list."
|
||||
msgstr ""
|
||||
msgstr "{obj.name} is toegevoegd aan het boodschappenlijstje."
|
||||
|
||||
#: .\cookbook\views\api.py:591
|
||||
msgid "ID of recipe a step is part of. For multiple repeat parameter."
|
||||
msgstr ""
|
||||
"ID van het recept waar de stap onderdeel van is. Herhaal parameter voor "
|
||||
"meerdere."
|
||||
|
||||
#: .\cookbook\views\api.py:592
|
||||
msgid "Query string matched (fuzzy) against object name."
|
||||
msgstr ""
|
||||
msgstr "Zoekterm komt overeen (fuzzy) met object naam."
|
||||
|
||||
#: .\cookbook\views\api.py:635
|
||||
msgid ""
|
||||
"Query string matched (fuzzy) against recipe name. In the future also "
|
||||
"fulltext search."
|
||||
msgstr ""
|
||||
"Zoekterm komt overeen (fuzzy) met recept naam. In de toekomst wordt zoeken "
|
||||
"op volledige tekst ondersteund."
|
||||
|
||||
#: .\cookbook\views\api.py:636
|
||||
msgid "ID of keyword a recipe should have. For multiple repeat parameter."
|
||||
msgstr ""
|
||||
"ID van etiket dat een recept moet hebben. Herhaal parameter voor meerdere."
|
||||
|
||||
#: .\cookbook\views\api.py:637
|
||||
msgid "ID of food a recipe should have. For multiple repeat parameter."
|
||||
msgstr ""
|
||||
"ID van ingrediënt dat een recept moet hebben. Herhaal parameter voor "
|
||||
"meerdere."
|
||||
|
||||
#: .\cookbook\views\api.py:638
|
||||
msgid "ID of unit a recipe should have."
|
||||
msgstr ""
|
||||
msgstr "ID van eenheid dat een recept moet hebben."
|
||||
|
||||
#: .\cookbook\views\api.py:639
|
||||
msgid "Rating a recipe should have. [0 - 5]"
|
||||
msgstr ""
|
||||
msgstr "Waardering die een recept moet hebben. [0 - 5]"
|
||||
|
||||
#: .\cookbook\views\api.py:640
|
||||
msgid "ID of book a recipe should be in. For multiple repeat parameter."
|
||||
msgstr ""
|
||||
"ID van boek dat een recept moet hebben. Herhaal parameter voor meerdere."
|
||||
|
||||
#: .\cookbook\views\api.py:641
|
||||
msgid ""
|
||||
"If recipe should have all (AND=false) or any (OR=<b>true</b>) of the "
|
||||
"provided keywords."
|
||||
msgstr ""
|
||||
"Als een recept alle moet hebben (AND=onwaar) of sommige (OR=<b>waar</b>) van "
|
||||
"de gegeven zoektermen."
|
||||
|
||||
#: .\cookbook\views\api.py:642
|
||||
msgid ""
|
||||
"If recipe should have all (AND=false) or any (OR=<b>true</b>) of the "
|
||||
"provided foods."
|
||||
msgstr ""
|
||||
"Als een recept alle moet hebben (AND=onwaar) of sommige (OR=<b>waar</b>) van "
|
||||
"de gegeven ingrediënten."
|
||||
|
||||
#: .\cookbook\views\api.py:643
|
||||
msgid ""
|
||||
"If recipe should be in all (AND=false) or any (OR=<b>true</b>) of the "
|
||||
"provided books."
|
||||
msgstr ""
|
||||
"Als een recept alle moet hebben (AND=onwaar) of sommige (OR=<b>waar</b>) van "
|
||||
"de gegeven boeken."
|
||||
|
||||
#: .\cookbook\views\api.py:644
|
||||
msgid "If only internal recipes should be returned. [true/<b>false</b>]"
|
||||
msgstr ""
|
||||
"Wanneer alleen interne recepten gevonden moeten worden. [waar/<b>onwaar</b>]"
|
||||
|
||||
#: .\cookbook\views\api.py:645
|
||||
msgid "Returns the results in randomized order. [true/<b>false</b>]"
|
||||
msgstr ""
|
||||
msgstr "Geeft de resultaten in willekeurige volgorde weer. [waar/<b>onwaar</b>]"
|
||||
|
||||
#: .\cookbook\views\api.py:646
|
||||
msgid "Returns new results first in search results. [true/<b>false</b>]"
|
||||
msgstr ""
|
||||
msgstr "Geeft nieuwe resultaten eerst weer. [waar/<b>onwaar</b>]"
|
||||
|
||||
#: .\cookbook\views\api.py:784
|
||||
msgid ""
|
||||
"Returns the shopping list entry with a primary key of id. Multiple values "
|
||||
"allowed."
|
||||
msgstr ""
|
||||
"Geeft het boodschappenlijstje item met een primaire sleutel van id. "
|
||||
"Meerdere waarden toegestaan."
|
||||
|
||||
#: .\cookbook\views\api.py:787
|
||||
msgid ""
|
||||
"Filter shopping list entries on checked. [true, false, both, <b>recent</"
|
||||
"b>]<br> - recent includes unchecked items and recently completed items."
|
||||
msgstr ""
|
||||
"Filter boodschappenlijstjes op aangevinkt. [waar,onwaar,beide,<b>recent</b>]"
|
||||
"<br> - recent bevat niet aangevinkte en recent voltooide items."
|
||||
|
||||
#: .\cookbook\views\api.py:789
|
||||
msgid "Returns the shopping list entries sorted by supermarket category order."
|
||||
msgstr ""
|
||||
"Geeft items op boodschappenlijstjes gesorteerd per supermarktcategorie weer."
|
||||
|
||||
#: .\cookbook\views\api.py:949 .\cookbook\views\data.py:42
|
||||
#: .\cookbook\views\edit.py:129 .\cookbook\views\new.py:95
|
||||
@ -2929,11 +2892,9 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\views\api.py:1109
|
||||
msgid "Connection Refused."
|
||||
msgstr ""
|
||||
msgstr "Verbinding geweigerd."
|
||||
|
||||
#: .\cookbook\views\api.py:1118
|
||||
#, fuzzy
|
||||
#| msgid "No useable data could be found."
|
||||
msgid "No usable data could be found."
|
||||
msgstr "Er is geen bruikbare data gevonden."
|
||||
|
||||
@ -3017,6 +2978,8 @@ msgid ""
|
||||
"The PDF Exporter is not enabled on this instance as it is still in an "
|
||||
"experimental state."
|
||||
msgstr ""
|
||||
"De PDF exporter is niet ingeschakeld op deze instantie gezien het in een "
|
||||
"experimentele staat is."
|
||||
|
||||
#: .\cookbook\views\lists.py:25
|
||||
msgid "Import Log"
|
||||
|
@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-09-13 22:40+0200\n"
|
||||
"PO-Revision-Date: 2021-10-23 09:06+0000\n"
|
||||
"Last-Translator: rustam <uzbekr@gmail.com>\n"
|
||||
"PO-Revision-Date: 2022-04-07 19:32+0000\n"
|
||||
"Last-Translator: Artem Aksenov <artemmillerr@gmail.com>\n"
|
||||
"Language-Team: Russian <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/ru/>\n"
|
||||
"Language: ru\n"
|
||||
@ -18,14 +18,14 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
|
||||
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
|
||||
"X-Generator: Weblate 4.8\n"
|
||||
"X-Generator: Weblate 4.10.1\n"
|
||||
|
||||
#: .\cookbook\filters.py:23 .\cookbook\templates\base.html:125
|
||||
#: .\cookbook\templates\forms\ingredients.html:34
|
||||
#: .\cookbook\templates\space.html:43 .\cookbook\templates\stats.html:28
|
||||
#: .\cookbook\templates\url_import.html:270
|
||||
msgid "Ingredients"
|
||||
msgstr "ингредиенты"
|
||||
msgstr "Ингредиенты"
|
||||
|
||||
#: .\cookbook\forms.py:50
|
||||
msgid ""
|
||||
@ -95,14 +95,14 @@ msgstr ""
|
||||
#: .\cookbook\forms.py:103 .\cookbook\forms.py:334
|
||||
#: .\cookbook\templates\url_import.html:154
|
||||
msgid "Name"
|
||||
msgstr "Имя"
|
||||
msgstr "Название"
|
||||
|
||||
#: .\cookbook\forms.py:104 .\cookbook\forms.py:335
|
||||
#: .\cookbook\templates\space.html:39 .\cookbook\templates\stats.html:24
|
||||
#: .\cookbook\templates\url_import.html:188
|
||||
#: .\cookbook\templates\url_import.html:573 .\cookbook\views\lists.py:112
|
||||
msgid "Keywords"
|
||||
msgstr "Ключевые поля"
|
||||
msgstr "Ключевые слова"
|
||||
|
||||
#: .\cookbook\forms.py:105
|
||||
msgid "Preparation time in minutes"
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -16,6 +16,7 @@ DICTIONARY = {
|
||||
'it': 'italian',
|
||||
# 'lv': 'Latvian',
|
||||
'es': 'spanish',
|
||||
'sv': 'swedish',
|
||||
}
|
||||
|
||||
|
||||
|
@ -42,7 +42,8 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
|
||||
api_serializer = None
|
||||
# extended values are computationally expensive and not needed in normal circumstances
|
||||
try:
|
||||
if str2bool(self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer:
|
||||
if str2bool(
|
||||
self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer:
|
||||
return fields
|
||||
except (AttributeError, KeyError) as e:
|
||||
pass
|
||||
@ -96,7 +97,8 @@ class CustomOnHandField(serializers.Field):
|
||||
shared_users = getattr(request, '_shared_users', None)
|
||||
if shared_users is None:
|
||||
try:
|
||||
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [self.context['request'].user.id]
|
||||
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [
|
||||
self.context['request'].user.id]
|
||||
except AttributeError: # Anonymous users (using share links) don't have shared users
|
||||
shared_users = []
|
||||
return obj.onhand_users.filter(id__in=shared_users).exists()
|
||||
@ -170,7 +172,8 @@ class FoodInheritFieldSerializer(WritableNestedModelSerializer):
|
||||
|
||||
|
||||
class UserPreferenceSerializer(WritableNestedModelSerializer):
|
||||
food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', many=True, allow_null=True, required=False, read_only=True)
|
||||
food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', many=True, allow_null=True,
|
||||
required=False, read_only=True)
|
||||
plan_share = UserNameSerializer(many=True, allow_null=True, required=False, read_only=True)
|
||||
shopping_share = UserNameSerializer(many=True, allow_null=True, required=False)
|
||||
food_children_exist = serializers.SerializerMethodField('get_food_children_exist')
|
||||
@ -189,9 +192,12 @@ class UserPreferenceSerializer(WritableNestedModelSerializer):
|
||||
class Meta:
|
||||
model = UserPreference
|
||||
fields = (
|
||||
'user', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_fractions', 'use_kj', 'search_style', 'show_recent', 'plan_share',
|
||||
'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'food_inherit_default', 'default_delay',
|
||||
'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days', 'csv_delim', 'csv_prefix',
|
||||
'user', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_fractions', 'use_kj', 'search_style',
|
||||
'show_recent', 'plan_share',
|
||||
'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping',
|
||||
'food_inherit_default', 'default_delay',
|
||||
'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days',
|
||||
'csv_delim', 'csv_prefix',
|
||||
'filter_to_supermarket', 'shopping_add_onhand', 'left_handed', 'food_children_exist'
|
||||
)
|
||||
|
||||
@ -393,7 +399,6 @@ class FoodSimpleSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Food
|
||||
fields = ('id', 'name')
|
||||
read_only_fields = ['id', 'name']
|
||||
|
||||
|
||||
class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedRecipeMixin):
|
||||
@ -416,7 +421,8 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
||||
shared_users = getattr(request, '_shared_users', None)
|
||||
if shared_users is None:
|
||||
try:
|
||||
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [self.context['request'].user.id]
|
||||
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [
|
||||
self.context['request'].user.id]
|
||||
except AttributeError:
|
||||
shared_users = []
|
||||
filter = Q(id__in=obj.substitute.all())
|
||||
@ -487,11 +493,15 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
||||
read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe')
|
||||
|
||||
|
||||
class IngredientSerializer(WritableNestedModelSerializer):
|
||||
food = FoodSerializer(allow_null=True)
|
||||
class IngredientSimpleSerializer(WritableNestedModelSerializer):
|
||||
food = FoodSimpleSerializer(allow_null=True)
|
||||
unit = UnitSerializer(allow_null=True)
|
||||
used_in_recipes = serializers.SerializerMethodField('get_used_in_recipes')
|
||||
amount = CustomDecimalField()
|
||||
|
||||
def get_used_in_recipes(self, obj):
|
||||
return list(Recipe.objects.filter(steps__ingredients=obj.id).values('id', 'name'))
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['space'] = self.context['request'].space
|
||||
return super().create(validated_data)
|
||||
@ -504,10 +514,14 @@ class IngredientSerializer(WritableNestedModelSerializer):
|
||||
model = Ingredient
|
||||
fields = (
|
||||
'id', 'food', 'unit', 'amount', 'note', 'order',
|
||||
'is_header', 'no_amount', 'original_text'
|
||||
'is_header', 'no_amount', 'original_text', 'used_in_recipes',
|
||||
)
|
||||
|
||||
|
||||
class IngredientSerializer(IngredientSimpleSerializer):
|
||||
food = FoodSerializer(allow_null=True)
|
||||
|
||||
|
||||
class StepSerializer(WritableNestedModelSerializer, ExtendedRecipeMixin):
|
||||
ingredients = IngredientSerializer(many=True)
|
||||
ingredients_markdown = serializers.SerializerMethodField('get_ingredients_markdown')
|
||||
@ -702,7 +716,8 @@ class RecipeBookEntrySerializer(serializers.ModelSerializer):
|
||||
def create(self, validated_data):
|
||||
book = validated_data['book']
|
||||
recipe = validated_data['recipe']
|
||||
if not book.get_owner() == self.context['request'].user and not self.context['request'].user in book.get_shared():
|
||||
if not book.get_owner() == self.context['request'].user and not self.context[
|
||||
'request'].user in book.get_shared():
|
||||
raise NotFound(detail=None, code=None)
|
||||
obj, created = RecipeBookEntry.objects.get_or_create(book=book, recipe=recipe)
|
||||
return obj
|
||||
@ -731,7 +746,7 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
|
||||
def create(self, validated_data):
|
||||
validated_data['created_by'] = self.context['request'].user
|
||||
mealplan = super().create(validated_data)
|
||||
if self.context['request'].data.get('addshopping', False):
|
||||
if self.context['request'].data.get('addshopping', False) and self.context['request'].data.get('recipe', None):
|
||||
SLR = RecipeShoppingEditor(user=validated_data['created_by'], space=validated_data['space'])
|
||||
SLR.create(mealplan=mealplan, servings=validated_data['servings'])
|
||||
return mealplan
|
||||
@ -755,13 +770,14 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
|
||||
def get_name(self, obj):
|
||||
if not isinstance(value := obj.servings, Decimal):
|
||||
value = Decimal(value)
|
||||
value = value.quantize(Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
|
||||
value = value.quantize(
|
||||
Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
|
||||
return (
|
||||
obj.name
|
||||
or getattr(obj.mealplan, 'title', None)
|
||||
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
|
||||
or obj.recipe.name
|
||||
) + f' ({value:.2g})'
|
||||
obj.name
|
||||
or getattr(obj.mealplan, 'title', None)
|
||||
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
|
||||
or obj.recipe.name
|
||||
) + f' ({value:.2g})'
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
# TODO remove once old shopping list
|
||||
@ -832,7 +848,8 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
|
||||
class Meta:
|
||||
model = ShoppingListEntry
|
||||
fields = (
|
||||
'id', 'list_recipe', 'food', 'unit', 'ingredient', 'ingredient_note', 'amount', 'order', 'checked', 'recipe_mealplan',
|
||||
'id', 'list_recipe', 'food', 'unit', 'ingredient', 'ingredient_note', 'amount', 'order', 'checked',
|
||||
'recipe_mealplan',
|
||||
'created_by', 'created_at', 'completed_at', 'delay_until'
|
||||
)
|
||||
read_only_fields = ('id', 'created_by', 'created_at',)
|
||||
@ -930,7 +947,10 @@ class ExportLogSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ExportLog
|
||||
fields = ('id', 'type', 'msg', 'running', 'total_recipes', 'exported_recipes', 'cache_duration', 'possibly_not_expired', 'created_by', 'created_at')
|
||||
fields = (
|
||||
'id', 'type', 'msg', 'running', 'total_recipes', 'exported_recipes', 'cache_duration',
|
||||
'possibly_not_expired',
|
||||
'created_by', 'created_at')
|
||||
read_only_fields = ('created_by',)
|
||||
|
||||
|
||||
@ -1042,10 +1062,12 @@ class RecipeExportSerializer(WritableNestedModelSerializer):
|
||||
|
||||
|
||||
class RecipeShoppingUpdateSerializer(serializers.ModelSerializer):
|
||||
list_recipe = serializers.IntegerField(write_only=True, allow_null=True, required=False, help_text=_("Existing shopping list to update"))
|
||||
list_recipe = serializers.IntegerField(write_only=True, allow_null=True, required=False,
|
||||
help_text=_("Existing shopping list to update"))
|
||||
ingredients = serializers.IntegerField(write_only=True, allow_null=True, required=False, help_text=_(
|
||||
"List of ingredient IDs from the recipe to add, if not provided all ingredients will be added."))
|
||||
servings = serializers.IntegerField(default=1, write_only=True, allow_null=True, required=False, help_text=_("Providing a list_recipe ID and servings of 0 will delete that shopping list."))
|
||||
servings = serializers.IntegerField(default=1, write_only=True, allow_null=True, required=False, help_text=_(
|
||||
"Providing a list_recipe ID and servings of 0 will delete that shopping list."))
|
||||
|
||||
class Meta:
|
||||
model = Recipe
|
||||
@ -1053,9 +1075,12 @@ class RecipeShoppingUpdateSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class FoodShoppingUpdateSerializer(serializers.ModelSerializer):
|
||||
amount = serializers.IntegerField(write_only=True, allow_null=True, required=False, help_text=_("Amount of food to add to the shopping list"))
|
||||
unit = serializers.IntegerField(write_only=True, allow_null=True, required=False, help_text=_("ID of unit to use for the shopping list"))
|
||||
delete = serializers.ChoiceField(choices=['true'], write_only=True, allow_null=True, allow_blank=True, help_text=_("When set to true will delete all food from active shopping lists."))
|
||||
amount = serializers.IntegerField(write_only=True, allow_null=True, required=False,
|
||||
help_text=_("Amount of food to add to the shopping list"))
|
||||
unit = serializers.IntegerField(write_only=True, allow_null=True, required=False,
|
||||
help_text=_("ID of unit to use for the shopping list"))
|
||||
delete = serializers.ChoiceField(choices=['true'], write_only=True, allow_null=True, allow_blank=True,
|
||||
help_text=_("When set to true will delete all food from active shopping lists."))
|
||||
|
||||
class Meta:
|
||||
model = Recipe
|
||||
|
12
cookbook/static/css/app.min.css
vendored
12
cookbook/static/css/app.min.css
vendored
@ -1,3 +1,15 @@
|
||||
.brand-icon {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.menu-dropdown-text {
|
||||
font-size: 14px;
|
||||
font-weight: 200;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.spinner-tandoor {
|
||||
animation: rotation 3s infinite linear;
|
||||
content: url("../assets/spinner.svg");
|
||||
|
@ -56,21 +56,46 @@
|
||||
{% block extra_head %} <!-- block for templates to put stuff into header -->
|
||||
{% endblock %}
|
||||
|
||||
<style>
|
||||
{% if request.user.userpreference.left_handed %}
|
||||
@media screen and (max-width: 600px) {
|
||||
#switcher .btn-circle {
|
||||
left: 80px !important;
|
||||
}
|
||||
}
|
||||
{% endif %}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-{% nav_color request %} bg-header" id="id_main_nav"
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-{% nav_color request %} bg-header"
|
||||
id="id_main_nav"
|
||||
style="{% sticky_nav request %}">
|
||||
|
||||
{% if not request.user.userpreference.left_handed %}
|
||||
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
|
||||
<a class="navbar-brand p-0 me-2 justify-content-center" href="{% base_path request 'base' %}"
|
||||
aria-label="Tandoor">
|
||||
<img class="brand-icon" src="{% static 'assets/brand_logo.svg' %}" alt="Logo">
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText"
|
||||
aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
|
||||
<a class="navbar-brand p-0 me-2 justify-content-center" href="{% base_path request 'base' %}" aria-label="Tandoor">
|
||||
<img class="brand-icon" src="{% static 'assets/brand_logo.png' %}" alt="" style="height: 5vh;">
|
||||
</a>
|
||||
{% if request.user.userpreference.left_handed %}
|
||||
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
|
||||
<a class="navbar-brand p-0 me-2 justify-content-center" href="{% base_path request 'base' %}"
|
||||
aria-label="Tandoor">
|
||||
<img class="brand-icon" src="{% static 'assets/brand_logo.svg' %}" alt="Logo">
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarText">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item {% if request.resolver_match.url_name in 'view_search' %}active{% endif %}">
|
||||
@ -102,129 +127,140 @@
|
||||
<i class="fas fa-toolbox fa-lg"></i>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-center dropdown-menu-center-large">
|
||||
<div class="row m-0">
|
||||
<div class="row m-0 mt-2 mt-md-0">
|
||||
<div class="col-4">
|
||||
<a href="{% url 'list_keyword' %}" class="p-1">
|
||||
<a href="{% url 'list_keyword' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-tags fa-2x"></i>
|
||||
</div>
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted">
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
|
||||
{% trans 'Keyword' %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a href="{% url 'list_food' %}" class="p-1">
|
||||
<a href="{% url 'list_food' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-leaf fa-2x"></i>
|
||||
</div>
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted">
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
|
||||
{% trans 'Foods' %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a href="{% url 'list_unit' %}" class="p-1">
|
||||
<a href="{% url 'list_unit' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-balance-scale fa-2x"></i>
|
||||
</div>
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted">
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
|
||||
{% trans 'Units' %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-0">
|
||||
<div class="row m-0 mt-2 mt-md-0">
|
||||
<div class="col-4">
|
||||
<a href="{% url 'list_supermarket' %}" class="p-1">
|
||||
<a href="{% url 'list_supermarket' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-store-alt fa-2x"></i>
|
||||
</div>
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted">
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
|
||||
{% trans 'Supermarket' %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a href="{% url 'list_supermarket_category' %}" class="p-1">
|
||||
<a href="{% url 'list_supermarket_category' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-cubes fa-2x"></i>
|
||||
</div>
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted">
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
|
||||
{% trans 'Supermarket Category' %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a href="{% url 'list_automation' %}" class="p-1">
|
||||
<a href="{% url 'list_automation' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-robot fa-2x"></i>
|
||||
</div>
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted">
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
|
||||
{% trans 'Automations' %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-0">
|
||||
<div class="row m-0 mt-2 mt-md-0">
|
||||
<div class="col-4">
|
||||
<a href="{% url 'list_user_file' %}" class="p-1">
|
||||
<a href="{% url 'list_user_file' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-file fa-2x"></i>
|
||||
</div>
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted">
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
|
||||
{% trans 'Files' %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a href="{% url 'data_batch_edit' %}" class="p-1">
|
||||
<a href="{% url 'data_batch_edit' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-edit fa-2x"></i>
|
||||
</div>
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted">
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
|
||||
{% trans 'Batch Edit' %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a href="{% url 'view_history' %}" class="p-1">
|
||||
<a href="{% url 'view_history' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-history fa-2x"></i>
|
||||
</div>
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted">
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
|
||||
{% trans 'History' %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="row m-0">
|
||||
<div class="row m-0 mt-2 mt-md-0">
|
||||
<div class="col-4">
|
||||
<a href="{% url 'view_export' %}" class="p-1">
|
||||
<a href="{% url 'view_ingredient_editor' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-th-list fa-2x"></i>
|
||||
</div>
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
|
||||
{% trans 'Ingredient Editor' %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a href="{% url 'view_export' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-file-export fa-2x"></i>
|
||||
</div>
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted">
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
|
||||
{% trans 'Export' %}
|
||||
</div>
|
||||
</div>
|
||||
@ -301,7 +337,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="container-fluid mt-2 mt-md-5 mt-xl-5 mt-lg-5" id="id_base_container">
|
||||
<div class="container-fluid mt-2 mt-md-5 mt-xl-5 mt-lg-5{% if request.user.userpreference.left_handed %} left-handed {% endif %}"
|
||||
id="id_base_container">
|
||||
<div class="row">
|
||||
<div class="col-xl-2 d-none d-xl-block">
|
||||
{% block content_xl_left %}
|
||||
@ -336,7 +373,7 @@
|
||||
{% block content_fluid %}
|
||||
{% endblock %}
|
||||
|
||||
{% user_prefs request as prefs%}
|
||||
{% user_prefs request as prefs %}
|
||||
{{ prefs|json_script:'user_preference' }}
|
||||
|
||||
</div>
|
||||
|
39
cookbook/templates/ingredient_editor.html
Normal file
39
cookbook/templates/ingredient_editor.html
Normal file
@ -0,0 +1,39 @@
|
||||
{% extends "base.html" %}
|
||||
{% load render_bundle from webpack_loader %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
|
||||
{% block title %}{% trans 'Ingredient Editor' %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<h3>{% trans 'Ingredient Editor' %}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div id="app">
|
||||
|
||||
<ingredient-editor-view></ingredient-editor-view>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block script %}
|
||||
{% if debug %}
|
||||
<script src="{% url 'js_reverse' %}"></script>
|
||||
{% else %}
|
||||
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
|
||||
{% endif %}
|
||||
|
||||
<script type="application/javascript">
|
||||
window.DEFAULT_FOOD = {{ food_id }}
|
||||
window.DEFAULT_UNIT = {{ unit_id }}
|
||||
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
|
||||
</script>
|
||||
|
||||
{% render_bundle 'ingredient_editor_view' %}
|
||||
{% endblock %}
|
@ -20,4 +20,8 @@
|
||||
<button class="btn btn-success" type="submit"><i class="fas fa-save"></i> {% trans 'Create Superuser account' %}</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
$('#id_name').focus()
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
@ -36,8 +36,7 @@
|
||||
<li class="list-group-item">
|
||||
{% trans 'Recipes' %} :
|
||||
<span class="badge badge-pill badge-info"
|
||||
>{{ counts.recipes }} / {% if request.space.max_recipes > 0 %} {{ request.space.max_recipes }}{%
|
||||
else %}∞{% endif %}</span
|
||||
>{{ counts.recipes }} / {% if request.space.max_recipes > 0 %} {{ request.space.max_recipes }}{% else %}∞{% endif %}</span
|
||||
>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
|
@ -47,7 +47,6 @@ router.register(r'user-name', api.UserNameViewSet, basename='username')
|
||||
router.register(r'user-preference', api.UserPreferenceViewSet)
|
||||
router.register(r'view-log', api.ViewLogViewSet)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.index, name='index'),
|
||||
path('setup/', views.setup, name='view_setup'),
|
||||
@ -72,6 +71,7 @@ urlpatterns = [
|
||||
path('settings/', views.user_settings, name='view_settings'),
|
||||
path('history/', views.history, name='view_history'),
|
||||
path('supermarket/', views.supermarket, name='view_supermarket'),
|
||||
path('ingredient-editor/', views.ingredient_editor, name='view_ingredient_editor'),
|
||||
path('abuse/<slug:token>', views.report_share_abuse, name='view_report_share_abuse'),
|
||||
|
||||
path('import/', import_export.import_recipe, name='view_import'),
|
||||
@ -116,7 +116,8 @@ urlpatterns = [
|
||||
path('api/share-link/<int:pk>', api.share_link, name='api_share_link'),
|
||||
path('api/get_facets/', api.get_facets, name='api_get_facets'),
|
||||
|
||||
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'), # TODO is this deprecated? not yet, some old forms remain, could likely be changed to generic API endpoints
|
||||
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'),
|
||||
# TODO is this deprecated? not yet, some old forms remain, could likely be changed to generic API endpoints
|
||||
path('dal/food/', dal.IngredientsAutocomplete.as_view(), name='dal_food'), # TODO is this deprecated?
|
||||
path('dal/unit/', dal.UnitAutocomplete.as_view(), name='dal_unit'), # TODO is this deprecated?
|
||||
|
||||
|
@ -70,7 +70,7 @@ from cookbook.serializer import (AutomationSerializer, BookmarkletImportSerializ
|
||||
SupermarketCategorySerializer, SupermarketSerializer,
|
||||
SyncLogSerializer, SyncSerializer, UnitSerializer,
|
||||
UserFileSerializer, UserNameSerializer, UserPreferenceSerializer,
|
||||
ViewLogSerializer)
|
||||
ViewLogSerializer, IngredientSimpleSerializer)
|
||||
from recipes import settings
|
||||
|
||||
|
||||
@ -121,14 +121,17 @@ class ExtendedRecipeMixin():
|
||||
|
||||
# add a recipe count annotation to the query
|
||||
# explanation on construction https://stackoverflow.com/a/43771738/15762829
|
||||
recipe_count = Recipe.objects.filter(**{recipe_filter: OuterRef('id')}, space=space).values(recipe_filter).annotate(count=Count('pk')).values('count')
|
||||
recipe_count = Recipe.objects.filter(**{recipe_filter: OuterRef('id')}, space=space).values(
|
||||
recipe_filter).annotate(count=Count('pk')).values('count')
|
||||
queryset = queryset.annotate(recipe_count=Coalesce(Subquery(recipe_count), 0))
|
||||
|
||||
# add a recipe image annotation to the query
|
||||
image_subquery = Recipe.objects.filter(**{recipe_filter: OuterRef('id')}, space=space).exclude(image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1]
|
||||
image_subquery = Recipe.objects.filter(**{recipe_filter: OuterRef('id')}, space=space).exclude(
|
||||
image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1]
|
||||
if tree:
|
||||
image_children_subquery = Recipe.objects.filter(**{f"{recipe_filter}__path__startswith": OuterRef('path')},
|
||||
space=space).exclude(image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1]
|
||||
image_children_subquery = Recipe.objects.filter(
|
||||
**{f"{recipe_filter}__path__startswith": OuterRef('path')},
|
||||
space=space).exclude(image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1]
|
||||
else:
|
||||
image_children_subquery = None
|
||||
if images:
|
||||
@ -144,11 +147,14 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
|
||||
def get_queryset(self):
|
||||
self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc())
|
||||
query = self.request.query_params.get('query', None)
|
||||
fuzzy = self.request.user.searchpreference.lookup or any([self.model.__name__.lower() in x for x in self.request.user.searchpreference.trigram.values_list('field', flat=True)])
|
||||
fuzzy = self.request.user.searchpreference.lookup or any([self.model.__name__.lower() in x for x in
|
||||
self.request.user.searchpreference.trigram.values_list(
|
||||
'field', flat=True)])
|
||||
|
||||
if query is not None and query not in ["''", '']:
|
||||
if fuzzy:
|
||||
if any([self.model.__name__.lower() in x for x in self.request.user.searchpreference.unaccent.values_list('field', flat=True)]):
|
||||
if any([self.model.__name__.lower() in x for x in
|
||||
self.request.user.searchpreference.unaccent.values_list('field', flat=True)]):
|
||||
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name__unaccent', query))
|
||||
else:
|
||||
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name', query))
|
||||
@ -156,7 +162,8 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
|
||||
else:
|
||||
# TODO have this check unaccent search settings or other search preferences?
|
||||
filter = Q(name__icontains=query)
|
||||
if any([self.model.__name__.lower() in x for x in self.request.user.searchpreference.unaccent.values_list('field', flat=True)]):
|
||||
if any([self.model.__name__.lower() in x for x in
|
||||
self.request.user.searchpreference.unaccent.values_list('field', flat=True)]):
|
||||
filter |= Q(name__unaccent__icontains=query)
|
||||
|
||||
self.queryset = (
|
||||
@ -277,10 +284,12 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin, ExtendedRecipeMixin):
|
||||
except self.model.DoesNotExist:
|
||||
self.queryset = self.model.objects.none()
|
||||
else:
|
||||
return self.annotate_recipe(queryset=super().get_queryset(), request=self.request, serializer=self.serializer_class, tree=True)
|
||||
return self.annotate_recipe(queryset=super().get_queryset(), request=self.request,
|
||||
serializer=self.serializer_class, tree=True)
|
||||
self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc())
|
||||
|
||||
return self.annotate_recipe(queryset=self.queryset, request=self.request, serializer=self.serializer_class, tree=True)
|
||||
return self.annotate_recipe(queryset=self.queryset, request=self.request, serializer=self.serializer_class,
|
||||
tree=True)
|
||||
|
||||
@decorators.action(detail=True, url_path='move/(?P<parent>[^/.]+)', methods=['PUT'], )
|
||||
@decorators.renderer_classes((TemplateHTMLRenderer, JSONRenderer))
|
||||
@ -456,12 +465,16 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
||||
pagination_class = DefaultPagination
|
||||
|
||||
def get_queryset(self):
|
||||
self.request._shared_users = [x.id for x in list(self.request.user.get_shopping_share())] + [self.request.user.id]
|
||||
self.request._shared_users = [x.id for x in list(self.request.user.get_shopping_share())] + [
|
||||
self.request.user.id]
|
||||
|
||||
self.queryset = super().get_queryset()
|
||||
shopping_status = ShoppingListEntry.objects.filter(space=self.request.space, food=OuterRef('id'), checked=False).values('id')
|
||||
shopping_status = ShoppingListEntry.objects.filter(space=self.request.space, food=OuterRef('id'),
|
||||
checked=False).values('id')
|
||||
# onhand_status = self.queryset.annotate(onhand_status=Exists(onhand_users_set__in=[shared_users]))
|
||||
return self.queryset.annotate(shopping_status=Exists(shopping_status)).prefetch_related('onhand_users', 'inherit_fields').select_related('recipe', 'supermarket_category')
|
||||
return self.queryset.annotate(shopping_status=Exists(shopping_status)).prefetch_related('onhand_users',
|
||||
'inherit_fields').select_related(
|
||||
'recipe', 'supermarket_category')
|
||||
|
||||
@decorators.action(detail=True, methods=['PUT'], serializer_class=FoodShoppingUpdateSerializer, )
|
||||
# TODO DRF only allows one action in a decorator action without overriding get_operation_id_base() this should be PUT and DELETE probably
|
||||
@ -472,7 +485,8 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
||||
shared_users = list(self.request.user.get_shopping_share())
|
||||
shared_users.append(request.user)
|
||||
if request.data.get('_delete', False) == 'true':
|
||||
ShoppingListEntry.objects.filter(food=obj, checked=False, space=request.space, created_by__in=shared_users).delete()
|
||||
ShoppingListEntry.objects.filter(food=obj, checked=False, space=request.space,
|
||||
created_by__in=shared_users).delete()
|
||||
content = {'msg': _(f'{obj.name} was removed from the shopping list.')}
|
||||
return Response(content, status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@ -480,7 +494,8 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
||||
unit = request.data.get('unit', None)
|
||||
content = {'msg': _(f'{obj.name} was added to the shopping list.')}
|
||||
|
||||
ShoppingListEntry.objects.create(food=obj, amount=amount, unit=unit, space=request.space, created_by=request.user)
|
||||
ShoppingListEntry.objects.create(food=obj, amount=amount, unit=unit, space=request.space,
|
||||
created_by=request.user)
|
||||
return Response(content, status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def destroy(self, *args, **kwargs):
|
||||
@ -579,8 +594,22 @@ class IngredientViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = IngredientSerializer
|
||||
permission_classes = [CustomIsUser]
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.request and self.request.query_params.get('simple', False):
|
||||
return IngredientSimpleSerializer
|
||||
return IngredientSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return self.queryset.filter(step__recipe__space=self.request.space)
|
||||
queryset = self.queryset.filter(step__recipe__space=self.request.space)
|
||||
food = self.request.query_params.get('food', None)
|
||||
if food and re.match(r'^(\d)+$', food):
|
||||
queryset = queryset.filter(food_id=food)
|
||||
|
||||
unit = self.request.query_params.get('unit', None)
|
||||
if unit and re.match(r'^(\d)+$', unit):
|
||||
queryset = queryset.filter(unit_id=unit)
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class StepViewSet(viewsets.ModelViewSet):
|
||||
@ -589,7 +618,8 @@ class StepViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [CustomIsUser]
|
||||
pagination_class = DefaultPagination
|
||||
query_params = [
|
||||
QueryParam(name='recipe', description=_('ID of recipe a step is part of. For multiple repeat parameter.'), qtype='int'),
|
||||
QueryParam(name='recipe', description=_('ID of recipe a step is part of. For multiple repeat parameter.'),
|
||||
qtype='int'),
|
||||
QueryParam(name='query', description=_('Query string matched (fuzzy) against object name.'), qtype='string'),
|
||||
]
|
||||
schema = QueryParamAutoSchema()
|
||||
@ -633,33 +663,63 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
||||
pagination_class = RecipePagination
|
||||
|
||||
query_params = [
|
||||
QueryParam(name='query', description=_('Query string matched (fuzzy) against recipe name. In the future also fulltext search.')),
|
||||
QueryParam(name='keywords', description=_('ID of keyword a recipe should have. For multiple repeat parameter. Equivalent to keywords_or'), qtype='int'),
|
||||
QueryParam(name='keywords_or', description=_('Keyword IDs, repeat for multiple. Return recipes with any of the keywords'), qtype='int'),
|
||||
QueryParam(name='keywords_and', description=_('Keyword IDs, repeat for multiple. Return recipes with all of the keywords.'), qtype='int'),
|
||||
QueryParam(name='keywords_or_not', description=_('Keyword IDs, repeat for multiple. Exclude recipes with any of the keywords.'), qtype='int'),
|
||||
QueryParam(name='keywords_and_not', description=_('Keyword IDs, repeat for multiple. Exclude recipes with all of the keywords.'), qtype='int'),
|
||||
QueryParam(name='foods', description=_('ID of food a recipe should have. For multiple repeat parameter.'), qtype='int'),
|
||||
QueryParam(name='foods_or', description=_('Food IDs, repeat for multiple. Return recipes with any of the foods'), qtype='int'),
|
||||
QueryParam(name='foods_and', description=_('Food IDs, repeat for multiple. Return recipes with all of the foods.'), qtype='int'),
|
||||
QueryParam(name='foods_or_not', description=_('Food IDs, repeat for multiple. Exclude recipes with any of the foods.'), qtype='int'),
|
||||
QueryParam(name='foods_and_not', description=_('Food IDs, repeat for multiple. Exclude recipes with all of the foods.'), qtype='int'),
|
||||
QueryParam(name='query', description=_(
|
||||
'Query string matched (fuzzy) against recipe name. In the future also fulltext search.')),
|
||||
QueryParam(name='keywords', description=_(
|
||||
'ID of keyword a recipe should have. For multiple repeat parameter. Equivalent to keywords_or'),
|
||||
qtype='int'),
|
||||
QueryParam(name='keywords_or',
|
||||
description=_('Keyword IDs, repeat for multiple. Return recipes with any of the keywords'),
|
||||
qtype='int'),
|
||||
QueryParam(name='keywords_and',
|
||||
description=_('Keyword IDs, repeat for multiple. Return recipes with all of the keywords.'),
|
||||
qtype='int'),
|
||||
QueryParam(name='keywords_or_not',
|
||||
description=_('Keyword IDs, repeat for multiple. Exclude recipes with any of the keywords.'),
|
||||
qtype='int'),
|
||||
QueryParam(name='keywords_and_not',
|
||||
description=_('Keyword IDs, repeat for multiple. Exclude recipes with all of the keywords.'),
|
||||
qtype='int'),
|
||||
QueryParam(name='foods', description=_('ID of food a recipe should have. For multiple repeat parameter.'),
|
||||
qtype='int'),
|
||||
QueryParam(name='foods_or',
|
||||
description=_('Food IDs, repeat for multiple. Return recipes with any of the foods'), qtype='int'),
|
||||
QueryParam(name='foods_and',
|
||||
description=_('Food IDs, repeat for multiple. Return recipes with all of the foods.'), qtype='int'),
|
||||
QueryParam(name='foods_or_not',
|
||||
description=_('Food IDs, repeat for multiple. Exclude recipes with any of the foods.'), qtype='int'),
|
||||
QueryParam(name='foods_and_not',
|
||||
description=_('Food IDs, repeat for multiple. Exclude recipes with all of the foods.'), qtype='int'),
|
||||
QueryParam(name='units', description=_('ID of unit a recipe should have.'), qtype='int'),
|
||||
QueryParam(name='rating', description=_('Rating a recipe should have or greater. [0 - 5] Negative value filters rating less than.'), qtype='int'),
|
||||
QueryParam(name='rating', description=_(
|
||||
'Rating a recipe should have or greater. [0 - 5] Negative value filters rating less than.'), qtype='int'),
|
||||
QueryParam(name='books', description=_('ID of book a recipe should be in. For multiple repeat parameter.')),
|
||||
QueryParam(name='books_or', description=_('Book IDs, repeat for multiple. Return recipes with any of the books'), qtype='int'),
|
||||
QueryParam(name='books_and', description=_('Book IDs, repeat for multiple. Return recipes with all of the books.'), qtype='int'),
|
||||
QueryParam(name='books_or_not', description=_('Book IDs, repeat for multiple. Exclude recipes with any of the books.'), qtype='int'),
|
||||
QueryParam(name='books_and_not', description=_('Book IDs, repeat for multiple. Exclude recipes with all of the books.'), qtype='int'),
|
||||
QueryParam(name='internal', description=_('If only internal recipes should be returned. [''true''/''<b>false</b>'']')),
|
||||
QueryParam(name='random', description=_('Returns the results in randomized order. [''true''/''<b>false</b>'']')),
|
||||
QueryParam(name='new', description=_('Returns new results first in search results. [''true''/''<b>false</b>'']')),
|
||||
QueryParam(name='timescooked', description=_('Filter recipes cooked X times or more. Negative values returns cooked less than X times'), qtype='int'),
|
||||
QueryParam(name='cookedon', description=_('Filter recipes last cooked on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||
QueryParam(name='createdon', description=_('Filter recipes created on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||
QueryParam(name='updatedon', description=_('Filter recipes updated on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||
QueryParam(name='viewedon', description=_('Filter recipes lasts viewed on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||
QueryParam(name='makenow', description=_('Filter recipes that can be made with OnHand food. [''true''/''<b>false</b>'']')),
|
||||
QueryParam(name='books_or',
|
||||
description=_('Book IDs, repeat for multiple. Return recipes with any of the books'), qtype='int'),
|
||||
QueryParam(name='books_and',
|
||||
description=_('Book IDs, repeat for multiple. Return recipes with all of the books.'), qtype='int'),
|
||||
QueryParam(name='books_or_not',
|
||||
description=_('Book IDs, repeat for multiple. Exclude recipes with any of the books.'), qtype='int'),
|
||||
QueryParam(name='books_and_not',
|
||||
description=_('Book IDs, repeat for multiple. Exclude recipes with all of the books.'), qtype='int'),
|
||||
QueryParam(name='internal',
|
||||
description=_('If only internal recipes should be returned. [''true''/''<b>false</b>'']')),
|
||||
QueryParam(name='random',
|
||||
description=_('Returns the results in randomized order. [''true''/''<b>false</b>'']')),
|
||||
QueryParam(name='new',
|
||||
description=_('Returns new results first in search results. [''true''/''<b>false</b>'']')),
|
||||
QueryParam(name='timescooked', description=_(
|
||||
'Filter recipes cooked X times or more. Negative values returns cooked less than X times'), qtype='int'),
|
||||
QueryParam(name='cookedon', description=_(
|
||||
'Filter recipes last cooked on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||
QueryParam(name='createdon', description=_(
|
||||
'Filter recipes created on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||
QueryParam(name='updatedon', description=_(
|
||||
'Filter recipes updated on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||
QueryParam(name='viewedon', description=_(
|
||||
'Filter recipes lasts viewed on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||
QueryParam(name='makenow',
|
||||
description=_('Filter recipes that can be made with OnHand food. [''true''/''<b>false</b>'']')),
|
||||
]
|
||||
schema = QueryParamAutoSchema()
|
||||
|
||||
@ -674,7 +734,8 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
||||
if not (share and self.detail):
|
||||
self.queryset = self.queryset.filter(space=self.request.space)
|
||||
|
||||
params = {x: self.request.GET.get(x) if len({**self.request.GET}[x]) == 1 else self.request.GET.getlist(x) for x in list(self.request.GET)}
|
||||
params = {x: self.request.GET.get(x) if len({**self.request.GET}[x]) == 1 else self.request.GET.getlist(x) for x
|
||||
in list(self.request.GET)}
|
||||
search = RecipeSearch(self.request, **params)
|
||||
self.queryset = search.get_queryset(self.queryset).prefetch_related('cooklog_set')
|
||||
return self.queryset
|
||||
@ -785,7 +846,8 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
||||
levels = int(request.query_params.get('levels', 1))
|
||||
except (ValueError, TypeError):
|
||||
levels = 1
|
||||
qs = obj.get_related_recipes(levels=levels) # TODO: make levels a user setting, included in request data?, keep solely in the backend?
|
||||
qs = obj.get_related_recipes(
|
||||
levels=levels) # TODO: make levels a user setting, included in request data?, keep solely in the backend?
|
||||
return Response(self.serializer_class(qs, many=True).data)
|
||||
|
||||
|
||||
@ -795,7 +857,8 @@ class ShoppingListRecipeViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [CustomIsOwner | CustomIsShared]
|
||||
|
||||
def get_queryset(self):
|
||||
self.queryset = self.queryset.filter(Q(shoppinglist__space=self.request.space) | Q(entries__space=self.request.space))
|
||||
self.queryset = self.queryset.filter(
|
||||
Q(shoppinglist__space=self.request.space) | Q(entries__space=self.request.space))
|
||||
return self.queryset.filter(
|
||||
Q(shoppinglist__created_by=self.request.user)
|
||||
| Q(shoppinglist__shared=self.request.user)
|
||||
@ -809,12 +872,17 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = ShoppingListEntrySerializer
|
||||
permission_classes = [CustomIsOwner | CustomIsShared]
|
||||
query_params = [
|
||||
QueryParam(name='id', description=_('Returns the shopping list entry with a primary key of id. Multiple values allowed.'), qtype='int'),
|
||||
QueryParam(name='id',
|
||||
description=_('Returns the shopping list entry with a primary key of id. Multiple values allowed.'),
|
||||
qtype='int'),
|
||||
QueryParam(
|
||||
name='checked',
|
||||
description=_('Filter shopping list entries on checked. [''true'', ''false'', ''both'', ''<b>recent</b>'']<br> - ''recent'' includes unchecked items and recently completed items.')
|
||||
description=_(
|
||||
'Filter shopping list entries on checked. [''true'', ''false'', ''both'', ''<b>recent</b>'']<br> - ''recent'' includes unchecked items and recently completed items.')
|
||||
),
|
||||
QueryParam(name='supermarket', description=_('Returns the shopping list entries sorted by supermarket category order.'), qtype='int'),
|
||||
QueryParam(name='supermarket',
|
||||
description=_('Returns the shopping list entries sorted by supermarket category order.'),
|
||||
qtype='int'),
|
||||
]
|
||||
schema = QueryParamAutoSchema()
|
||||
|
||||
|
@ -61,7 +61,8 @@ def search(request):
|
||||
if request.user.userpreference.search_style == UserPreference.NEW:
|
||||
return search_v2(request)
|
||||
f = RecipeFilter(request.GET,
|
||||
queryset=Recipe.objects.filter(space=request.user.userpreference.space).all().order_by(Lower('name').asc()),
|
||||
queryset=Recipe.objects.filter(space=request.user.userpreference.space).all().order_by(
|
||||
Lower('name').asc()),
|
||||
space=request.space)
|
||||
if request.user.userpreference.search_style == UserPreference.LARGE:
|
||||
table = RecipeTable(f.qs)
|
||||
@ -225,6 +226,19 @@ def supermarket(request):
|
||||
return render(request, 'supermarket.html', {})
|
||||
|
||||
|
||||
@group_required('user')
|
||||
def ingredient_editor(request):
|
||||
template_vars = {'food_id': -1, 'unit_id': -1}
|
||||
food_id = request.GET.get('food_id', None)
|
||||
if food_id and re.match(r'^(\d)+$', food_id):
|
||||
template_vars['food_id'] = food_id
|
||||
|
||||
unit_id = request.GET.get('unit_id', None)
|
||||
if unit_id and re.match(r'^(\d)+$', unit_id):
|
||||
template_vars['unit_id'] = unit_id
|
||||
return render(request, 'ingredient_editor.html', template_vars)
|
||||
|
||||
|
||||
@group_required('user')
|
||||
def meal_plan_entry(request, pk):
|
||||
plan = MealPlan.objects.filter(space=request.space).get(pk=pk)
|
||||
@ -304,6 +318,7 @@ def user_settings(request):
|
||||
up.use_fractions = form.cleaned_data['use_fractions']
|
||||
up.use_kj = form.cleaned_data['use_kj']
|
||||
up.sticky_navbar = form.cleaned_data['sticky_navbar']
|
||||
up.left_handed = form.cleaned_data['left_handed']
|
||||
|
||||
up.save()
|
||||
|
||||
|
@ -15,13 +15,13 @@ Code contributions are always welcome. There is no special rules for what you ne
|
||||
just do your best and we will work together to get your idea and code merged into the project.
|
||||
|
||||
!!! info
|
||||
The dev setup is a little messy as this application combines the best (at least in my opinion) of django and Vue.js.
|
||||
The dev setup is a little messy as this application combines the best (at least in my opinion) of both Django and Vue.js.
|
||||
|
||||
### Django
|
||||
This application is developed using the Django framework for Python. They have excellent
|
||||
[documentation](https://www.djangoproject.com/start/) on how to get started, so I will only give you the basics here.
|
||||
|
||||
1. Clone this repository wherever you like and install the Python language for your OS (at least version 3.8)
|
||||
1. Clone this repository wherever you like and install the Python language for your OS (I recommend using version 3.10 or above)
|
||||
2. Open it in your favorite editor/IDE (e.g. PyCharm)
|
||||
1. If you want, create a virtual environment for all your packages.
|
||||
3. Install all required packages: `pip install -r requirements.txt`
|
||||
@ -32,12 +32,13 @@ There is **no** need to set any environment variables. By default, a simple sqli
|
||||
populated from default values.
|
||||
|
||||
### Vue.js
|
||||
Some of the more complex pages use [Vue.js](https://vuejs.org/) to enhance the frontend.
|
||||
Most new frontend pages are build using [Vue.js](https://vuejs.org/).
|
||||
|
||||
In order to work on these pages you will have to install a Javascript package manager of your choice. The following examples use yarn.
|
||||
|
||||
Run `yarn install` to install the dependencies. After that you can use `yarn serve` to start the development server
|
||||
and go ahead and test your changes. Before committing please make sure to pack the source using `yarn build`.
|
||||
and go ahead and test your changes. If you do not want to work on those pages but want the application to work properly during
|
||||
development run `yarn build` to build the frontend pages once.
|
||||
|
||||
#### API Client
|
||||
The API Client is generated automatically from the openapi interface provided by the django rest framework.
|
||||
@ -51,11 +52,7 @@ Generate the schema using `openapi-generator-cli generate -g typescript-axios -i
|
||||
|
||||
## Contribute Documentation
|
||||
The documentation is build from the markdown files in the [docs](https://github.com/vabene1111/recipes/tree/develop/docs)
|
||||
folder of the GitHub repository.
|
||||
|
||||
!!! warning "Deployment Branch"
|
||||
The documentation is currently build from the `develop` branch of the GitHub repository as it is evolving rapidly.
|
||||
This will likely change in the future to prevent issues with documentation being released before the features.
|
||||
folder of the GitHub repository.
|
||||
|
||||
In order to contribute to the documentation you can fork the repository and edit the markdown files in the browser.
|
||||
|
||||
@ -69,10 +66,6 @@ If you know any foreign languages that is not yet translated feel free to contri
|
||||
|
||||
Translations are managed on [translate.tandoor.dev](https://translate.tandoor.dev/), a self hosted instance of [Weblate](https://weblate.org/de/).
|
||||
|
||||
!!! info "Weblate functionality"
|
||||
Translations have only recently been migrated to weblate so I do not 100% understand each feature.
|
||||
Please feel free to contact me if you need any help getting started.
|
||||
|
||||
You can simply register an account and then follow these steps to add translations:
|
||||
|
||||
1. After registering you are asked to select your languages. This is optional but allows weblate to only show you relevant translations
|
||||
@ -80,13 +73,15 @@ You can simply register an account and then follow these steps to add translatio
|
||||
3. Select Tandoor and on the top right hand corner select `Watch project Tandoor` (click on `Not watching`)
|
||||
4. Go back to the dashboard. It now shows you the relevant translations for your languages. Click the pencil icon to get started.
|
||||
|
||||
!!!! info "Creating a new languagte"
|
||||
!!! info "Creating a new language"
|
||||
To create a new language you must first select Tandoor (the project) and then a component.
|
||||
Here you will have the option to add the language. Afterwards you can also simply add it to the other components as well.
|
||||
Once a new language is (partially) finished let me know on GitHub so I can add it to the language switcher in Tandoor itself.
|
||||
|
||||
There is also [a lot of documentation](https://docs.weblate.org/en/latest/user/translating.html) available from Weblate directly.
|
||||
|
||||

|
||||
|
||||
It is also possible to provide the translations directly by creating a new language
|
||||
using `manage.py makemessages -l <language_code> -i venv`. Once finished, simply open a PR with the changed files.
|
||||
using `manage.py makemessages -l <language_code> -i venv`. Once finished, simply open a PR with the changed files. This sometimes causes issues merging
|
||||
with weblate so I would prefer the use of weblate.
|
||||
|
@ -42,8 +42,8 @@ services:
|
||||
- web_recipes
|
||||
volumes:
|
||||
- nginx_config:/etc/nginx/conf.d:ro
|
||||
- staticfiles:/static
|
||||
- ${MEDIA_FILES_DIR:-./mediafiles}:/media
|
||||
- staticfiles:/static:ro
|
||||
- ${MEDIA_FILES_DIR:-./mediafiles}:/media:ro
|
||||
networks:
|
||||
tandoor:
|
||||
ipv6_address: ${IPV6_PREFIX:?NO_IPV6_PREFIX}::4
|
||||
|
@ -11,6 +11,7 @@ services:
|
||||
- default
|
||||
|
||||
web_recipes:
|
||||
restart: always
|
||||
image: vabene1111/recipes
|
||||
env_file:
|
||||
- ./.env
|
||||
@ -32,8 +33,8 @@ services:
|
||||
- web_recipes
|
||||
volumes:
|
||||
- nginx_config:/etc/nginx/conf.d:ro
|
||||
- staticfiles:/static
|
||||
- ./mediafiles:/media
|
||||
- staticfiles:/static:ro
|
||||
- ./mediafiles:/media:ro
|
||||
networks:
|
||||
- default
|
||||
- nginx-proxy
|
||||
|
@ -9,6 +9,7 @@ services:
|
||||
- ./.env
|
||||
|
||||
web_recipes:
|
||||
restart: always
|
||||
image: vabene1111/recipes
|
||||
env_file:
|
||||
- ./.env
|
||||
@ -30,8 +31,8 @@ services:
|
||||
- web_recipes
|
||||
volumes:
|
||||
- nginx_config:/etc/nginx/conf.d:ro
|
||||
- staticfiles:/static
|
||||
- ./mediafiles:/media
|
||||
- staticfiles:/static:ro
|
||||
- ./mediafiles:/media:ro
|
||||
|
||||
volumes:
|
||||
nginx_config:
|
||||
|
@ -11,6 +11,7 @@ services:
|
||||
- default
|
||||
|
||||
web_recipes:
|
||||
restart: always
|
||||
image: vabene1111/recipes
|
||||
env_file:
|
||||
- ./.env
|
||||
@ -30,8 +31,8 @@ services:
|
||||
- ./.env
|
||||
volumes:
|
||||
- nginx_config:/etc/nginx/conf.d:ro
|
||||
- staticfiles:/static
|
||||
- ./mediafiles:/media
|
||||
- staticfiles:/static:ro
|
||||
- ./mediafiles:/media:ro
|
||||
labels: # traefik example labels
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.recipes.rule=Host(`recipes.mydomain.com`, `recipes.myotherdomain.com`)"
|
||||
@ -45,7 +46,7 @@ services:
|
||||
|
||||
networks:
|
||||
default:
|
||||
traefik: # This is you external traefik network
|
||||
traefik: # This is your external traefik network
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
|
@ -4,7 +4,6 @@ metadata:
|
||||
labels:
|
||||
app: recipes
|
||||
name: recipes-nginx-config
|
||||
namespace: default
|
||||
data:
|
||||
nginx-config: |-
|
||||
events {
|
||||
|
@ -2,7 +2,6 @@ kind: Secret
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: recipes
|
||||
namespace: default
|
||||
type: Opaque
|
||||
data:
|
||||
# echo -n 'db-password' | base64
|
||||
|
@ -2,4 +2,3 @@ apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: recipes
|
||||
namespace: default
|
||||
|
@ -2,7 +2,6 @@ apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: recipes-media
|
||||
namespace: default
|
||||
labels:
|
||||
app: recipes
|
||||
spec:
|
||||
@ -16,7 +15,6 @@ apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: recipes-static
|
||||
namespace: default
|
||||
labels:
|
||||
app: recipes
|
||||
spec:
|
||||
|
@ -5,7 +5,6 @@ metadata:
|
||||
app: recipes
|
||||
tier: database
|
||||
name: recipes-postgresql
|
||||
namespace: default
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
@ -22,7 +21,6 @@ spec:
|
||||
app: recipes
|
||||
tier: database
|
||||
name: recipes-postgresql
|
||||
namespace: default
|
||||
spec:
|
||||
containers:
|
||||
- name: recipes-db
|
||||
@ -124,7 +122,6 @@ spec:
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: data
|
||||
namespace: default
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
|
@ -5,7 +5,6 @@ metadata:
|
||||
app: recipes
|
||||
tier: database
|
||||
name: recipes-postgresql
|
||||
namespace: default
|
||||
spec:
|
||||
ports:
|
||||
- name: postgresql
|
||||
|
@ -2,7 +2,6 @@ apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: recipes
|
||||
namespace: default
|
||||
labels:
|
||||
app: recipes
|
||||
environment: production
|
||||
|
@ -2,7 +2,6 @@ apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: recipes
|
||||
namespace: default
|
||||
labels:
|
||||
app: recipes
|
||||
tier: frontend
|
||||
|
@ -5,7 +5,6 @@ metadata:
|
||||
#cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
#kubernetes.io/ingress.class: nginx
|
||||
name: recipes
|
||||
namespace: default
|
||||
spec:
|
||||
rules:
|
||||
- host: recipes.local
|
||||
|
@ -377,6 +377,7 @@ LANGUAGES = [
|
||||
('pl', _('Polish')),
|
||||
('ru', _('Russian')),
|
||||
('es', _('Spanish')),
|
||||
('sv', _('Swedish')),
|
||||
]
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
|
234
vue/src/apps/IngredientEditorView/IngredientEditorView.vue
Normal file
234
vue/src/apps/IngredientEditorView/IngredientEditorView.vue
Normal file
@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<beta-warning></beta-warning>
|
||||
|
||||
<div class="row mt-2">
|
||||
<div class="col col-md-6">
|
||||
<generic-multiselect @change="food = $event.val; refreshList()"
|
||||
:model="Models.FOOD"
|
||||
:initial_single_selection="food"
|
||||
:multiple="false"></generic-multiselect>
|
||||
<b-button @click="show_food_delete=true" :disabled="food === null"><i class="fas fa-trash-alt"></i>
|
||||
</b-button>
|
||||
<b-button @click="generic_model = Models.FOOD; generic_action=Actions.MERGE" :disabled="food === null">
|
||||
<i class="fas fa-compress-arrows-alt"></i>
|
||||
</b-button>
|
||||
<generic-modal-form :model="Models.FOOD" :action="generic_action" :show="generic_model === Models.FOOD"
|
||||
:item1="food"
|
||||
@finish-action="food = null; generic_action=null; generic_model=null"/>
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
|
||||
<generic-multiselect
|
||||
@change="unit = $event.val; refreshList()"
|
||||
:model="Models.UNIT"
|
||||
:initial_single_selection="unit"
|
||||
:multiple="false"></generic-multiselect>
|
||||
|
||||
<b-button @click="generic_model = Models.UNIT; generic_action=Actions.DELETE" :disabled="unit === null">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</b-button>
|
||||
<b-button @click="generic_model = Models.UNIT; generic_action=Actions.MERGE" :disabled="unit === null">
|
||||
<i class="fas fa-compress-arrows-alt"></i>
|
||||
</b-button>
|
||||
<generic-modal-form :model="Models.UNIT" :action="generic_action" :show="generic_model === Models.UNIT"
|
||||
:item1="unit"
|
||||
@finish-action="unit = null; generic_action=null; generic_model=null"/>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-2">
|
||||
<div class="col col-md-12">
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('Amount') }}</th>
|
||||
<th>{{ $t('Unit') }}</th>
|
||||
<th>{{ $t('Food') }}</th>
|
||||
<th>{{ $t('Note') }}</th>
|
||||
<th>
|
||||
<b-button variant="success" @click="updateIngredient()"><i class="fas fa-save"></i>
|
||||
</b-button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr v-if="loading">
|
||||
<td colspan="4">
|
||||
<loading-spinner></loading-spinner>
|
||||
</td>
|
||||
</tr>
|
||||
<template v-if="!loading">
|
||||
<tbody v-for="i in ingredients" v-bind:key="i.id">
|
||||
<tr v-if="i.used_in_recipes.length > 0">
|
||||
<td colspan="5">
|
||||
<a v-for="r in i.used_in_recipes" :href="resolveDjangoUrl('view_recipe',r.id)"
|
||||
v-bind:key="r.id" target="_blank" rel="noreferrer nofollow">{{ r.name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width: 5vw">
|
||||
<input type="number" class="form-control" v-model="i.amount"
|
||||
@input="$set(i, 'changed', true)">
|
||||
</td>
|
||||
<td style="width: 30vw">
|
||||
<generic-multiselect @change="i.unit = $event.val; $set(i, 'changed', true)"
|
||||
:initial_single_selection="i.unit"
|
||||
:model="Models.UNIT"
|
||||
:search_on_load="false"
|
||||
:allow_create="true"
|
||||
:create_placeholder="$t('Create')"
|
||||
:multiple="false"></generic-multiselect>
|
||||
</td>
|
||||
<td style="width: 30vw">
|
||||
<generic-multiselect @change="i.food = $event.val; $set(i, 'changed', true)"
|
||||
:initial_single_selection="i.food"
|
||||
:model="Models.FOOD"
|
||||
:search_on_load="false"
|
||||
:allow_create="true"
|
||||
:create_placeholder="$t('Create')"
|
||||
:multiple="false"></generic-multiselect>
|
||||
</td>
|
||||
<td style="width: 30vw">
|
||||
<input class="form-control" v-model="i.note" @keydown="$set(i, 'changed', true)">
|
||||
|
||||
</td>
|
||||
<td style="width: 5vw">
|
||||
<b-button :disabled="i.changed !== true"
|
||||
:variant="(i.changed !== true) ? 'primary' : 'success'"
|
||||
@click="updateIngredient(i)">
|
||||
<i class="fas fa-save"></i>
|
||||
</b-button>
|
||||
<b-button variant="danger"
|
||||
@click="deleteIngredient(i)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</b-button>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue"
|
||||
import {BootstrapVue} from "bootstrap-vue"
|
||||
|
||||
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||
import {ApiMixin, ResolveUrlMixin, StandardToasts} from "@/utils/utils"
|
||||
import {ApiApiFactory} from "@/utils/openapi/api";
|
||||
import GenericMultiselect from "@/components/GenericMultiselect";
|
||||
import GenericModalForm from "@/components/Modals/GenericModalForm";
|
||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
import BetaWarning from "@/components/BetaWarning";
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
export default {
|
||||
name: "IngredientEditorView",
|
||||
mixins: [ApiMixin, ResolveUrlMixin],
|
||||
components: {BetaWarning, LoadingSpinner, GenericMultiselect, GenericModalForm},
|
||||
data() {
|
||||
return {
|
||||
ingredients: [],
|
||||
loading: false,
|
||||
food: null,
|
||||
unit: null,
|
||||
generic_action: null,
|
||||
generic_model: null,
|
||||
show_food_delete: false,
|
||||
show_unit_delete: false,
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
mounted() {
|
||||
this.$i18n.locale = window.CUSTOM_LOCALE
|
||||
if (window.DEFAULT_FOOD !== -1) {
|
||||
this.food = {id: window.DEFAULT_FOOD}
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.retrieveFood(this.food.id).then(r => {
|
||||
this.food = r.data
|
||||
})
|
||||
}
|
||||
if (window.DEFAULT_UNIT !== -1) {
|
||||
this.unit = {id: window.DEFAULT_UNIT}
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.retrieveUnit(this.unit.id).then(r => {
|
||||
this.unit = r.data
|
||||
})
|
||||
}
|
||||
this.refreshList()
|
||||
},
|
||||
methods: {
|
||||
refreshList: function () {
|
||||
if (this.food === null && this.unit === null) {
|
||||
this.ingredients = []
|
||||
} else {
|
||||
this.loading = true
|
||||
let apiClient = new ApiApiFactory()
|
||||
let params = {'query': {'simple': 1}}
|
||||
if (this.food !== null) {
|
||||
params.query.food = this.food.id
|
||||
}
|
||||
if (this.unit !== null) {
|
||||
params.query.unit = this.unit.id
|
||||
}
|
||||
apiClient.listIngredients(params).then(result => {
|
||||
this.ingredients = result.data
|
||||
this.loading = false
|
||||
}).catch((err) => {
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
},
|
||||
updateIngredient: function (i) {
|
||||
let update_list = []
|
||||
if (i === undefined) {
|
||||
this.ingredients.forEach(x => {
|
||||
if (x.changed) {
|
||||
update_list.push(x)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
update_list = [i]
|
||||
}
|
||||
|
||||
update_list.forEach(i => {
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.updateIngredient(i.id, i).then(r => {
|
||||
this.$set(i, 'changed', false)
|
||||
}).catch((r, e) => {
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
|
||||
})
|
||||
})
|
||||
},
|
||||
deleteIngredient: function (i){
|
||||
if (confirm(this.$t('delete_confirmation', this.$t('Ingredient')))){
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.destroyIngredient(i.id).then(r => {
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_DELETE)
|
||||
this.ingredients = this.ingredients.filter(li => li.id !== i.id)
|
||||
}).catch(e => {
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_DELETE)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
18
vue/src/apps/IngredientEditorView/main.js
Normal file
18
vue/src/apps/IngredientEditorView/main.js
Normal file
@ -0,0 +1,18 @@
|
||||
import Vue from 'vue'
|
||||
import App from './IngredientEditorView.vue'
|
||||
import i18n from '@/i18n'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
// TODO move this and other default stuff to centralized JS file (verify nothing breaks)
|
||||
let publicPath = localStorage.STATIC_URL + 'vue/'
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
publicPath = 'http://localhost:8080/'
|
||||
}
|
||||
export default __webpack_public_path__ = publicPath // eslint-disable-line
|
||||
|
||||
|
||||
new Vue({
|
||||
i18n,
|
||||
render: h => h(App),
|
||||
}).$mount('#app')
|
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div id="app" style="margin-bottom: 4vh" v-if="this_model">
|
||||
<generic-modal-form v-if="this_model" :model="this_model" :action="this_action" :item1="this_item" :item2="this_target" :show="show_modal" @finish-action="finishAction" />
|
||||
<generic-modal-form v-if="this_model" :model="this_model" :action="this_action" :item1="this_item"
|
||||
:item2="this_target" :show="show_modal" @finish-action="finishAction"/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-2 d-none d-md-block"></div>
|
||||
@ -17,17 +18,20 @@
|
||||
<div class="col-md-9" style="margin-top: 1vh">
|
||||
<h3>
|
||||
<!-- <span><b-button variant="link" size="sm" class="text-dark shadow-none"><i class="fas fa-chevron-down"></i></b-button></span> -->
|
||||
<model-menu />
|
||||
<model-menu/>
|
||||
<span>{{ $t(this.this_model.name) }}</span>
|
||||
<span v-if="apiName !== 'Step' && apiName !== 'CustomFilter'">
|
||||
<b-button variant="link" @click="startAction({ action: 'new' })">
|
||||
<i class="fas fa-plus-circle fa-2x"></i>
|
||||
</b-button> </span
|
||||
><!-- TODO add proper field to model config to determine if create should be available or not -->
|
||||
>
|
||||
<!-- TODO add proper field to model config to determine if create should be available or not -->
|
||||
</h3>
|
||||
</div>
|
||||
<div class="col-md-3" style="position: relative; margin-top: 1vh">
|
||||
<b-form-checkbox v-model="show_split" name="check-button" v-if="paginated" class="shadow-none" style="position: relative; top: 50%; transform: translateY(-50%)" switch>
|
||||
<b-form-checkbox v-model="show_split" name="check-button" v-if="paginated"
|
||||
class="shadow-none"
|
||||
style="position: relative; top: 50%; transform: translateY(-50%)" switch>
|
||||
{{ $t("show_split_screen") }}
|
||||
</b-form-checkbox>
|
||||
</div>
|
||||
@ -37,19 +41,29 @@
|
||||
<div class="col" :class="{ 'col-md-6': show_split }">
|
||||
<!-- model isn't paginated and loads in one API call -->
|
||||
<div v-if="!paginated">
|
||||
<generic-horizontal-card v-for="i in items_left" v-bind:key="i.id" :item="i" :model="this_model" @item-action="startAction($event, 'left')" @finish-action="finishAction" />
|
||||
<generic-horizontal-card v-for="i in items_left" v-bind:key="i.id" :item="i"
|
||||
:model="this_model" @item-action="startAction($event, 'left')"
|
||||
@finish-action="finishAction"/>
|
||||
</div>
|
||||
<!-- model is paginated and needs managed -->
|
||||
<generic-infinite-cards v-if="paginated" :card_counts="left_counts" :scroll="show_split" @search="getItems($event, 'left')" @reset="resetList('left')">
|
||||
<generic-infinite-cards v-if="paginated" :card_counts="left_counts" :scroll="show_split"
|
||||
@search="getItems($event, 'left')" @reset="resetList('left')">
|
||||
<template v-slot:cards>
|
||||
<generic-horizontal-card v-for="i in items_left" v-bind:key="i.id" :item="i" :model="this_model" @item-action="startAction($event, 'left')" @finish-action="finishAction" />
|
||||
<generic-horizontal-card v-for="i in items_left" v-bind:key="i.id" :item="i"
|
||||
:model="this_model"
|
||||
@item-action="startAction($event, 'left')"
|
||||
@finish-action="finishAction"/>
|
||||
</template>
|
||||
</generic-infinite-cards>
|
||||
</div>
|
||||
<div class="col col-md-6" v-if="show_split">
|
||||
<generic-infinite-cards v-if="this_model" :card_counts="right_counts" :scroll="show_split" @search="getItems($event, 'right')" @reset="resetList('right')">
|
||||
<generic-infinite-cards v-if="this_model" :card_counts="right_counts" :scroll="show_split"
|
||||
@search="getItems($event, 'right')" @reset="resetList('right')">
|
||||
<template v-slot:cards>
|
||||
<generic-horizontal-card v-for="i in items_right" v-bind:key="i.id" :item="i" :model="this_model" @item-action="startAction($event, 'right')" @finish-action="finishAction" />
|
||||
<generic-horizontal-card v-for="i in items_right" v-bind:key="i.id" :item="i"
|
||||
:model="this_model"
|
||||
@item-action="startAction($event, 'right')"
|
||||
@finish-action="finishAction"/>
|
||||
</template>
|
||||
</generic-infinite-cards>
|
||||
</div>
|
||||
@ -62,18 +76,18 @@
|
||||
|
||||
<script>
|
||||
import Vue from "vue"
|
||||
import { BootstrapVue } from "bootstrap-vue"
|
||||
import {BootstrapVue} from "bootstrap-vue"
|
||||
|
||||
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||
|
||||
import { CardMixin, ApiMixin, getConfig } from "@/utils/utils"
|
||||
import { StandardToasts, ToastMixin } from "@/utils/utils"
|
||||
import {CardMixin, ApiMixin, getConfig, resolveDjangoUrl} from "@/utils/utils"
|
||||
import {StandardToasts, ToastMixin} from "@/utils/utils"
|
||||
|
||||
import GenericInfiniteCards from "@/components/GenericInfiniteCards"
|
||||
import GenericHorizontalCard from "@/components/GenericHorizontalCard"
|
||||
import GenericModalForm from "@/components/Modals/GenericModalForm"
|
||||
import ModelMenu from "@/components/ContextMenu/ModelMenu"
|
||||
import { ApiApiFactory } from "@/utils/openapi/api"
|
||||
import {ApiApiFactory} from "@/utils/openapi/api"
|
||||
//import StorageQuota from "@/components/StorageQuota";
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
@ -94,8 +108,8 @@ export default {
|
||||
// this.Models and this.Actions inherited from ApiMixin
|
||||
items_left: [],
|
||||
items_right: [],
|
||||
right_counts: { max: 9999, current: 0 },
|
||||
left_counts: { max: 9999, current: 0 },
|
||||
right_counts: {max: 9999, current: 0},
|
||||
left_counts: {max: 9999, current: 0},
|
||||
this_model: undefined,
|
||||
model_menu: undefined,
|
||||
this_action: undefined,
|
||||
@ -127,7 +141,7 @@ export default {
|
||||
this.header_component_name = this.this_model?.list?.header_component?.name ?? undefined
|
||||
this.$nextTick(() => {
|
||||
if (!this.paginated) {
|
||||
this.getItems({ page: 1 }, "left")
|
||||
this.getItems({page: 1}, "left")
|
||||
}
|
||||
})
|
||||
this.$i18n.locale = window.CUSTOM_LOCALE
|
||||
@ -144,7 +158,6 @@ export default {
|
||||
let target = e?.target ?? undefined
|
||||
this.this_item = source
|
||||
this.this_target = target
|
||||
|
||||
switch (e.action) {
|
||||
case "delete":
|
||||
this.this_action = this.Actions.DELETE
|
||||
@ -159,6 +172,16 @@ export default {
|
||||
this.this_action = this.Actions.UPDATE
|
||||
this.show_modal = true
|
||||
break
|
||||
case "ingredient-editor": {
|
||||
let url = resolveDjangoUrl("view_ingredient_editor")
|
||||
if (this.this_model === this.Models.FOOD) {
|
||||
window.location.href = url + '?food_id=' + e.source.id
|
||||
}
|
||||
if (this.this_model === this.Models.UNIT) {
|
||||
window.location.href = url + '?unit_id=' + e.source.id
|
||||
}
|
||||
break
|
||||
}
|
||||
case "move":
|
||||
if (target == null) {
|
||||
this.this_item = e.source
|
||||
@ -236,7 +259,7 @@ export default {
|
||||
},
|
||||
getItems: function (params = {}, col) {
|
||||
let column = col || "left"
|
||||
params.options = { query: { extended: 1 } } // returns extended values in API response
|
||||
params.options = {query: {extended: 1}} // returns extended values in API response
|
||||
this.genericAPI(this.this_model, this.Actions.LIST, params)
|
||||
.then((result) => {
|
||||
let results = result.data?.results ?? result.data
|
||||
@ -257,7 +280,7 @@ export default {
|
||||
})
|
||||
},
|
||||
getThis: function (id, callback) {
|
||||
return this.genericAPI(this.this_model, this.Actions.FETCH, { id: id })
|
||||
return this.genericAPI(this.this_model, this.Actions.FETCH, {id: id})
|
||||
},
|
||||
saveThis: function (item) {
|
||||
if (!item?.id) {
|
||||
@ -268,7 +291,7 @@ export default {
|
||||
// then place all new items at the top of the list - could sort instead
|
||||
this.items_left = [result.data].concat(this.destroyCard(result?.data?.id, this.items_left))
|
||||
// this creates a deep copy to make sure that columns stay independent
|
||||
this.items_right = [{ ...result.data }].concat(this.destroyCard(result?.data?.id, this.items_right))
|
||||
this.items_right = [{...result.data}].concat(this.destroyCard(result?.data?.id, this.items_right))
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||
})
|
||||
.catch((err) => {
|
||||
@ -291,10 +314,10 @@ export default {
|
||||
addShopping: function (food) {
|
||||
let api = new ApiApiFactory()
|
||||
food.shopping = true
|
||||
api.createShoppingListEntry({ food: food, amount: 1 }).then(() => {
|
||||
api.createShoppingListEntry({food: food, amount: 1}).then(() => {
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||
this.refreshCard(food, this.items_left)
|
||||
this.refreshCard({ ...food }, this.items_right)
|
||||
this.refreshCard({...food}, this.items_right)
|
||||
})
|
||||
},
|
||||
updateThis: function (item) {
|
||||
@ -313,7 +336,7 @@ export default {
|
||||
this.clearState()
|
||||
return
|
||||
}
|
||||
this.genericAPI(this.this_model, this.Actions.MOVE, { source: source_id, target: target_id })
|
||||
this.genericAPI(this.this_model, this.Actions.MOVE, {source: source_id, target: target_id})
|
||||
.then((result) => {
|
||||
this.moveUpdateItem(source_id, target_id)
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_MOVE)
|
||||
@ -395,8 +418,8 @@ export default {
|
||||
let params = {
|
||||
root: item.id,
|
||||
pageSize: 200,
|
||||
query: { extended: 1 },
|
||||
options: { query: { extended: 1 } },
|
||||
query: {extended: 1},
|
||||
options: {query: {extended: 1}},
|
||||
}
|
||||
this.genericAPI(this.this_model, this.Actions.LIST, params)
|
||||
.then((result) => {
|
||||
@ -415,7 +438,7 @@ export default {
|
||||
getRecipes: function (col, item) {
|
||||
let parent = {}
|
||||
// TODO: make this generic
|
||||
let params = { pageSize: 50, random: true }
|
||||
let params = {pageSize: 50, random: true}
|
||||
params[this.this_recipe_param] = item.id
|
||||
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params)
|
||||
.then((result) => {
|
||||
@ -434,7 +457,7 @@ export default {
|
||||
refreshThis: function (id) {
|
||||
this.getThis(id).then((result) => {
|
||||
this.refreshCard(result.data, this.items_left)
|
||||
this.refreshCard({ ...result.data }, this.items_right)
|
||||
this.refreshCard({...result.data}, this.items_right)
|
||||
})
|
||||
},
|
||||
deleteThis: function (id) {
|
||||
|
@ -11,7 +11,7 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<label for="id_name"> {{ $t("Name") }}</label>
|
||||
<input class="form-control" id="id_name" v-model="recipe.name" />
|
||||
<input class="form-control" id="id_name" v-model="recipe.name"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pt-2">
|
||||
@ -19,14 +19,16 @@
|
||||
<label for="id_description">
|
||||
{{ $t("Description") }}
|
||||
</label>
|
||||
<textarea id="id_description" class="form-control" v-model="recipe.description" maxlength="512"></textarea>
|
||||
<textarea id="id_description" class="form-control" v-model="recipe.description"
|
||||
maxlength="512"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image and misc properties -->
|
||||
<div class="row pt-2">
|
||||
<div class="col-md-6" style="max-height: 50vh; min-height: 30vh">
|
||||
<input id="id_file_upload" ref="file_upload" type="file" hidden @change="uploadImage($event.target.files[0])" />
|
||||
<input id="id_file_upload" ref="file_upload" type="file" hidden
|
||||
@change="uploadImage($event.target.files[0])"/>
|
||||
|
||||
<div
|
||||
class="h-100 w-100 border border-primary rounded"
|
||||
@ -35,26 +37,31 @@
|
||||
@dragover.prevent
|
||||
@click="$refs.file_upload.click()"
|
||||
>
|
||||
<i class="far fa-image fa-10x text-primary" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%)" v-if="!recipe.image"></i>
|
||||
<i class="far fa-image fa-10x text-primary"
|
||||
style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%)"
|
||||
v-if="!recipe.image"></i>
|
||||
|
||||
<img :src="recipe.image" id="id_image" class="img img-fluid img-responsive" style="object-fit: cover; height: 100%" v-if="recipe.image" />
|
||||
<img :src="recipe.image" id="id_image" class="img img-fluid img-responsive"
|
||||
style="object-fit: cover; height: 100%" v-if="recipe.image"/>
|
||||
</div>
|
||||
<button style="bottom: 10px; left: 30px; position: absolute" class="btn btn-danger" @click="deleteImage" v-if="recipe.image">{{ $t("Delete") }}</button>
|
||||
<button style="bottom: 10px; left: 30px; position: absolute" class="btn btn-danger"
|
||||
@click="deleteImage" v-if="recipe.image">{{ $t("Delete") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mt-1">
|
||||
<label for="id_name"> {{ $t("Preparation") }} {{ $t("Time") }} ({{ $t("min") }})</label>
|
||||
<input class="form-control" id="id_prep_time" v-model="recipe.working_time" type="number" />
|
||||
<br />
|
||||
<input class="form-control" id="id_prep_time" v-model="recipe.working_time" type="number"/>
|
||||
<br/>
|
||||
<label for="id_name"> {{ $t("Waiting") }} {{ $t("Time") }} ({{ $t("min") }})</label>
|
||||
<input class="form-control" id="id_wait_time" v-model="recipe.waiting_time" type="number" />
|
||||
<br />
|
||||
<input class="form-control" id="id_wait_time" v-model="recipe.waiting_time" type="number"/>
|
||||
<br/>
|
||||
<label for="id_name"> {{ $t("Servings") }}</label>
|
||||
<input class="form-control" id="id_servings" v-model="recipe.servings" type="number" />
|
||||
<br />
|
||||
<input class="form-control" id="id_servings" v-model="recipe.servings" type="number"/>
|
||||
<br/>
|
||||
<label for="id_name"> {{ $t("Servings") }} {{ $t("Text") }}</label>
|
||||
<input class="form-control" id="id_servings_text" v-model="recipe.servings_text" maxlength="32" />
|
||||
<br />
|
||||
<input class="form-control" id="id_servings_text" v-model="recipe.servings_text" maxlength="32"/>
|
||||
<br/>
|
||||
<label for="id_name"> {{ $t("Keywords") }}</label>
|
||||
<multiselect
|
||||
v-model="recipe.keywords"
|
||||
@ -122,26 +129,31 @@
|
||||
<div class="card-body" v-if="recipe.nutrition !== null">
|
||||
<b-alert show>
|
||||
There is currently only very basic support for tracking nutritional information. A
|
||||
<a href="https://github.com/vabene1111/recipes/issues/896" target="_blank" rel="noreferrer nofollow">big update</a> is planned to improve on this in many different areas.
|
||||
<a href="https://github.com/vabene1111/recipes/issues/896" target="_blank"
|
||||
rel="noreferrer nofollow">big update</a> is planned to improve on this in many
|
||||
different areas.
|
||||
</b-alert>
|
||||
|
||||
<label for="id_name"> {{ $t(energy()) }}</label>
|
||||
|
||||
<input class="form-control" id="id_calories" v-model="recipe.nutrition.calories" />
|
||||
<input class="form-control" id="id_calories" v-model="recipe.nutrition.calories"/>
|
||||
|
||||
<label for="id_name"> {{ $t("Carbohydrates") }}</label>
|
||||
<input class="form-control" id="id_carbohydrates" v-model="recipe.nutrition.carbohydrates" />
|
||||
<input class="form-control" id="id_carbohydrates"
|
||||
v-model="recipe.nutrition.carbohydrates"/>
|
||||
|
||||
<label for="id_name"> {{ $t("Fats") }}</label>
|
||||
<input class="form-control" id="id_fats" v-model="recipe.nutrition.fats" />
|
||||
<input class="form-control" id="id_fats" v-model="recipe.nutrition.fats"/>
|
||||
|
||||
<label for="id_name"> {{ $t("Proteins") }}</label>
|
||||
<input class="form-control" id="id_proteins" v-model="recipe.nutrition.proteins" />
|
||||
<input class="form-control" id="id_proteins" v-model="recipe.nutrition.proteins"/>
|
||||
</div>
|
||||
</b-collapse>
|
||||
</div>
|
||||
<b-card-header header-tag="header" class="p-1" role="tab">
|
||||
<b-button squared block v-b-toggle.additional_collapse class="text-left" variant="outline-primary">{{ $t("additional_options") }}</b-button>
|
||||
<b-button squared block v-b-toggle.additional_collapse class="text-left"
|
||||
variant="outline-primary">{{ $t("additional_options") }}
|
||||
</b-button>
|
||||
</b-card-header>
|
||||
<b-collapse id="additional_collapse" class="mt-2" v-model="additional_visible">
|
||||
<b-form-group>
|
||||
@ -150,8 +162,12 @@
|
||||
<b-input-group-text squared>
|
||||
<b-form-checkbox v-model="recipe.create_food"></b-form-checkbox>
|
||||
</b-input-group-text>
|
||||
<b-input-group-text squared v-if="recipe.create_food"> {{ $t("Name") }}</b-input-group-text>
|
||||
<b-form-input squared v-if="recipe.create_food" v-model="recipe.food_name" id="food_name"></b-form-input>
|
||||
<b-input-group-text squared v-if="recipe.create_food"> {{
|
||||
$t("Name")
|
||||
}}
|
||||
</b-input-group-text>
|
||||
<b-form-input squared v-if="recipe.create_food" v-model="recipe.food_name"
|
||||
id="food_name"></b-form-input>
|
||||
</b-input-group-append>
|
||||
<em class="small text-muted">
|
||||
{{ $t("create_food_desc") }}
|
||||
@ -162,7 +178,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Steps -->
|
||||
<draggable :list="recipe.steps" group="steps" :empty-insert-threshold="10" handle=".handle" @sort="sortSteps()">
|
||||
<draggable :list="recipe.steps" group="steps" :empty-insert-threshold="10" handle=".handle"
|
||||
@sort="sortSteps()">
|
||||
<div v-for="(step, step_index) in recipe.steps" v-bind:key="step_index">
|
||||
<div class="card mt-2 mb-2">
|
||||
<div class="card-body pr-2 pl-2 pr-md-5 pl-md-5" :id="`id_card_step_${step_index}`">
|
||||
@ -176,26 +193,33 @@
|
||||
</h4>
|
||||
</div>
|
||||
<div class="col-1" style="text-align: right">
|
||||
<a class="btn shadow-none btn-lg" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<a class="btn shadow-none btn-lg" href="#" role="button" id="dropdownMenuLink"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fas fa-ellipsis-v text-muted"></i>
|
||||
</a>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuLink">
|
||||
<button class="dropdown-item" @click="removeStep(step)"><i class="fa fa-trash fa-fw"></i> {{ $t("Delete") }}</button>
|
||||
<button class="dropdown-item" @click="removeStep(step)"><i
|
||||
class="fa fa-trash fa-fw"></i> {{ $t("Delete") }}
|
||||
</button>
|
||||
|
||||
<button type="button" class="dropdown-item" v-if="!step.show_as_header" @click="step.show_as_header = true">
|
||||
<button type="button" class="dropdown-item" v-if="!step.show_as_header"
|
||||
@click="step.show_as_header = true">
|
||||
<i class="fas fa-eye fa-fw"></i> {{ $t("Show_as_header") }}
|
||||
</button>
|
||||
|
||||
<button type="button" class="dropdown-item" v-if="step.show_as_header" @click="step.show_as_header = false">
|
||||
<button type="button" class="dropdown-item" v-if="step.show_as_header"
|
||||
@click="step.show_as_header = false">
|
||||
<i class="fas fa-eye-slash fa-fw"></i> {{ $t("Hide_as_header") }}
|
||||
</button>
|
||||
|
||||
<button class="dropdown-item" @click="moveStep(step, step_index - 1)" v-if="step_index > 0">
|
||||
<button class="dropdown-item" @click="moveStep(step, step_index - 1)"
|
||||
v-if="step_index > 0">
|
||||
<i class="fa fa-arrow-up fa-fw"></i>
|
||||
{{ $t("Move_Up") }}
|
||||
</button>
|
||||
<button class="dropdown-item" @click="moveStep(step, step_index + 1)" v-if="step_index !== recipe.steps.length - 1">
|
||||
<button class="dropdown-item" @click="moveStep(step, step_index + 1)"
|
||||
v-if="step_index !== recipe.steps.length - 1">
|
||||
<i class="fa fa-arrow-down fa-fw"></i> {{ $t("Move_Down") }}
|
||||
</button>
|
||||
</div>
|
||||
@ -206,30 +230,36 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<label :for="'id_step_' + step.id + 'name'">{{ $t("Step_Name") }}</label>
|
||||
<input class="form-control" v-model="step.name" :id="'id_step_' + step.id + 'name'" />
|
||||
<input class="form-control" v-model="step.name"
|
||||
:id="'id_step_' + step.id + 'name'"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- step data visibility controller -->
|
||||
<div class="row pt-2">
|
||||
<div class="col col-md-12">
|
||||
<b-button pill variant="primary" size="sm" class="ml-1" @click="step.time_visible = true" v-if="!step.time_visible">
|
||||
<b-button pill variant="primary" size="sm" class="ml-1"
|
||||
@click="step.time_visible = true" v-if="!step.time_visible">
|
||||
<i class="fas fa-plus-circle"></i> {{ $t("Time") }}
|
||||
</b-button>
|
||||
|
||||
<b-button pill variant="primary" size="sm" class="ml-1" @click="step.ingredients_visible = true" v-if="!step.ingredients_visible">
|
||||
<b-button pill variant="primary" size="sm" class="ml-1"
|
||||
@click="step.ingredients_visible = true" v-if="!step.ingredients_visible">
|
||||
<i class="fas fa-plus-circle"></i> {{ $t("Ingredients") }}
|
||||
</b-button>
|
||||
|
||||
<b-button pill variant="primary" size="sm" class="ml-1" @click="step.instruction_visible = true" v-if="!step.instruction_visible">
|
||||
<b-button pill variant="primary" size="sm" class="ml-1"
|
||||
@click="step.instruction_visible = true" v-if="!step.instruction_visible">
|
||||
<i class="fas fa-plus-circle"></i> {{ $t("Instructions") }}
|
||||
</b-button>
|
||||
|
||||
<b-button pill variant="primary" size="sm" class="ml-1" @click="step.step_recipe_visible = true" v-if="!step.step_recipe_visible">
|
||||
<b-button pill variant="primary" size="sm" class="ml-1"
|
||||
@click="step.step_recipe_visible = true" v-if="!step.step_recipe_visible">
|
||||
<i class="fas fa-plus-circle"></i> {{ $t("Recipe") }}
|
||||
</b-button>
|
||||
|
||||
<b-button pill variant="primary" size="sm" class="ml-1" @click="step.file_visible = true" v-if="!step.file_visible">
|
||||
<b-button pill variant="primary" size="sm" class="ml-1"
|
||||
@click="step.file_visible = true" v-if="!step.file_visible">
|
||||
<i class="fas fa-plus-circle"></i> {{ $t("File") }}
|
||||
</b-button>
|
||||
<b-button
|
||||
@ -250,7 +280,8 @@
|
||||
<div class="row pt-2" v-if="step.time_visible">
|
||||
<div class="col-md-12">
|
||||
<label :for="'id_step_' + step.id + '_time'">{{ $t("step_time_minutes") }}</label>
|
||||
<input class="form-control" v-model="step.time" :id="'id_step_' + step.id + '_time'" />
|
||||
<input class="form-control" v-model="step.time"
|
||||
:id="'id_step_' + step.id + '_time'"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -333,18 +364,23 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 pr-0 pl-0 pr-md-2 pl-md-2 mt-2">
|
||||
<draggable :list="step.ingredients" group="ingredients" :empty-insert-threshold="10" handle=".handle" @sort="sortIngredients(step)">
|
||||
<div v-for="(ingredient, index) in step.ingredients" :key="ingredient.id">
|
||||
<hr class="d-md-none" />
|
||||
<!-- TODO improve rendering/add switch to toggle on/off -->
|
||||
<div class="small text-muted" v-if="ingredient.original_text">{{ingredient.original_text}}</div>
|
||||
<draggable :list="step.ingredients" group="ingredients"
|
||||
:empty-insert-threshold="10" handle=".handle"
|
||||
@sort="sortIngredients(step)">
|
||||
<div v-for="(ingredient, index) in step.ingredients"
|
||||
:key="ingredient.id">
|
||||
<hr class="d-md-none"/>
|
||||
<div class="d-flex">
|
||||
<div class="flex-grow-0 handle align-self-start">
|
||||
<button type="button" class="btn btn-lg shadow-none pr-0 pl-1 pr-md-2 pl-md-2"><i class="fas fa-arrows-alt-v"></i></button>
|
||||
<button type="button"
|
||||
class="btn btn-lg shadow-none pr-0 pl-1 pr-md-2 pl-md-2">
|
||||
<i class="fas fa-arrows-alt-v"></i></button>
|
||||
</div>
|
||||
|
||||
<div class="flex-fill row" style="margin-left: 4px; margin-right: 4px">
|
||||
<div class="col-lg-2 col-md-6 small-padding" v-if="!ingredient.is_header">
|
||||
<div class="flex-fill row"
|
||||
style="margin-left: 4px; margin-right: 4px">
|
||||
<div class="col-lg-2 col-md-6 small-padding"
|
||||
v-if="!ingredient.is_header">
|
||||
<input
|
||||
class="form-control"
|
||||
v-model="ingredient.amount"
|
||||
@ -355,7 +391,8 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-2 col-md-6 small-padding" v-if="!ingredient.is_header">
|
||||
<div class="col-lg-2 col-md-6 small-padding"
|
||||
v-if="!ingredient.is_header">
|
||||
<!-- search set to false to allow API to drive results & order -->
|
||||
<multiselect
|
||||
v-if="!ingredient.no_amount"
|
||||
@ -382,10 +419,14 @@
|
||||
:loading="units_loading"
|
||||
@search-change="searchUnits"
|
||||
>
|
||||
<template v-slot:noOptions>{{ $t("empty_list") }}</template>
|
||||
<template v-slot:noOptions>{{
|
||||
$t("empty_list")
|
||||
}}
|
||||
</template>
|
||||
</multiselect>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6 small-padding" v-if="!ingredient.is_header">
|
||||
<div class="col-lg-4 col-md-6 small-padding"
|
||||
v-if="!ingredient.is_header">
|
||||
<!-- search set to false to allow API to drive results & order -->
|
||||
|
||||
<multiselect
|
||||
@ -412,10 +453,14 @@
|
||||
:loading="foods_loading"
|
||||
@search-change="searchFoods"
|
||||
>
|
||||
<template v-slot:noOptions>{{ $t("empty_list") }}</template>
|
||||
<template v-slot:noOptions>{{
|
||||
$t("empty_list")
|
||||
}}
|
||||
</template>
|
||||
</multiselect>
|
||||
</div>
|
||||
<div class="small-padding" v-bind:class="{ 'col-lg-4 col-md-6': !ingredient.is_header, 'col-lg-12 col-md-12': ingredient.is_header }">
|
||||
<div class="small-padding"
|
||||
v-bind:class="{ 'col-lg-4 col-md-6': !ingredient.is_header, 'col-lg-12 col-md-12': ingredient.is_header }">
|
||||
<input
|
||||
class="form-control"
|
||||
maxlength="256"
|
||||
@ -446,32 +491,43 @@
|
||||
<i class="fas fa-ellipsis-v text-muted"></i>
|
||||
</a>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuLink2">
|
||||
<button type="button" class="dropdown-item" @click="removeIngredient(step, ingredient)">
|
||||
<div class="dropdown-menu dropdown-menu-right"
|
||||
aria-labelledby="dropdownMenuLink2">
|
||||
<button type="button" class="dropdown-item"
|
||||
@click="removeIngredient(step, ingredient)">
|
||||
<i class="fa fa-trash fa-fw"></i>
|
||||
{{ $t("Delete") }}
|
||||
</button>
|
||||
|
||||
<button type="button" class="dropdown-item" v-if="!ingredient.is_header" @click="ingredient.is_header = true">
|
||||
<button type="button" class="dropdown-item"
|
||||
v-if="!ingredient.is_header"
|
||||
@click="ingredient.is_header = true">
|
||||
<i class="fas fa-heading fa-fw"></i>
|
||||
{{ $t("Make_Header") }}
|
||||
</button>
|
||||
|
||||
<button type="button" class="dropdown-item" v-if="ingredient.is_header" @click="ingredient.is_header = false">
|
||||
<button type="button" class="dropdown-item"
|
||||
v-if="ingredient.is_header"
|
||||
@click="ingredient.is_header = false">
|
||||
<i class="fas fa-leaf fa-fw"></i>
|
||||
{{ $t("Make_Ingredient") }}
|
||||
</button>
|
||||
|
||||
<button type="button" class="dropdown-item" v-if="!ingredient.no_amount" @click="ingredient.no_amount = true">
|
||||
<button type="button" class="dropdown-item"
|
||||
v-if="!ingredient.no_amount"
|
||||
@click="ingredient.no_amount = true">
|
||||
<i class="fas fa-balance-scale-right fa-fw"></i>
|
||||
{{ $t("Disable_Amount") }}
|
||||
</button>
|
||||
|
||||
<button type="button" class="dropdown-item" v-if="ingredient.no_amount" @click="ingredient.no_amount = false">
|
||||
<button type="button" class="dropdown-item"
|
||||
v-if="ingredient.no_amount"
|
||||
@click="ingredient.no_amount = false">
|
||||
<i class="fas fa-balance-scale-right fa-fw"></i>
|
||||
{{ $t("Enable_Amount") }}
|
||||
</button>
|
||||
<button type="button" class="dropdown-item" @click="copyTemplateReference(index, ingredient)">
|
||||
<button type="button" class="dropdown-item"
|
||||
@click="copyTemplateReference(index, ingredient)">
|
||||
<i class="fas fa-code"></i>
|
||||
{{ $t("Copy_template_reference") }}
|
||||
</button>
|
||||
@ -483,8 +539,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-2 offset-md-5" style="text-align: center; margin-top: 8px">
|
||||
<button class="btn btn-success btn-block" @click="addIngredient(step)"><i class="fa fa-plus"></i></button>
|
||||
<div class="col-md-2 offset-md-5"
|
||||
style="text-align: center; margin-top: 8px">
|
||||
<button class="btn btn-success btn-block" @click="addIngredient(step)">
|
||||
<i class="fa fa-plus"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -517,24 +575,39 @@
|
||||
{{ $t("Add_Step") }}
|
||||
</button>
|
||||
|
||||
<button type="button" v-b-modal:id_modal_sort class="btn btn-warning shadow-none"><i class="fas fa-sort-amount-down-alt fa-lg"></i></button>
|
||||
<button type="button" v-b-modal:id_modal_sort class="btn btn-warning shadow-none"><i
|
||||
class="fas fa-sort-amount-down-alt fa-lg"></i></button>
|
||||
</b-button-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</draggable>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<div class="row" v-if="recipe.steps.length === 0">
|
||||
<div class="col col-md-12 text-center">
|
||||
<b-button-group>
|
||||
<button type="button" @click="addStep(0)" class="btn btn-success shadow-none">
|
||||
{{ $t("Add_Step") }}
|
||||
</button>
|
||||
</b-button-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<!-- bottom buttons save/close/view -->
|
||||
<div class="row fixed-bottom p-2 b-2 border-top text-center" style="background: white" v-if="recipe !== undefined">
|
||||
<div class="row fixed-bottom p-2 b-2 border-top text-center" style="background: white"
|
||||
v-if="recipe !== undefined">
|
||||
<div class="col-md-3 col-6">
|
||||
<a :href="resolveDjangoUrl('delete_recipe', recipe.id)" class="btn btn-block btn-danger shadow-none">{{ $t("Delete") }}</a>
|
||||
<a :href="resolveDjangoUrl('delete_recipe', recipe.id)"
|
||||
class="btn btn-block btn-danger shadow-none">{{ $t("Delete") }}</a>
|
||||
</div>
|
||||
<div class="col-md-3 col-6">
|
||||
<a :href="resolveDjangoUrl('view_recipe', recipe.id)">
|
||||
@ -542,12 +615,15 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3 col-6">
|
||||
<button type="button" @click="updateRecipe(false)" v-b-tooltip.hover :title="`${$t('Key_Ctrl')} + S`" class="btn btn-sm btn-block btn-info shadow-none">
|
||||
<button type="button" @click="updateRecipe(false)" v-b-tooltip.hover
|
||||
:title="`${$t('Key_Ctrl')} + S`" class="btn btn-sm btn-block btn-info shadow-none">
|
||||
{{ $t("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-3 col-6">
|
||||
<button type="button" @click="updateRecipe(true)" v-b-tooltip.hover :title="`${$t('Key_Ctrl')} + ${$t('Key_Shift')} + S`" class="btn btn-sm btn-block btn-success shadow-none">
|
||||
<button type="button" @click="updateRecipe(true)" v-b-tooltip.hover
|
||||
:title="`${$t('Key_Ctrl')} + ${$t('Key_Shift')} + S`"
|
||||
class="btn btn-sm btn-block btn-success shadow-none">
|
||||
{{ $t("Save_and_View") }}
|
||||
</button>
|
||||
</div>
|
||||
@ -555,9 +631,11 @@
|
||||
|
||||
<!-- modal for sorting steps -->
|
||||
<b-modal id="id_modal_sort" v-bind:title="$t('Sort')" ok-only>
|
||||
<draggable :list="recipe.steps" group="step_sorter" :empty-insert-threshold="10" handle=".handle" @sort="sortSteps()" class="list-group" tag="ul">
|
||||
<draggable :list="recipe.steps" group="step_sorter" :empty-insert-threshold="10" handle=".handle"
|
||||
@sort="sortSteps()" class="list-group" tag="ul">
|
||||
<li class="list-group-item" v-for="(step, step_index) in recipe.steps" v-bind:key="step_index">
|
||||
<button type="button" class="btn btn-lg shadow-none handle"><i class="fas fa-arrows-alt-v"></i></button>
|
||||
<button type="button" class="btn btn-lg shadow-none handle"><i class="fas fa-arrows-alt-v"></i>
|
||||
</button>
|
||||
<template v-if="step.name !== ''">{{ step.name }}</template>
|
||||
<template v-else>{{ $t("Step") }} {{ step_index + 1 }}</template>
|
||||
</li>
|
||||
@ -572,25 +650,34 @@
|
||||
@cancel="paste_ingredients = paste_step = undefined"
|
||||
@close="paste_ingredients = paste_step = undefined"
|
||||
>
|
||||
<b-form-textarea id="paste_ingredients" v-model="paste_ingredients" :placeholder="$t('paste_ingredients_placeholder')" rows="10"></b-form-textarea>
|
||||
<b-form-textarea id="paste_ingredients" v-model="paste_ingredients"
|
||||
:placeholder="$t('paste_ingredients_placeholder')" rows="10"></b-form-textarea>
|
||||
</b-modal>
|
||||
|
||||
<!-- form to create files on the fly -->
|
||||
<generic-modal-form :model="Models.USERFILE" :action="Actions.CREATE" :show="show_file_create" @finish-action="fileCreated" />
|
||||
<generic-modal-form :model="Models.USERFILE" :action="Actions.CREATE" :show="show_file_create"
|
||||
@finish-action="fileCreated"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue"
|
||||
import { BootstrapVue } from "bootstrap-vue"
|
||||
import {BootstrapVue} from "bootstrap-vue"
|
||||
|
||||
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||
|
||||
import draggable from "vuedraggable"
|
||||
import { ApiMixin, resolveDjangoUrl, ResolveUrlMixin, StandardToasts, convertEnergyToCalories, energyHeading } from "@/utils/utils"
|
||||
import {
|
||||
ApiMixin,
|
||||
resolveDjangoUrl,
|
||||
ResolveUrlMixin,
|
||||
StandardToasts,
|
||||
convertEnergyToCalories,
|
||||
energyHeading
|
||||
} from "@/utils/utils"
|
||||
import Multiselect from "vue-multiselect"
|
||||
import { ApiApiFactory } from "@/utils/openapi/api"
|
||||
import {ApiApiFactory} from "@/utils/openapi/api"
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
|
||||
import VueMarkdownEditor from "@kangc/v-md-editor"
|
||||
@ -615,7 +702,7 @@ Vue.use(BootstrapVue)
|
||||
export default {
|
||||
name: "RecipeEditView",
|
||||
mixins: [ResolveUrlMixin, ApiMixin],
|
||||
components: { Multiselect, LoadingSpinner, draggable, GenericModalForm },
|
||||
components: {Multiselect, LoadingSpinner, draggable, GenericModalForm},
|
||||
data() {
|
||||
return {
|
||||
recipe_id: window.RECIPE_ID,
|
||||
@ -763,7 +850,10 @@ export default {
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
|
||||
this.recipe_changed = false
|
||||
if (this.create_food) {
|
||||
apiFactory.createFood({ name: this.recipe.food_name, recipe: { id: this.recipe.id, name: this.recipe.name } })
|
||||
apiFactory.createFood({
|
||||
name: this.recipe.food_name,
|
||||
recipe: {id: this.recipe.id, name: this.recipe.name}
|
||||
})
|
||||
}
|
||||
if (view_after) {
|
||||
location.href = resolveDjangoUrl("view_recipe", this.recipe_id)
|
||||
@ -852,12 +942,12 @@ export default {
|
||||
this.$nextTick(() => document.getElementById(`amount_${this.recipe.steps.indexOf(step)}_${step.ingredients.length - 1}`).select())
|
||||
},
|
||||
removeIngredient: function (step, ingredient) {
|
||||
if (confirm(this.$t("confirm_delete", { object: this.$t("Ingredient") }))) {
|
||||
if (confirm(this.$t("confirm_delete", {object: this.$t("Ingredient")}))) {
|
||||
step.ingredients = step.ingredients.filter((item) => item !== ingredient)
|
||||
}
|
||||
},
|
||||
removeStep: function (step) {
|
||||
if (confirm(this.$t("confirm_delete", { object: this.$t("Step") }))) {
|
||||
if (confirm(this.$t("confirm_delete", {object: this.$t("Step")}))) {
|
||||
this.recipe.steps = this.recipe.steps.filter((item) => item !== step)
|
||||
}
|
||||
},
|
||||
@ -870,7 +960,7 @@ export default {
|
||||
let [tmp, step, id] = index.split("_")
|
||||
|
||||
let new_food = this.recipe.steps[step].ingredients[id]
|
||||
new_food.food = { name: tag }
|
||||
new_food.food = {name: tag}
|
||||
this.foods.push(new_food.food)
|
||||
this.recipe.steps[step].ingredients[id] = new_food
|
||||
},
|
||||
@ -878,12 +968,12 @@ export default {
|
||||
let [tmp, step, id] = index.split("_")
|
||||
|
||||
let new_unit = this.recipe.steps[step].ingredients[id]
|
||||
new_unit.unit = { name: tag }
|
||||
new_unit.unit = {name: tag}
|
||||
this.units.push(new_unit.unit)
|
||||
this.recipe.steps[step].ingredients[id] = new_unit
|
||||
},
|
||||
addKeyword: function (tag) {
|
||||
let new_keyword = { label: tag, name: tag }
|
||||
let new_keyword = {label: tag, name: tag}
|
||||
this.recipe.keywords.push(new_keyword)
|
||||
},
|
||||
searchKeywords: function (query) {
|
||||
@ -906,7 +996,7 @@ export default {
|
||||
|
||||
this.files_loading = true
|
||||
apiFactory
|
||||
.listUserFiles({ query: { query: query } })
|
||||
.listUserFiles({query: {query: query}})
|
||||
.then((response) => {
|
||||
this.files = response.data
|
||||
this.files_loading = false
|
||||
@ -918,7 +1008,7 @@ export default {
|
||||
},
|
||||
searchRecipes: function (query) {
|
||||
this.recipes_loading = true
|
||||
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, { query: query })
|
||||
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, {query: query})
|
||||
.then((result) => {
|
||||
this.recipes = result.data.results
|
||||
this.recipes_loading = false
|
||||
@ -984,10 +1074,15 @@ export default {
|
||||
this.show_file_create = false
|
||||
},
|
||||
scrollToStep: function (step_index) {
|
||||
document.getElementById("id_step_" + step_index).scrollIntoView({ behavior: "smooth" })
|
||||
document.getElementById("id_step_" + step_index).scrollIntoView({behavior: "smooth"})
|
||||
},
|
||||
addNutrition: function () {
|
||||
this.recipe.nutrition = {}
|
||||
this.recipe.nutrition = {
|
||||
carbohydrates: 0,
|
||||
fats: 0,
|
||||
proteins: 0,
|
||||
calories: 0,
|
||||
}
|
||||
},
|
||||
removeNutrition: function () {
|
||||
this.recipe.nutrition = null
|
||||
@ -1020,15 +1115,15 @@ export default {
|
||||
this.recipe.steps[step].ingredients_visible = true
|
||||
ing_list.forEach((ing) => {
|
||||
if (ing.trim() !== "") {
|
||||
this.genericPostAPI("api_ingredient_from_string", { text: ing }).then((result) => {
|
||||
this.genericPostAPI("api_ingredient_from_string", {text: ing}).then((result) => {
|
||||
let unit = null
|
||||
if (result.data.unit !== "") {
|
||||
unit = { name: result.data.unit }
|
||||
unit = {name: result.data.unit}
|
||||
}
|
||||
this.recipe.steps[step].ingredients.splice(order, 0, {
|
||||
amount: result.data.amount,
|
||||
unit: unit,
|
||||
food: { name: result.data.food },
|
||||
food: {name: result.data.food},
|
||||
note: result.data.note,
|
||||
original_text: ing,
|
||||
})
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div id="app" style="margin-bottom: 4vh">
|
||||
<RecipeSwitcher ref="ref_recipe_switcher" />
|
||||
<RecipeSwitcher ref="ref_recipe_switcher"/>
|
||||
<div class="row">
|
||||
<div class="col-12 col-xl-8 col-lg-10 offset-xl-2 offset-lg-1">
|
||||
<div class="row">
|
||||
@ -8,15 +8,21 @@
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-lg-10 col-xl-8 mt-3 mb-3">
|
||||
<b-input-group>
|
||||
<b-input class="form-control form-control-lg form-control-borderless form-control-search" v-model="search.search_input" v-bind:placeholder="$t('Search')"></b-input>
|
||||
<b-input
|
||||
class="form-control form-control-lg form-control-borderless form-control-search"
|
||||
v-model="search.search_input" v-bind:placeholder="$t('Search')"></b-input>
|
||||
<b-input-group-append>
|
||||
<b-button v-b-tooltip.hover :title="$t('show_sql')" @click="showSQL()" v-if="debug && ui.sql_debug">
|
||||
<b-button v-b-tooltip.hover :title="$t('show_sql')" @click="showSQL()"
|
||||
v-if="debug && ui.sql_debug">
|
||||
<i class="fas fa-bug" style="font-size: 1.5em"></i>
|
||||
</b-button>
|
||||
<b-button variant="light" v-b-tooltip.hover :title="$t('Random Recipes')" @click="openRandom()">
|
||||
<b-button variant="light" v-b-tooltip.hover :title="$t('Random Recipes')"
|
||||
@click="openRandom()">
|
||||
<i class="fas fa-dice-five" style="font-size: 1.5em"></i>
|
||||
</b-button>
|
||||
<b-button v-b-toggle.collapse_advanced_search v-b-tooltip.hover :title="$t('advanced_search_settings')" v-bind:variant="searchFiltered(true) ? 'danger' : 'primary'">
|
||||
<b-button v-b-toggle.collapse_advanced_search v-b-tooltip.hover
|
||||
:title="$t('advanced_search_settings')"
|
||||
v-bind:variant="searchFiltered(true) ? 'danger' : 'primary'">
|
||||
<!-- TODO consider changing this icon to a filter -->
|
||||
<i class="fas fa-caret-down" v-if="!search.advanced_search_visible"></i>
|
||||
<i class="fas fa-caret-up" v-if="search.advanced_search_visible"></i>
|
||||
@ -26,15 +32,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<b-collapse id="collapse_advanced_search" class="mt-2 shadow-sm" v-model="search.advanced_search_visible">
|
||||
<b-collapse id="collapse_advanced_search" class="mt-2 shadow-sm"
|
||||
v-model="search.advanced_search_visible">
|
||||
<div class="card">
|
||||
<div class="card-body p-4">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<a class="btn btn-primary btn-block text-uppercase" :href="resolveDjangoUrl('new_recipe')">{{ $t("New_Recipe") }}</a>
|
||||
<a class="btn btn-primary btn-block text-uppercase"
|
||||
:href="resolveDjangoUrl('new_recipe')">{{ $t("New_Recipe") }}</a>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<a class="btn btn-primary btn-block text-uppercase" :href="resolveDjangoUrl('data_import_url')">{{ $t("Import") }}</a>
|
||||
<a class="btn btn-primary btn-block text-uppercase"
|
||||
:href="resolveDjangoUrl('data_import_url')">{{ $t("Import") }}</a>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<button
|
||||
@ -53,99 +62,191 @@
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<button id="id_settings_button" class="btn btn-primary btn-block text-uppercase"><i class="fas fa-cog fa-lg m-1"></i></button>
|
||||
<button id="id_settings_button"
|
||||
class="btn btn-primary btn-block text-uppercase"><i
|
||||
class="fas fa-cog fa-lg m-1"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<b-popover target="id_settings_button" triggers="click" placement="bottom">
|
||||
<b-tabs content-class="mt-1 text-nowrap" small>
|
||||
<b-tab :title="$t('Settings')" active :title-link-class="['mx-0']">
|
||||
<b-form-group v-bind:label="$t('Recently_Viewed')" label-for="popover-input-1" label-cols="8" class="mb-1">
|
||||
<b-form-input type="number" v-model="ui.recently_viewed" id="popover-input-1" size="sm" class="mt-1"></b-form-input>
|
||||
<b-form-group v-bind:label="$t('Recently_Viewed')"
|
||||
label-for="popover-input-1" label-cols="8" class="mb-1">
|
||||
<b-form-input type="number" v-model="ui.recently_viewed"
|
||||
id="popover-input-1" size="sm"
|
||||
class="mt-1"></b-form-input>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group v-bind:label="$t('Recipes_per_page')" label-for="popover-input-page-count" label-cols="8" class="mb-1">
|
||||
<b-form-input type="number" v-model="ui.page_size" id="popover-input-page-count" size="sm" class="mt-1"></b-form-input>
|
||||
<b-form-group v-bind:label="$t('Recipes_per_page')"
|
||||
label-for="popover-input-page-count" label-cols="8"
|
||||
class="mb-1">
|
||||
<b-form-input type="number" v-model="ui.page_size"
|
||||
id="popover-input-page-count" size="sm"
|
||||
class="mt-1"></b-form-input>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group v-bind:label="$t('Meal_Plan')" label-for="popover-input-2" label-cols="8" class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_meal_plan" id="popover-input-2" size="sm" class="mt-2"></b-form-checkbox>
|
||||
<b-form-group v-bind:label="$t('Meal_Plan')" label-for="popover-input-2"
|
||||
label-cols="8" class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_meal_plan"
|
||||
id="popover-input-2" size="sm"
|
||||
class="mt-2"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group v-if="ui.show_meal_plan" v-bind:label="$t('Meal_Plan_Days')" label-for="popover-input-5" label-cols="8" class="mb-1">
|
||||
<b-form-input type="number" v-model="ui.meal_plan_days" id="popover-input-5" size="sm" class="mt-1"></b-form-input>
|
||||
<b-form-group v-if="ui.show_meal_plan"
|
||||
v-bind:label="$t('Meal_Plan_Days')"
|
||||
label-for="popover-input-5" label-cols="8" class="mb-1">
|
||||
<b-form-input type="number" v-model="ui.meal_plan_days"
|
||||
id="popover-input-5" size="sm"
|
||||
class="mt-1"></b-form-input>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group v-bind:label="$t('Sort_by_new')" label-for="popover-input-3" label-cols="8" class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.sort_by_new" id="popover-input-3" size="sm" class="mt-2"></b-form-checkbox>
|
||||
<b-form-group v-bind:label="$t('Sort_by_new')"
|
||||
label-for="popover-input-3" label-cols="8" class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.sort_by_new"
|
||||
id="popover-input-3" size="sm"
|
||||
class="mt-2"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
<div class="row" style="margin-top: 1vh">
|
||||
<div class="col-12">
|
||||
<a :href="resolveDjangoUrl('view_settings') + '#search'">{{ $t("Search Settings") }}</a>
|
||||
<a :href="resolveDjangoUrl('view_settings') + '#search'">{{
|
||||
$t("Search Settings")
|
||||
}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</b-tab>
|
||||
<b-tab :title="$t('fields')" :title-link-class="['mx-0']">
|
||||
<b-form-group v-bind:label="$t('show_keywords')" label-for="popover-show_keywords" label-cols="8" class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_keywords" id="popover-show_keywords" size="sm" class="mt-2"></b-form-checkbox>
|
||||
<b-form-group v-bind:label="$t('show_keywords')"
|
||||
label-for="popover-show_keywords" label-cols="8"
|
||||
class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_keywords"
|
||||
id="popover-show_keywords" size="sm"
|
||||
class="mt-2"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
<b-form-group v-bind:label="$t('show_foods')" label-for="popover-show_foods" label-cols="8" class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_foods" id="popover-show_foods" size="sm" class="mt-2"></b-form-checkbox>
|
||||
<b-form-group v-bind:label="$t('show_foods')"
|
||||
label-for="popover-show_foods" label-cols="8"
|
||||
class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_foods"
|
||||
id="popover-show_foods" size="sm"
|
||||
class="mt-2"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
<b-form-group v-bind:label="$t('show_books')" label-for="popover-input-show_books" label-cols="8" class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_books" id="popover-input-show_books" size="sm" class="mt-2"></b-form-checkbox>
|
||||
<b-form-group v-bind:label="$t('show_books')"
|
||||
label-for="popover-input-show_books" label-cols="8"
|
||||
class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_books"
|
||||
id="popover-input-show_books" size="sm"
|
||||
class="mt-2"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
<b-form-group v-bind:label="$t('show_rating')" label-for="popover-show_rating" label-cols="8" class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_rating" id="popover-show_rating" size="sm" class="mt-2"></b-form-checkbox>
|
||||
<b-form-group v-bind:label="$t('show_rating')"
|
||||
label-for="popover-show_rating" label-cols="8"
|
||||
class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_rating"
|
||||
id="popover-show_rating" size="sm"
|
||||
class="mt-2"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
<b-form-group v-bind:label="$t('show_units')" label-for="popover-show_units" label-cols="8" class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_units" id="popover-show_units" size="sm" class="mt-2"></b-form-checkbox>
|
||||
<b-form-group v-bind:label="$t('show_units')"
|
||||
label-for="popover-show_units" label-cols="8"
|
||||
class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_units"
|
||||
id="popover-show_units" size="sm"
|
||||
class="mt-2"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
<b-form-group v-bind:label="$t('show_filters')" label-for="popover-show_filters" label-cols="8" class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_filters" id="popover-show_filters" size="sm" class="mt-2"></b-form-checkbox>
|
||||
<b-form-group v-bind:label="$t('show_filters')"
|
||||
label-for="popover-show_filters" label-cols="8"
|
||||
class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_filters"
|
||||
id="popover-show_filters" size="sm"
|
||||
class="mt-2"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
<b-form-group v-bind:label="$t('show_sortby')" label-for="popover-show_sortby" label-cols="8" class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_sortby" id="popover-show_sortby" size="sm" class="mt-2"></b-form-checkbox>
|
||||
<b-form-group v-bind:label="$t('show_sortby')"
|
||||
label-for="popover-show_sortby" label-cols="8"
|
||||
class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_sortby"
|
||||
id="popover-show_sortby" size="sm"
|
||||
class="mt-2"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
<b-form-group v-bind:label="$t('times_cooked')" label-for="popover-show_timescooked" label-cols="8" class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_timescooked" id="popover-show_cooked" size="sm" class="mt-2"></b-form-checkbox>
|
||||
<b-form-group v-bind:label="$t('times_cooked')"
|
||||
label-for="popover-show_timescooked" label-cols="8"
|
||||
class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_timescooked"
|
||||
id="popover-show_cooked" size="sm"
|
||||
class="mt-2"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
<b-form-group v-bind:label="$t('make_now')" label-for="popover-show_makenow" label-cols="8" class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_makenow" id="popover-show_makenow" size="sm" class="mt-2"></b-form-checkbox>
|
||||
<b-form-group v-bind:label="$t('make_now')"
|
||||
label-for="popover-show_makenow" label-cols="8"
|
||||
class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_makenow"
|
||||
id="popover-show_makenow" size="sm"
|
||||
class="mt-2"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
<b-form-group v-bind:label="$t('last_cooked')" label-for="popover-show_cookedon" label-cols="8" class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_cookedon" id="popover-show_cookedon" size="sm" class="mt-2"></b-form-checkbox>
|
||||
<b-form-group v-bind:label="$t('last_cooked')"
|
||||
label-for="popover-show_cookedon" label-cols="8"
|
||||
class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_cookedon"
|
||||
id="popover-show_cookedon" size="sm"
|
||||
class="mt-2"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
<b-form-group v-bind:label="$t('last_viewed')" label-for="popover-show_viewedon" label-cols="8" class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_viewedon" id="popover-show_viewedon" size="sm" class="mt-2"></b-form-checkbox>
|
||||
<b-form-group v-bind:label="$t('last_viewed')"
|
||||
label-for="popover-show_viewedon" label-cols="8"
|
||||
class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_viewedon"
|
||||
id="popover-show_viewedon" size="sm"
|
||||
class="mt-2"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
<b-form-group v-bind:label="$t('created_on')" label-for="popover-show_createdon" label-cols="8" class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_createdon" id="popover-show_createdon" size="sm" class="mt-2"></b-form-checkbox>
|
||||
<b-form-group v-bind:label="$t('created_on')"
|
||||
label-for="popover-show_createdon" label-cols="8"
|
||||
class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_createdon"
|
||||
id="popover-show_createdon" size="sm"
|
||||
class="mt-2"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
<b-form-group v-bind:label="$t('updatedon')" label-for="popover-show_updatedon" label-cols="8" class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_updatedon" id="popover-show_updatedon" size="sm" class="mt-2"></b-form-checkbox>
|
||||
<b-form-group v-bind:label="$t('updatedon')"
|
||||
label-for="popover-show_updatedon" label-cols="8"
|
||||
class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_updatedon"
|
||||
id="popover-show_updatedon" size="sm"
|
||||
class="mt-2"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
</b-tab>
|
||||
|
||||
<b-tab :title="$t('advanced')" :title-link-class="['mx-0']">
|
||||
<b-form-group v-bind:label="$t('remember_search')" label-for="popover-rem-search" label-cols="8" class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.remember_search" id="popover-rem-search" size="sm" class="mt-2"></b-form-checkbox>
|
||||
<b-form-group v-bind:label="$t('remember_search')"
|
||||
label-for="popover-rem-search" label-cols="8"
|
||||
class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.remember_search"
|
||||
id="popover-rem-search" size="sm"
|
||||
class="mt-2"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
<b-form-group v-if="ui.remember_search" v-bind:label="$t('remember_hours')" label-for="popover-input-rem-hours" label-cols="8" class="mb-1">
|
||||
<b-form-input type="number" v-model="ui.remember_hours" id="popover-rem-hours" size="sm" class="mt-1"></b-form-input>
|
||||
<b-form-group v-if="ui.remember_search"
|
||||
v-bind:label="$t('remember_hours')"
|
||||
label-for="popover-input-rem-hours" label-cols="8"
|
||||
class="mb-1">
|
||||
<b-form-input type="number" v-model="ui.remember_hours"
|
||||
id="popover-rem-hours" size="sm"
|
||||
class="mt-1"></b-form-input>
|
||||
</b-form-group>
|
||||
<b-form-group v-bind:label="$t('tree_select')" label-for="popover-input-treeselect" label-cols="8" class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.tree_select" id="popover-input-treeselect" size="sm" class="mt-2"></b-form-checkbox>
|
||||
<b-form-group v-bind:label="$t('tree_select')"
|
||||
label-for="popover-input-treeselect" label-cols="8"
|
||||
class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.tree_select"
|
||||
id="popover-input-treeselect" size="sm"
|
||||
class="mt-2"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
<b-form-group v-if="debug" v-bind:label="$t('sql_debug')" label-for="popover-input-sqldebug" label-cols="8" class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.sql_debug" id="popover-input-sqldebug" size="sm" class="mt-2"></b-form-checkbox>
|
||||
<b-form-group v-if="debug" v-bind:label="$t('sql_debug')"
|
||||
label-for="popover-input-sqldebug" label-cols="8"
|
||||
class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.sql_debug"
|
||||
id="popover-input-sqldebug" size="sm"
|
||||
class="mt-2"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
</b-tab>
|
||||
</b-tabs>
|
||||
|
||||
<div class="row" style="margin-top: 1vh">
|
||||
<div class="col-12" style="text-align: right">
|
||||
<b-button size="sm" variant="secondary" style="margin-right: 8px" @click="$root.$emit('bv::hide::popover')">{{ $t("Close") }} </b-button>
|
||||
<b-button size="sm" variant="secondary" style="margin-right: 8px"
|
||||
@click="$root.$emit('bv::hide::popover')">{{ $t("Close") }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</b-popover>
|
||||
@ -185,18 +286,23 @@
|
||||
<h6 class="mb-0" v-if="ui.expert_mode && search.keywords_fields > 1">
|
||||
{{ $t("Keywords") }}
|
||||
</h6>
|
||||
<span class="text-sm-left text-warning" v-if="ui.expert_mode && search.keywords_fields > 1 && hasDuplicateFilter(search.search_keywords, search.keywords_fields)">{{
|
||||
$t("warning_duplicate_filter")
|
||||
}}</span>
|
||||
<span class="text-sm-left text-warning"
|
||||
v-if="ui.expert_mode && search.keywords_fields > 1 && hasDuplicateFilter(search.search_keywords, search.keywords_fields)">{{
|
||||
$t("warning_duplicate_filter")
|
||||
}}</span>
|
||||
<div class="row" v-if="ui.show_keywords">
|
||||
<div class="col-12">
|
||||
<b-input-group class="mt-2" v-for="(k, a) in keywordFields" :key="a">
|
||||
<template #prepend v-if="ui.expert_mode">
|
||||
<b-input-group-text style="width: 3em" @click="addField('keywords', k)">
|
||||
<i class="fas fa-plus-circle text-primary" v-if="k == search.keywords_fields && k < 4" />
|
||||
<b-input-group-text style="width: 3em"
|
||||
@click="addField('keywords', k)">
|
||||
<i class="fas fa-plus-circle text-primary"
|
||||
v-if="k == search.keywords_fields && k < 4"/>
|
||||
</b-input-group-text>
|
||||
<b-input-group-text style="width: 3em" @click="removeField('keywords', k)">
|
||||
<i class="fas fa-minus-circle text-primary" v-if="k == search.keywords_fields && k > 1" />
|
||||
<b-input-group-text style="width: 3em"
|
||||
@click="removeField('keywords', k)">
|
||||
<i class="fas fa-minus-circle text-primary"
|
||||
v-if="k == search.keywords_fields && k > 1"/>
|
||||
</b-input-group-text>
|
||||
</template>
|
||||
<treeselect
|
||||
@ -233,14 +339,20 @@
|
||||
switch
|
||||
style="width: 5em"
|
||||
>
|
||||
<span class="text-uppercase" v-if="search.search_keywords[a].operator">{{ $t("or") }}</span>
|
||||
<span class="text-uppercase"
|
||||
v-if="search.search_keywords[a].operator">{{
|
||||
$t("or")
|
||||
}}</span>
|
||||
<span class="text-uppercase" v-else>{{ $t("and") }}</span>
|
||||
</b-form-checkbox>
|
||||
</b-input-group-text>
|
||||
</b-input-group-append>
|
||||
<b-input-group-append v-if="ui.expert_mode">
|
||||
<b-input-group-text>
|
||||
<b-form-checkbox v-model="search.search_keywords[a].not" name="check-button" @change="refreshData(false)" class="shadow-none">
|
||||
<b-form-checkbox v-model="search.search_keywords[a].not"
|
||||
name="check-button"
|
||||
@change="refreshData(false)"
|
||||
class="shadow-none">
|
||||
<span class="text-uppercase">{{ $t("not") }}</span>
|
||||
</b-form-checkbox>
|
||||
</b-input-group-text>
|
||||
@ -253,18 +365,23 @@
|
||||
<h6 class="mt-2 mb-0" v-if="ui.expert_mode && search.foods_fields > 1">
|
||||
{{ $t("Foods") }}
|
||||
</h6>
|
||||
<span class="text-sm-left text-warning" v-if="ui.expert_mode && search.foods_fields > 1 && hasDuplicateFilter(search.search_foods, search.foods_fields)">{{
|
||||
$t("warning_duplicate_filter")
|
||||
}}</span>
|
||||
<span class="text-sm-left text-warning"
|
||||
v-if="ui.expert_mode && search.foods_fields > 1 && hasDuplicateFilter(search.search_foods, search.foods_fields)">{{
|
||||
$t("warning_duplicate_filter")
|
||||
}}</span>
|
||||
<div class="row" v-if="ui.show_foods">
|
||||
<div class="col-12">
|
||||
<b-input-group class="mt-2" v-for="(f, i) in foodFields" :key="i">
|
||||
<template #prepend v-if="ui.expert_mode">
|
||||
<b-input-group-text style="width: 3em" @click="addField('foods', f)">
|
||||
<i class="fas fa-plus-circle text-primary" v-if="f == search.foods_fields && f < 4" />
|
||||
<b-input-group-text style="width: 3em"
|
||||
@click="addField('foods', f)">
|
||||
<i class="fas fa-plus-circle text-primary"
|
||||
v-if="f == search.foods_fields && f < 4"/>
|
||||
</b-input-group-text>
|
||||
<b-input-group-text style="width: 3em" @click="removeField('foods', f)">
|
||||
<i class="fas fa-minus-circle text-primary" v-if="f == search.foods_fields && f > 1" />
|
||||
<b-input-group-text style="width: 3em"
|
||||
@click="removeField('foods', f)">
|
||||
<i class="fas fa-minus-circle text-primary"
|
||||
v-if="f == search.foods_fields && f > 1"/>
|
||||
</b-input-group-text>
|
||||
</template>
|
||||
<treeselect
|
||||
@ -293,15 +410,24 @@
|
||||
/>
|
||||
<b-input-group-append>
|
||||
<b-input-group-text>
|
||||
<b-form-checkbox v-model="search.search_foods[i].operator" name="check-button" @change="refreshData(false)" class="shadow-none" switch style="width: 5em">
|
||||
<span class="text-uppercase" v-if="search.search_foods[i].operator">{{ $t("or") }}</span>
|
||||
<b-form-checkbox v-model="search.search_foods[i].operator"
|
||||
name="check-button"
|
||||
@change="refreshData(false)"
|
||||
class="shadow-none" switch style="width: 5em">
|
||||
<span class="text-uppercase"
|
||||
v-if="search.search_foods[i].operator">{{
|
||||
$t("or")
|
||||
}}</span>
|
||||
<span class="text-uppercase" v-else>{{ $t("and") }}</span>
|
||||
</b-form-checkbox>
|
||||
</b-input-group-text>
|
||||
</b-input-group-append>
|
||||
<b-input-group-append v-if="ui.expert_mode">
|
||||
<b-input-group-text>
|
||||
<b-form-checkbox v-model="search.search_foods[i].not" name="check-button" @change="refreshData(false)" class="shadow-none">
|
||||
<b-form-checkbox v-model="search.search_foods[i].not"
|
||||
name="check-button"
|
||||
@change="refreshData(false)"
|
||||
class="shadow-none">
|
||||
<span class="text-uppercase">{{ $t("not") }}</span>
|
||||
</b-form-checkbox>
|
||||
</b-input-group-text>
|
||||
@ -314,18 +440,23 @@
|
||||
<h6 class="mt-2 mb-0" v-if="ui.expert_mode && search.books_fields > 1">
|
||||
{{ $t("Books") }}
|
||||
</h6>
|
||||
<span class="text-sm-left text-warning" v-if="ui.expert_mode && search.books_fields > 1 && hasDuplicateFilter(search.search_books, search.books_fields)">{{
|
||||
$t("warning_duplicate_filter")
|
||||
}}</span>
|
||||
<span class="text-sm-left text-warning"
|
||||
v-if="ui.expert_mode && search.books_fields > 1 && hasDuplicateFilter(search.search_books, search.books_fields)">{{
|
||||
$t("warning_duplicate_filter")
|
||||
}}</span>
|
||||
<div class="row" v-if="ui.show_books">
|
||||
<div class="col-12">
|
||||
<b-input-group class="mt-2" v-for="(b, i) in bookFields" :key="i">
|
||||
<template #prepend v-if="ui.expert_mode">
|
||||
<b-input-group-text style="width: 3em" @click="addField('books', b)">
|
||||
<i class="fas fa-plus-circle text-primary" v-if="b == search.books_fields && b < 4" />
|
||||
<b-input-group-text style="width: 3em"
|
||||
@click="addField('books', b)">
|
||||
<i class="fas fa-plus-circle text-primary"
|
||||
v-if="b == search.books_fields && b < 4"/>
|
||||
</b-input-group-text>
|
||||
<b-input-group-text style="width: 3em" @click="removeField('books', b)">
|
||||
<i class="fas fa-minus-circle text-primary" v-if="b == search.books_fields && b > 1" />
|
||||
<b-input-group-text style="width: 3em"
|
||||
@click="removeField('books', b)">
|
||||
<i class="fas fa-minus-circle text-primary"
|
||||
v-if="b == search.books_fields && b > 1"/>
|
||||
</b-input-group-text>
|
||||
</template>
|
||||
<generic-multiselect
|
||||
@ -339,15 +470,24 @@
|
||||
></generic-multiselect>
|
||||
<b-input-group-append>
|
||||
<b-input-group-text>
|
||||
<b-form-checkbox v-model="search.search_books[i].operator" name="check-button" @change="refreshData(false)" class="shadow-none" style="width: 5em" switch>
|
||||
<span class="text-uppercase" v-if="search.search_books[i].operator">{{ $t("or") }}</span>
|
||||
<b-form-checkbox v-model="search.search_books[i].operator"
|
||||
name="check-button"
|
||||
@change="refreshData(false)"
|
||||
class="shadow-none" style="width: 5em" switch>
|
||||
<span class="text-uppercase"
|
||||
v-if="search.search_books[i].operator">{{
|
||||
$t("or")
|
||||
}}</span>
|
||||
<span class="text-uppercase" v-else>{{ $t("and") }}</span>
|
||||
</b-form-checkbox>
|
||||
</b-input-group-text>
|
||||
</b-input-group-append>
|
||||
<b-input-group-append v-if="ui.expert_mode">
|
||||
<b-input-group-text>
|
||||
<b-form-checkbox v-model="search.search_books[i].not" name="check-button" @change="refreshData(false)" class="shadow-none">
|
||||
<b-form-checkbox v-model="search.search_books[i].not"
|
||||
name="check-button"
|
||||
@change="refreshData(false)"
|
||||
class="shadow-none">
|
||||
<span class="text-uppercase">{{ $t("not") }}</span>
|
||||
</b-form-checkbox>
|
||||
</b-input-group-text>
|
||||
@ -371,8 +511,12 @@
|
||||
/>
|
||||
<b-input-group-append>
|
||||
<b-input-group-text>
|
||||
<b-form-checkbox v-model="search.search_rating_gte" name="check-button" @change="refreshData(false)" class="shadow-none" switch style="width: 5em">
|
||||
<span class="text-uppercase" v-if="search.search_rating_gte">>=</span>
|
||||
<b-form-checkbox v-model="search.search_rating_gte"
|
||||
name="check-button"
|
||||
@change="refreshData(false)"
|
||||
class="shadow-none" switch style="width: 5em">
|
||||
<span class="text-uppercase"
|
||||
v-if="search.search_rating_gte">>=</span>
|
||||
<span class="text-uppercase" v-else><=</span>
|
||||
</b-form-checkbox>
|
||||
</b-input-group-text>
|
||||
@ -395,8 +539,13 @@
|
||||
></generic-multiselect>
|
||||
<b-input-group-append>
|
||||
<b-input-group-text>
|
||||
<b-form-checkbox v-model="search.search_units_or" name="check-button" @change="refreshData(false)" class="shadow-none" style="width: 4em" switch>
|
||||
<span class="text-uppercase" v-if="search.search_units_or">{{ $t("or") }}</span>
|
||||
<b-form-checkbox v-model="search.search_units_or"
|
||||
name="check-button"
|
||||
@change="refreshData(false)"
|
||||
class="shadow-none" style="width: 4em" switch>
|
||||
<span class="text-uppercase" v-if="search.search_units_or">{{
|
||||
$t("or")
|
||||
}}</span>
|
||||
<span class="text-uppercase" v-else>{{ $t("and") }}</span>
|
||||
</b-form-checkbox>
|
||||
</b-input-group-text>
|
||||
@ -406,17 +555,23 @@
|
||||
</div>
|
||||
|
||||
<!-- special switches -->
|
||||
<div class="row g-0" v-if="ui.show_timescooked || ui.show_makenow || ui.show_cookedon">
|
||||
<div class="row g-0"
|
||||
v-if="ui.show_timescooked || ui.show_makenow || ui.show_cookedon">
|
||||
<div class="col-12">
|
||||
<b-input-group class="mt-2">
|
||||
<!-- times cooked -->
|
||||
<b-input-group-prepend is-text v-if="ui.show_timescooked">
|
||||
{{ $t("times_cooked") }}
|
||||
</b-input-group-prepend>
|
||||
<b-form-input id="timescooked" type="number" min="0" v-model="search.timescooked" v-if="ui.show_timescooked"></b-form-input>
|
||||
<b-form-input id="timescooked" type="number" min="0"
|
||||
v-model="search.timescooked"
|
||||
v-if="ui.show_timescooked"></b-form-input>
|
||||
<b-input-group-append v-if="ui.show_timescooked">
|
||||
<b-input-group-text>
|
||||
<b-form-checkbox v-model="search.timescooked_gte" name="check-button" @change="refreshData(false)" class="shadow-none" switch style="width: 4em">
|
||||
<b-form-checkbox v-model="search.timescooked_gte"
|
||||
name="check-button"
|
||||
@change="refreshData(false)"
|
||||
class="shadow-none" switch style="width: 4em">
|
||||
<span class="text-uppercase" v-if="search.timescooked_gte">>=</span>
|
||||
<span class="text-uppercase" v-else><=</span>
|
||||
</b-form-checkbox>
|
||||
@ -435,7 +590,10 @@
|
||||
@input="refreshData(false)"
|
||||
/>
|
||||
<b-input-group-text>
|
||||
<b-form-checkbox v-model="search.cookedon_gte" name="check-button" @change="refreshData(false)" class="shadow-none" switch style="width: 4em">
|
||||
<b-form-checkbox v-model="search.cookedon_gte"
|
||||
name="check-button"
|
||||
@change="refreshData(false)"
|
||||
class="shadow-none" switch style="width: 4em">
|
||||
<span class="text-uppercase" v-if="search.cookedon_gte">>=</span>
|
||||
<span class="text-uppercase" v-else><=</span>
|
||||
</b-form-checkbox>
|
||||
@ -455,7 +613,10 @@
|
||||
@input="refreshData(false)"
|
||||
/>
|
||||
<b-input-group-text>
|
||||
<b-form-checkbox v-model="search.createdon_gte" name="check-button" @change="refreshData(false)" class="shadow-none" switch style="width: 4em">
|
||||
<b-form-checkbox v-model="search.createdon_gte"
|
||||
name="check-button"
|
||||
@change="refreshData(false)"
|
||||
class="shadow-none" switch style="width: 4em">
|
||||
<span class="text-uppercase" v-if="search.createdon_gte">>=</span>
|
||||
<span class="text-uppercase" v-else><=</span>
|
||||
</b-form-checkbox>
|
||||
@ -473,7 +634,10 @@
|
||||
@input="refreshData(false)"
|
||||
/>
|
||||
<b-input-group-text>
|
||||
<b-form-checkbox v-model="search.viewedon_gte" name="check-button" @change="refreshData(false)" class="shadow-none" switch style="width: 4em">
|
||||
<b-form-checkbox v-model="search.viewedon_gte"
|
||||
name="check-button"
|
||||
@change="refreshData(false)"
|
||||
class="shadow-none" switch style="width: 4em">
|
||||
<span class="text-uppercase" v-if="search.viewedon_gte">>=</span>
|
||||
<span class="text-uppercase" v-else><=</span>
|
||||
</b-form-checkbox>
|
||||
@ -491,7 +655,10 @@
|
||||
@input="refreshData(false)"
|
||||
/>
|
||||
<b-input-group-text>
|
||||
<b-form-checkbox v-model="search.updatedon_gte" name="check-button" @change="refreshData(false)" class="shadow-none" switch style="width: 4em">
|
||||
<b-form-checkbox v-model="search.updatedon_gte"
|
||||
name="check-button"
|
||||
@change="refreshData(false)"
|
||||
class="shadow-none" switch style="width: 4em">
|
||||
<span class="text-uppercase" v-if="search.updatedon_gte">>=</span>
|
||||
<span class="text-uppercase" v-else><=</span>
|
||||
</b-form-checkbox>
|
||||
@ -500,7 +667,9 @@
|
||||
<b-input-group-append v-if="ui.show_makenow">
|
||||
<b-input-group-text>
|
||||
{{ $t("make_now") }}
|
||||
<b-form-checkbox v-model="search.makenow" name="check-button" @change="refreshData(false)" class="shadow-none" switch style="width: 4em" />
|
||||
<b-form-checkbox v-model="search.makenow" name="check-button"
|
||||
@change="refreshData(false)"
|
||||
class="shadow-none" switch style="width: 4em"/>
|
||||
</b-input-group-text>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
@ -510,14 +679,16 @@
|
||||
<!-- Buttons -->
|
||||
<div class="row justify-content-end small">
|
||||
<div class="col-auto">
|
||||
<b-button class="my-0" variant="link" size="sm" @click="search.explain_visible = !search.explain_visible">
|
||||
<b-button class="my-0" variant="link" size="sm"
|
||||
@click="search.explain_visible = !search.explain_visible">
|
||||
<div v-if="!search.explain_visible">
|
||||
<i class="far fa-eye"></i>
|
||||
{{ $t("explain") }}
|
||||
</div>
|
||||
<div v-else><i class="far fa-eye-slash"></i> {{ $t("explain") }}</div>
|
||||
</b-button>
|
||||
<b-button class="my-0" variant="link" size="sm" @click="ui.expert_mode = !ui.expert_mode">
|
||||
<b-button class="my-0" variant="link" size="sm"
|
||||
@click="ui.expert_mode = !ui.expert_mode">
|
||||
<div v-if="!ui.expert_mode">
|
||||
<i class="fas fa-circle"></i>
|
||||
{{ $t("expert_mode") }}
|
||||
@ -540,20 +711,21 @@
|
||||
<!-- TODO find a way to localize this that works without explaining localization to each language translator -->
|
||||
Show all recipes that are matched
|
||||
<span v-if="search.search_input">
|
||||
by <i>{{ search.search_input }}</i> <br />
|
||||
by <i>{{ search.search_input }}</i> <br/>
|
||||
</span>
|
||||
<span v-else> without any search term <br /> </span>
|
||||
<span v-else> without any search term <br/> </span>
|
||||
|
||||
<span v-if="search.search_internal"> and are <span class="text-success">internal</span> <br /></span>
|
||||
<span v-if="search.search_internal"> and are <span class="text-success">internal</span> <br/></span>
|
||||
|
||||
<span v-for="k in search.search_keywords" v-bind:key="k.id">
|
||||
<template v-if="k.items.length > 0">
|
||||
and
|
||||
<b v-if="k.not">don't</b>
|
||||
contain
|
||||
<b v-if="k.operator">any</b><b v-else>all</b> of the following <span class="text-success">keywords</span>:
|
||||
<b v-if="k.operator">any</b><b
|
||||
v-else>all</b> of the following <span class="text-success">keywords</span>:
|
||||
<i>{{ k.items.flatMap((x) => x.name).join(", ") }}</i>
|
||||
<br />
|
||||
<br/>
|
||||
</template>
|
||||
</span>
|
||||
|
||||
@ -562,9 +734,11 @@
|
||||
and
|
||||
<b v-if="k.not">don't</b>
|
||||
contain
|
||||
<b v-if="k.operator">any</b><b v-else>all</b> of the following <span class="text-success">foods</span>:
|
||||
<b v-if="k.operator">any</b><b
|
||||
v-else>all</b> of the following <span
|
||||
class="text-success">foods</span>:
|
||||
<i>{{ k.items.flatMap((x) => x.name).join(", ") }}</i>
|
||||
<br />
|
||||
<br/>
|
||||
</template>
|
||||
</span>
|
||||
|
||||
@ -573,40 +747,48 @@
|
||||
and
|
||||
<b v-if="k.not">don't</b>
|
||||
contain
|
||||
<b v-if="k.operator">any</b><b v-else>all</b> of the following <span class="text-success">books</span>:
|
||||
<b v-if="k.operator">any</b><b
|
||||
v-else>all</b> of the following <span
|
||||
class="text-success">books</span>:
|
||||
<i>{{ k.items.flatMap((x) => x.name).join(", ") }}</i>
|
||||
<br />
|
||||
<br/>
|
||||
</template>
|
||||
</span>
|
||||
|
||||
<span v-if="search.makenow"> and you can <span class="text-success">make right now</span> (based on the on hand flag) <br /></span>
|
||||
<span v-if="search.makenow"> and you can <span class="text-success">make right now</span> (based on the on hand flag) <br/></span>
|
||||
|
||||
<span v-if="search.search_units.length > 0">
|
||||
and contain <b v-if="search.search_units_or">any</b><b v-else>all</b> of the following <span class="text-success">units</span>:
|
||||
and contain <b v-if="search.search_units_or">any</b><b
|
||||
v-else>all</b> of the following <span
|
||||
class="text-success">units</span>:
|
||||
<i>{{ search.search_units.flatMap((x) => x.name).join(", ") }}</i
|
||||
><br />
|
||||
><br/>
|
||||
</span>
|
||||
|
||||
<span v-if="search.search_rating !== undefined">
|
||||
and have a <span class="text-success">rating</span> <template v-if="search.search_rating_gte">greater than</template><template v-else> less than</template> or
|
||||
equal to {{ search.search_rating }}<br />
|
||||
and have a <span class="text-success">rating</span> <template
|
||||
v-if="search.search_rating_gte">greater than</template><template
|
||||
v-else> less than</template> or
|
||||
equal to {{ search.search_rating }}<br/>
|
||||
</span>
|
||||
|
||||
<span v-if="search.lastcooked !== undefined">
|
||||
and have been <span class="text-success">last cooked</span> <template v-if="search.lastcooked_gte"> after</template><template v-else> before</template>
|
||||
and have been <span class="text-success">last cooked</span> <template
|
||||
v-if="search.lastcooked_gte"> after</template><template v-else> before</template>
|
||||
<i>{{ search.lastcooked }}</i
|
||||
><br />
|
||||
><br/>
|
||||
</span>
|
||||
|
||||
<span v-if="search.timescooked !== undefined">
|
||||
and have <span class="text-success">been cooked</span> <template v-if="search.timescooked_gte"> at least</template><template v-else> less than</template> or
|
||||
equal to<i>{{ search.timescooked }}</i> times <br />
|
||||
and have <span class="text-success">been cooked</span> <template
|
||||
v-if="search.timescooked_gte"> at least</template><template v-else> less than</template> or
|
||||
equal to<i>{{ search.timescooked }}</i> times <br/>
|
||||
</span>
|
||||
|
||||
<span v-if="search.sort_order.length > 0">
|
||||
<span class="text-success">order</span> by
|
||||
<i>{{ search.sort_order.flatMap((x) => x.text).join(", ") }}</i>
|
||||
<br />
|
||||
<br/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -617,33 +799,37 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="recipes.length > 0">
|
||||
<div class="row align-content-center">
|
||||
<div class="col col-md-6" style="margin-top: 2vh">
|
||||
<b-dropdown id="sortby" :text="sortByLabel" variant="link" toggle-class="text-decoration-none " class="m-0 p-0">
|
||||
<div v-for="o in sortOptions" :key="o.id">
|
||||
<b-dropdown-item
|
||||
v-on:click="
|
||||
<div class="row align-content-center">
|
||||
<div class="col col-md-6" style="margin-top: 2vh">
|
||||
<b-dropdown id="sortby" :text="sortByLabel" variant="link" toggle-class="text-decoration-none "
|
||||
class="m-0 p-0">
|
||||
<div v-for="o in sortOptions" :key="o.id">
|
||||
<b-dropdown-item
|
||||
v-on:click="
|
||||
search.sort_order = [o]
|
||||
refreshData(false)
|
||||
"
|
||||
>
|
||||
<span>{{ o.text }}</span>
|
||||
</b-dropdown-item>
|
||||
</div>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
<div class="col col-md-6 text-right" style="margin-top: 2vh">
|
||||
>
|
||||
<span>{{ o.text }}</span>
|
||||
</b-dropdown-item>
|
||||
</div>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
<div class="col col-md-6 text-right" style="margin-top: 2vh">
|
||||
<span class="text-muted">
|
||||
{{ $t("Page") }} {{ search.pagination_page }}/{{ Math.ceil(pagination_count / ui.page_size) }}
|
||||
{{ $t("Page") }} {{
|
||||
search.pagination_page
|
||||
}}/{{ Math.ceil(pagination_count / ui.page_size) }}
|
||||
<a href="#" @click="resetSearch()"><i class="fas fa-times-circle"></i> {{ $t("Reset") }}</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="recipes.length > 0">
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-gap: 0.8rem">
|
||||
<div
|
||||
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-gap: 0.8rem">
|
||||
<template v-if="!searchFiltered()">
|
||||
<recipe-card
|
||||
v-bind:key="`mp_${m.id}`"
|
||||
@ -654,14 +840,17 @@
|
||||
footer_icon="far fa-calendar-alt"
|
||||
></recipe-card>
|
||||
</template>
|
||||
<recipe-card v-for="r in recipes" v-bind:key="r.id" :recipe="r" :footer_text="isRecentOrNew(r)[0]" :footer_icon="isRecentOrNew(r)[1]"></recipe-card>
|
||||
<recipe-card v-for="r in recipes" v-bind:key="r.id" :recipe="r"
|
||||
:footer_text="isRecentOrNew(r)[0]"
|
||||
:footer_icon="isRecentOrNew(r)[1]"></recipe-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top: 2vh" v-if="!random_search">
|
||||
<div class="col col-md-12">
|
||||
<b-pagination pills v-model="search.pagination_page" :total-rows="pagination_count" :per-page="ui.page_size" @change="pageChange" align="center"></b-pagination>
|
||||
<b-pagination pills v-model="search.pagination_page" :total-rows="pagination_count"
|
||||
:per-page="ui.page_size" @change="pageChange" align="center"></b-pagination>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 d-none d-md-block"></div>
|
||||
@ -681,7 +870,9 @@
|
||||
{{ $t("search_import_help_text") }}
|
||||
</b-card-text>
|
||||
|
||||
<b-button variant="primary" :href="resolveDjangoUrl('data_import_url')"><i class="fas fa-file-import"></i> {{ $t("Import") }} </b-button>
|
||||
<b-button variant="primary" :href="resolveDjangoUrl('data_import_url')"><i
|
||||
class="fas fa-file-import"></i> {{ $t("Import") }}
|
||||
</b-button>
|
||||
</b-card>
|
||||
|
||||
<b-card v-bind:title="$t('Create')" class="text-center">
|
||||
@ -689,7 +880,9 @@
|
||||
{{ $t("search_create_help_text") }}
|
||||
</b-card-text>
|
||||
|
||||
<b-button variant="primary" :href="resolveDjangoUrl('new_recipe')"><i class="fas fa-plus"></i> {{ $t("Create") }} </b-button>
|
||||
<b-button variant="primary" :href="resolveDjangoUrl('new_recipe')"><i
|
||||
class="fas fa-plus"></i> {{ $t("Create") }}
|
||||
</b-button>
|
||||
</b-card>
|
||||
</b-card-group>
|
||||
</div>
|
||||
@ -702,17 +895,17 @@
|
||||
|
||||
<script>
|
||||
import Vue from "vue"
|
||||
import { BootstrapVue } from "bootstrap-vue"
|
||||
import {BootstrapVue} from "bootstrap-vue"
|
||||
import VueCookies from "vue-cookies"
|
||||
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||
|
||||
import moment from "moment"
|
||||
import _debounce from "lodash/debounce"
|
||||
import Multiselect from "vue-multiselect"
|
||||
import { Treeselect, LOAD_CHILDREN_OPTIONS } from "@riophae/vue-treeselect"
|
||||
import {Treeselect, LOAD_CHILDREN_OPTIONS} from "@riophae/vue-treeselect"
|
||||
import "@riophae/vue-treeselect/dist/vue-treeselect.css"
|
||||
|
||||
import { ApiMixin, ResolveUrlMixin, StandardToasts, ToastMixin } from "@/utils/utils"
|
||||
import {ApiMixin, ResolveUrlMixin, StandardToasts, ToastMixin} from "@/utils/utils"
|
||||
import LoadingSpinner from "@/components/LoadingSpinner" // TODO: is this deprecated?
|
||||
import RecipeCard from "@/components/RecipeCard"
|
||||
import GenericMultiselect from "@/components/GenericMultiselect"
|
||||
@ -727,13 +920,13 @@ let UI_COOKIE_NAME = "ui_search_settings"
|
||||
export default {
|
||||
name: "RecipeSearchView",
|
||||
mixins: [ResolveUrlMixin, ApiMixin, ToastMixin],
|
||||
components: { GenericMultiselect, RecipeCard, Treeselect, RecipeSwitcher, Multiselect },
|
||||
components: {GenericMultiselect, RecipeCard, Treeselect, RecipeSwitcher, Multiselect},
|
||||
data() {
|
||||
return {
|
||||
// this.Models and this.Actions inherited from ApiMixin
|
||||
recipes: [],
|
||||
recipes_loading: true,
|
||||
facets: { Books: [], Foods: [], Keywords: [] },
|
||||
facets: {Books: [], Foods: [], Keywords: []},
|
||||
meal_plans: [],
|
||||
last_viewed_recipes: [],
|
||||
sortMenu: false,
|
||||
@ -744,22 +937,22 @@ export default {
|
||||
search_input: "",
|
||||
search_internal: false,
|
||||
search_keywords: [
|
||||
{ items: [], operator: true, not: false },
|
||||
{ items: [], operator: false, not: false },
|
||||
{ items: [], operator: true, not: true },
|
||||
{ items: [], operator: false, not: true },
|
||||
{items: [], operator: true, not: false},
|
||||
{items: [], operator: false, not: false},
|
||||
{items: [], operator: true, not: true},
|
||||
{items: [], operator: false, not: true},
|
||||
],
|
||||
search_foods: [
|
||||
{ items: [], operator: true, not: false },
|
||||
{ items: [], operator: false, not: false },
|
||||
{ items: [], operator: true, not: true },
|
||||
{ items: [], operator: false, not: true },
|
||||
{items: [], operator: true, not: false},
|
||||
{items: [], operator: false, not: false},
|
||||
{items: [], operator: true, not: true},
|
||||
{items: [], operator: false, not: true},
|
||||
],
|
||||
search_books: [
|
||||
{ items: [], operator: true, not: false },
|
||||
{ items: [], operator: false, not: false },
|
||||
{ items: [], operator: true, not: true },
|
||||
{ items: [], operator: false, not: true },
|
||||
{items: [], operator: true, not: false},
|
||||
{items: [], operator: false, not: false},
|
||||
{items: [], operator: true, not: true},
|
||||
{items: [], operator: false, not: true},
|
||||
],
|
||||
search_units: [],
|
||||
search_units_or: true,
|
||||
@ -868,12 +1061,12 @@ export default {
|
||||
}
|
||||
|
||||
return [
|
||||
{ id: 5, label: "⭐⭐⭐⭐⭐" + ratingCount(this.facets.Ratings?.["5.0"] ?? 0) + label(5) },
|
||||
{ id: 4, label: "⭐⭐⭐⭐ " + ratingCount(this.facets.Ratings?.["4.0"] ?? 0) + label() },
|
||||
{ id: 3, label: "⭐⭐⭐ " + ratingCount(this.facets.Ratings?.["3.0"] ?? 0) + label() },
|
||||
{ id: 2, label: "⭐⭐ " + ratingCount(this.facets.Ratings?.["2.0"] ?? 0) + label() },
|
||||
{ id: 1, label: "⭐ " + ratingCount(this.facets.Ratings?.["1.0"] ?? 0) + label(1) },
|
||||
{ id: 0, label: this.$t("Unrated") + ratingCount(this.facets.Ratings?.["0.0"] ?? 0) },
|
||||
{id: 5, label: "⭐⭐⭐⭐⭐" + ratingCount(this.facets.Ratings?.["5.0"] ?? 0) + label(5)},
|
||||
{id: 4, label: "⭐⭐⭐⭐ " + ratingCount(this.facets.Ratings?.["4.0"] ?? 0) + label()},
|
||||
{id: 3, label: "⭐⭐⭐ " + ratingCount(this.facets.Ratings?.["3.0"] ?? 0) + label()},
|
||||
{id: 2, label: "⭐⭐ " + ratingCount(this.facets.Ratings?.["2.0"] ?? 0) + label()},
|
||||
{id: 1, label: "⭐ " + ratingCount(this.facets.Ratings?.["1.0"] ?? 0) + label(1)},
|
||||
{id: 0, label: this.$t("Unrated") + ratingCount(this.facets.Ratings?.["0.0"] ?? 0)},
|
||||
]
|
||||
},
|
||||
keywordFields: function () {
|
||||
@ -936,22 +1129,22 @@ export default {
|
||||
this.facets.Keywords = []
|
||||
for (let x of urlParams.getAll("keyword")) {
|
||||
this.search.search_keywords[0].items.push(Number.parseInt(x))
|
||||
this.facets.Keywords.push({ id: x, name: "loading..." })
|
||||
this.facets.Keywords.push({id: x, name: "loading..."})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: figure out how to find nested items and load keyword/food children for that branch
|
||||
// probably a backend change in facets to pre-load children of nested items
|
||||
for (let x of this.search.search_foods.map((x) => x.items).flat()) {
|
||||
this.facets.Foods.push({ id: x, name: "loading..." })
|
||||
this.facets.Foods.push({id: x, name: "loading..."})
|
||||
}
|
||||
|
||||
for (let x of this.search.search_keywords.map((x) => x.items).flat()) {
|
||||
this.facets.Keywords.push({ id: x, name: "loading..." })
|
||||
this.facets.Keywords.push({id: x, name: "loading..."})
|
||||
}
|
||||
|
||||
for (let x of this.search.search_books.map((x) => x.items).flat()) {
|
||||
this.facets.Books.push({ id: x, name: "loading..." })
|
||||
this.facets.Books.push({id: x, name: "loading..."})
|
||||
}
|
||||
|
||||
this.loadMealPlan()
|
||||
@ -1001,13 +1194,13 @@ export default {
|
||||
"ui.expert_mode": function (newVal, oldVal) {
|
||||
if (!newVal) {
|
||||
this.search.search_keywords = this.search.search_keywords.map((x) => {
|
||||
return { ...x, not: false }
|
||||
return {...x, not: false}
|
||||
})
|
||||
this.search.search_foods = this.search.search_foods.map((x) => {
|
||||
return { ...x, not: false }
|
||||
return {...x, not: false}
|
||||
})
|
||||
this.search.search_books = this.search.search_books.map((x) => {
|
||||
return { ...x, not: false }
|
||||
return {...x, not: false}
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -1077,13 +1270,13 @@ export default {
|
||||
},
|
||||
resetSearch: function (filter = undefined) {
|
||||
this.search.search_keywords = this.search.search_keywords.map((x) => {
|
||||
return { ...x, items: [] }
|
||||
return {...x, items: []}
|
||||
})
|
||||
this.search.search_foods = this.search.search_foods.map((x) => {
|
||||
return { ...x, items: [] }
|
||||
return {...x, items: []}
|
||||
})
|
||||
this.search.search_books = this.search.search_books.map((x) => {
|
||||
return { ...x, items: [] }
|
||||
return {...x, items: []}
|
||||
})
|
||||
this.search.search_input = filter?.query ?? ""
|
||||
this.search.search_internal = filter?.internal ?? false
|
||||
@ -1138,12 +1331,12 @@ export default {
|
||||
if (!this.ui.tree_select) {
|
||||
return
|
||||
}
|
||||
let params = { hash: hash }
|
||||
let params = {hash: hash}
|
||||
if (facet) {
|
||||
params[facet] = id
|
||||
}
|
||||
return this.genericGetAPI("api_get_facets", params).then((response) => {
|
||||
this.facets = { ...this.facets, ...response.data.facets }
|
||||
this.facets = {...this.facets, ...response.data.facets}
|
||||
})
|
||||
},
|
||||
showSQL: function () {
|
||||
@ -1154,14 +1347,14 @@ export default {
|
||||
})
|
||||
},
|
||||
// TODO refactor to combine with load KeywordChildren
|
||||
loadFoodChildren({ action, parentNode, callback }) {
|
||||
loadFoodChildren({action, parentNode, callback}) {
|
||||
if (action === LOAD_CHILDREN_OPTIONS) {
|
||||
if (this.facets?.cache_key) {
|
||||
this.getFacets(this.facets.cache_key, "food", parentNode.id).then(callback())
|
||||
}
|
||||
}
|
||||
},
|
||||
loadKeywordChildren({ action, parentNode, callback }) {
|
||||
loadKeywordChildren({action, parentNode, callback}) {
|
||||
if (action === LOAD_CHILDREN_OPTIONS) {
|
||||
if (this.facets?.cache_key) {
|
||||
this.getFacets(this.facets.cache_key, "keyword", parentNode.id).then(callback())
|
||||
@ -1172,7 +1365,7 @@ export default {
|
||||
return
|
||||
},
|
||||
buildParams: function (random) {
|
||||
let params = { options: { query: {} }, page: this.search.pagination_page, pageSize: this.ui.page_size }
|
||||
let params = {options: {query: {}}, page: this.search.pagination_page, pageSize: this.ui.page_size}
|
||||
if (this.search.search_filter) {
|
||||
params.options.query.filter = this.search.search_filter.id
|
||||
return params
|
||||
@ -1293,7 +1486,7 @@ export default {
|
||||
;["page", "pageSize"].forEach((key) => {
|
||||
delete search[key]
|
||||
})
|
||||
search = { ...search, ...search.options.query }
|
||||
search = {...search, ...search.options.query}
|
||||
console.log("after concat", search)
|
||||
let params = {
|
||||
name: filtername,
|
||||
|
@ -440,7 +440,6 @@
|
||||
:initial_selection="settings.shopping_share"
|
||||
label="username"
|
||||
:multiple="true"
|
||||
:allow_create="false"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
:placeholder="$t('User')"
|
||||
/>
|
||||
|
@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<div v-if="recipes !== {}">
|
||||
<div id="switcher" class="align-center">
|
||||
<i class="btn btn-primary fas fa-receipt fa-xl fa-fw shadow-none btn-circle" v-b-toggle.related-recipes />
|
||||
<i class="btn btn-primary fas fa-receipt fa-xl fa-fw shadow-none btn-circle" v-b-toggle.related-recipes/>
|
||||
</div>
|
||||
<b-sidebar id="related-recipes" backdrop right bottom no-header shadow="sm" style="z-index: 10000" @shown="updatePinnedRecipes()">
|
||||
<b-sidebar id="related-recipes" backdrop right bottom no-header shadow="sm" style="z-index: 10000"
|
||||
@shown="updatePinnedRecipes()">
|
||||
<template #default="{ hide }">
|
||||
<div class="d-flex flex-column justify-content-end h-100 p-3 align-items-end">
|
||||
<h5>{{$t("Planned")}} <i class="fas fa-calendar fa-fw"></i></h5>
|
||||
<h5>{{ $t("Planned") }} <i class="fas fa-calendar fa-fw"></i></h5>
|
||||
|
||||
<div class="text-right">
|
||||
<template v-if="planned_recipes.length > 0">
|
||||
@ -18,24 +19,25 @@
|
||||
hide()
|
||||
"
|
||||
href="javascript:void(0);"
|
||||
>{{ r.name }}</a
|
||||
>{{ r.name }}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="text-muted">{{$t("nothing_planned_today")}}</span>
|
||||
<span class="text-muted">{{ $t("nothing_planned_today") }}</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<h5>{{$t("Pinned")}} <i class="fas fa-thumbtack fa-fw"></i></h5>
|
||||
<h5>{{ $t("Pinned") }} <i class="fas fa-thumbtack fa-fw"></i></h5>
|
||||
|
||||
<template v-if="pinned_recipes.length > 0">
|
||||
<div class="text-right">
|
||||
<div v-for="r in pinned_recipes" :key="`pin${r.id}`">
|
||||
<b-row class="pb-1 pt-1">
|
||||
<b-col cols="2">
|
||||
<a href="javascript:void(0)" @click="unPinRecipe(r)" class="text-muted"><i class="fas fa-times"></i></a>
|
||||
<a href="javascript:void(0)" @click="unPinRecipe(r)" class="text-muted"><i
|
||||
class="fas fa-times"></i></a>
|
||||
</b-col>
|
||||
<b-col cols="10">
|
||||
<a
|
||||
@ -45,7 +47,7 @@
|
||||
"
|
||||
href="javascript:void(0);"
|
||||
class="align-self-end"
|
||||
>{{ r.name }}
|
||||
>{{ r.name }}
|
||||
</a>
|
||||
</b-col>
|
||||
</b-row>
|
||||
@ -53,7 +55,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="text-muted">{{$t("no_pinned_recipes")}}</span>
|
||||
<span class="text-muted">{{ $t("no_pinned_recipes") }}</span>
|
||||
</template>
|
||||
|
||||
<template v-if="related_recipes.length > 0">
|
||||
@ -67,7 +69,7 @@
|
||||
hide()
|
||||
"
|
||||
href="javascript:void(0);"
|
||||
>{{ r.name }}</a
|
||||
>{{ r.name }}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@ -77,8 +79,8 @@
|
||||
</template>
|
||||
<template #footer="{ hide }">
|
||||
<div class="d-flex bg-dark text-light align-items-center px-3 py-2">
|
||||
<strong class="mr-auto">{{$t("Quick actions")}}</strong>
|
||||
<b-button size="sm" @click="hide">{{$t("Close")}}</b-button>
|
||||
<strong class="mr-auto">{{ $t("Quick actions") }}</strong>
|
||||
<b-button size="sm" @click="hide">{{ $t("Close") }}</b-button>
|
||||
</div>
|
||||
</template>
|
||||
</b-sidebar>
|
||||
@ -86,14 +88,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const { ApiApiFactory } = require("@/utils/openapi/api")
|
||||
import { ResolveUrlMixin } from "@/utils/utils"
|
||||
const {ApiApiFactory} = require("@/utils/openapi/api")
|
||||
import {ResolveUrlMixin} from "@/utils/utils"
|
||||
|
||||
export default {
|
||||
name: "RecipeSwitcher",
|
||||
mixins: [ResolveUrlMixin],
|
||||
props: {
|
||||
recipe: { type: Number, default: undefined },
|
||||
recipe: {type: Number, default: undefined},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -158,7 +160,12 @@ export default {
|
||||
// get related recipes and save them for later
|
||||
if (this.$parent.recipe) {
|
||||
this.related_recipes = [this.$parent.recipe]
|
||||
return apiClient.relatedRecipe(this.$parent.recipe.id, { query: { levels: 2, format: "json" } }).then((result) => {
|
||||
return apiClient.relatedRecipe(this.$parent.recipe.id, {
|
||||
query: {
|
||||
levels: 2,
|
||||
format: "json"
|
||||
}
|
||||
}).then((result) => {
|
||||
this.related_recipes = this.related_recipes.concat(result.data)
|
||||
})
|
||||
}
|
||||
@ -172,16 +179,16 @@ export default {
|
||||
// TODO move to utility function moment is in maintenance mode https://momentjs.com/docs/
|
||||
var tzoffset = new Date().getTimezoneOffset() * 60000 //offset in milliseconds
|
||||
let today = new Date(Date.now() - tzoffset).toISOString().split("T")[0]
|
||||
return apiClient.listMealPlans({ query: { from_date: today, to_date: today } }).then((result) => {
|
||||
return apiClient.listMealPlans({query: {from_date: today, to_date: today}}).then((result) => {
|
||||
let promises = []
|
||||
result.data.forEach((mealplan) => {
|
||||
this.planned_recipes.push({ ...mealplan?.recipe, servings: mealplan?.servings })
|
||||
this.planned_recipes.push({...mealplan?.recipe, servings: mealplan?.servings})
|
||||
const serving_factor = (mealplan?.servings ?? mealplan?.recipe?.servings ?? 1) / (mealplan?.recipe?.servings ?? 1)
|
||||
promises.push(
|
||||
apiClient.relatedRecipe(mealplan?.recipe?.id, { query: { levels: 2 } }).then((r) => {
|
||||
apiClient.relatedRecipe(mealplan?.recipe?.id, {query: {levels: 2}}).then((r) => {
|
||||
// scale all recipes to mealplan servings
|
||||
r.data = r.data.map((x) => {
|
||||
return { ...x, factor: serving_factor }
|
||||
return {...x, factor: serving_factor}
|
||||
})
|
||||
this.planned_recipes = [...this.planned_recipes, ...r.data]
|
||||
})
|
||||
@ -213,20 +220,28 @@ export default {
|
||||
z-index: 9000;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
#switcher .btn-circle {
|
||||
position: fixed;
|
||||
top: 9px;
|
||||
left: 80px;
|
||||
color: white;
|
||||
top: 12px;
|
||||
right: 79px;
|
||||
color: rgba(46, 46, 46, 0.5);
|
||||
width: 56px;
|
||||
height: 40px;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1;
|
||||
background-color: transparent;
|
||||
border: 1px solid rgba(46, 46, 46, 0.5);
|
||||
border-radius: 0.1875rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 2000px) {
|
||||
@media (min-width: 992px) {
|
||||
#switcher .btn-circle {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
right: 50px;
|
||||
bottom: 40px;
|
||||
right: 40px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -6,7 +6,10 @@
|
||||
</template>
|
||||
<b-dropdown-item v-on:click="$emit('item-action', 'edit')" v-if="show_edit"> <i class="fas fa-pencil-alt fa-fw"></i> {{ $t("Edit") }} </b-dropdown-item>
|
||||
|
||||
|
||||
<b-dropdown-item v-on:click="$emit('item-action', 'delete')" v-if="show_delete"> <i class="fas fa-trash-alt fa-fw"></i> {{ $t("Delete") }} </b-dropdown-item>
|
||||
<b-dropdown-item v-on:click="$emit('item-action', 'ingredient-editor')" v-if="show_ingredient_editor"> <i class="fas fa-th-list fa-dw"></i> {{ $t("Ingredient Editor") }} </b-dropdown-item>
|
||||
|
||||
<b-dropdown-item v-on:click="$emit('item-action', 'add-shopping')" v-if="show_shopping">
|
||||
<i class="fas fa-cart-plus fa-fw"></i> {{ $t("Add_to_Shopping") }}
|
||||
</b-dropdown-item>
|
||||
@ -24,8 +27,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {ResolveUrlMixin} from "@/utils/utils";
|
||||
|
||||
export default {
|
||||
name: "GenericContextMenu",
|
||||
mixins: [ResolveUrlMixin],
|
||||
props: {
|
||||
show_edit: { type: Boolean, default: true },
|
||||
show_delete: { type: Boolean, default: true },
|
||||
@ -33,6 +39,7 @@ export default {
|
||||
show_merge: { type: Boolean, default: false },
|
||||
show_shopping: { type: Boolean, default: false },
|
||||
show_onhand: { type: Boolean, default: false },
|
||||
show_ingredient_editor: { type: Boolean, default: false },
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -61,6 +61,7 @@
|
||||
:show_move="useMove"
|
||||
:show_shopping="useShopping"
|
||||
:show_onhand="useOnhand"
|
||||
:show_ingredient_editor="useIngredientEditor"
|
||||
@item-action="$emit('item-action', { action: $event, source: item })"
|
||||
>
|
||||
</generic-context-menu>
|
||||
@ -132,11 +133,12 @@ import GenericOrderedPill from "@/components/GenericOrderedPill"
|
||||
import RecipeCard from "@/components/RecipeCard"
|
||||
import { mixin as clickaway } from "vue-clickaway"
|
||||
import { createPopper } from "@popperjs/core"
|
||||
import {ApiMixin} from "@/utils/utils";
|
||||
|
||||
export default {
|
||||
name: "GenericHorizontalCard",
|
||||
components: { GenericContextMenu, RecipeCard, Badges, GenericPill, GenericOrderedPill },
|
||||
mixins: [clickaway],
|
||||
mixins: [clickaway, ApiMixin],
|
||||
props: {
|
||||
item: { type: Object },
|
||||
model: { type: Object },
|
||||
@ -186,6 +188,9 @@ export default {
|
||||
useDrag: function () {
|
||||
return this.useMove || this.useMerge
|
||||
},
|
||||
useIngredientEditor: function (){
|
||||
return (this.model === this.Models.FOOD || this.model === this.Models.UNIT)
|
||||
},
|
||||
itemTags: function () {
|
||||
return this.model?.tags ?? []
|
||||
},
|
||||
|
@ -19,6 +19,7 @@
|
||||
@search-change="search"
|
||||
@input="selectionChanged"
|
||||
@tag="addNew"
|
||||
@open="selectOpened()"
|
||||
>
|
||||
</multiselect>
|
||||
</template>
|
||||
@ -26,11 +27,11 @@
|
||||
<script>
|
||||
import Vue from "vue"
|
||||
import Multiselect from "vue-multiselect"
|
||||
import { ApiMixin } from "@/utils/utils"
|
||||
import {ApiMixin, StandardToasts} from "@/utils/utils"
|
||||
|
||||
export default {
|
||||
name: "GenericMultiselect",
|
||||
components: { Multiselect },
|
||||
components: {Multiselect},
|
||||
mixins: [ApiMixin],
|
||||
data() {
|
||||
return {
|
||||
@ -42,16 +43,16 @@ export default {
|
||||
}
|
||||
},
|
||||
props: {
|
||||
placeholder: { type: String, default: undefined },
|
||||
placeholder: {type: String, default: undefined},
|
||||
model: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
label: { type: String, default: "name" },
|
||||
parent_variable: { type: String, default: undefined },
|
||||
limit: { type: Number, default: 25 },
|
||||
label: {type: String, default: "name"},
|
||||
parent_variable: {type: String, default: undefined},
|
||||
limit: {type: Number, default: 25},
|
||||
sticky_options: {
|
||||
type: Array,
|
||||
default() {
|
||||
@ -68,10 +69,11 @@ export default {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
multiple: { type: Boolean, default: true },
|
||||
allow_create: { type: Boolean, default: false },
|
||||
create_placeholder: { type: String, default: "You Forgot to Add a Tag Placeholder" },
|
||||
clear: { type: Number },
|
||||
search_on_load: {type: Boolean, default: true},
|
||||
multiple: {type: Boolean, default: true},
|
||||
allow_create: {type: Boolean, default: false},
|
||||
create_placeholder: {type: String, default: "You Forgot to Add a Tag Placeholder"},
|
||||
clear: {type: Number},
|
||||
},
|
||||
watch: {
|
||||
initial_selection: function (newVal, oldVal) {
|
||||
@ -82,12 +84,12 @@ export default {
|
||||
empty[this.label] = `..${this.$t("loading")}..`
|
||||
this.selected_objects.forEach((x) => {
|
||||
if (typeof x !== "object") {
|
||||
this.selected_objects[this.selected_objects.indexOf(x)] = { ...empty, id: x }
|
||||
this.selected_objects[this.selected_objects.indexOf(x)] = {...empty, id: x}
|
||||
get_details.push(x)
|
||||
}
|
||||
})
|
||||
get_details.forEach((x) => {
|
||||
this.genericAPI(this.model, this.Actions.FETCH, { id: x })
|
||||
this.genericAPI(this.model, this.Actions.FETCH, {id: x})
|
||||
.then((result) => {
|
||||
// this.selected_objects[this.selected_objects.map((y) => y.id).indexOf(x)] = result.data
|
||||
Vue.set(this.selected_objects, this.selected_objects.map((y) => y.id).indexOf(x), result.data)
|
||||
@ -103,8 +105,8 @@ export default {
|
||||
if (typeof this.selected_objects !== "object") {
|
||||
let empty = {}
|
||||
empty[this.label] = `..${this.$t("loading")}..`
|
||||
this.selected_objects = { ...empty, id: this.selected_objects }
|
||||
this.genericAPI(this.model, this.Actions.FETCH, { id: this.selected_objects })
|
||||
this.selected_objects = {...empty, id: this.selected_objects}
|
||||
this.genericAPI(this.model, this.Actions.FETCH, {id: this.selected_objects})
|
||||
.then((result) => {
|
||||
this.selected_objects = result.data
|
||||
})
|
||||
@ -123,7 +125,9 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.id = Math.random()
|
||||
this.search("")
|
||||
if (this.search_on_load) {
|
||||
this.search("")
|
||||
}
|
||||
if (this.multiple || !this.initial_single_selection) {
|
||||
this.selected_objects = this.initial_selection
|
||||
} else {
|
||||
@ -170,15 +174,30 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
selectOpened: function () {
|
||||
if (this.objects.length < 1) {
|
||||
this.search("")
|
||||
}
|
||||
},
|
||||
selectionChanged: function () {
|
||||
this.$emit("change", { var: this.parent_variable, val: this.selected_objects })
|
||||
this.$emit("change", {var: this.parent_variable, val: this.selected_objects})
|
||||
},
|
||||
addNew(e) {
|
||||
this.$emit("new", e)
|
||||
// could refactor as Promise - seems unnecessary
|
||||
setTimeout(() => {
|
||||
this.search("")
|
||||
}, 750)
|
||||
//TODO add ability to choose field name other than "name"
|
||||
console.log('CREATEING NEW with -> ' , e)
|
||||
this.genericAPI(this.model, this.Actions.CREATE, {name: e}).then(result => {
|
||||
let createdObj = result.data?.results ?? result.data
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||
if (this.multiple) {
|
||||
this.selected_objects.push(createdObj)
|
||||
} else {
|
||||
this.selected_objects = createdObj
|
||||
}
|
||||
this.objects.push(createdObj)
|
||||
this.selectionChanged()
|
||||
}).catch((r, err) => {
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -48,7 +48,6 @@
|
||||
:initial_single_selection="entryEditing.meal_type"
|
||||
:allow_create="true"
|
||||
:create_placeholder="$t('Create_New_Meal_Type')"
|
||||
@new="createMealType"
|
||||
></generic-multiselect>
|
||||
<span class="text-danger" v-if="missing_meal_type">{{ $t("Meal_Type_Required") }}</span>
|
||||
<small tabindex="-1" class="form-text text-muted" v-if="!missing_meal_type">{{ $t("Meal_Type") }}</small>
|
||||
@ -228,20 +227,6 @@ export default {
|
||||
this.entryEditing.meal_type = null
|
||||
}
|
||||
},
|
||||
createMealType(event) {
|
||||
if (event != "") {
|
||||
let apiClient = new ApiApiFactory()
|
||||
|
||||
apiClient
|
||||
.createMealType({ name: event })
|
||||
.then((e) => {
|
||||
this.$emit("reload-meal-types")
|
||||
})
|
||||
.catch((error) => {
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
|
||||
})
|
||||
}
|
||||
},
|
||||
selectRecipe(event) {
|
||||
this.missing_recipe = false
|
||||
if (event.val != null) {
|
||||
|
@ -176,8 +176,11 @@ export default {
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_DELETE)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_DELETE)
|
||||
if (err.response.status === 403){
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_DELETE_PROTECTED)
|
||||
}else {
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_DELETE)
|
||||
}
|
||||
this.$emit("finish-action", "cancel")
|
||||
})
|
||||
},
|
||||
|
@ -18,7 +18,6 @@
|
||||
:label="list_label"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
:placeholder="modelName"
|
||||
@new="addNew"
|
||||
>
|
||||
</generic-multiselect>
|
||||
<em v-if="help" class="small text-muted">{{ help }}</em>
|
||||
@ -119,19 +118,6 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addNew: function (e) {
|
||||
// if create a new item requires more than 1 parameter or the field 'name' is insufficient this will need reworked
|
||||
// in a perfect world this would trigger a new modal and allow editing all fields
|
||||
this.genericAPI(this.model, this.Actions.CREATE, { name: e })
|
||||
.then((result) => {
|
||||
this.new_value = result.data
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
||||
})
|
||||
},
|
||||
// ordered lookups have nested attributes that need flattened attributes to drive lookup
|
||||
flattenItems: function (itemlist) {
|
||||
let flat_items = []
|
||||
|
@ -34,7 +34,7 @@
|
||||
</template>
|
||||
<p class="mt-1">
|
||||
<last-cooked :recipe="recipe"></last-cooked>
|
||||
<keywords-component :recipe="recipe" style="margin-top: 4px"></keywords-component>
|
||||
<keywords-component :recipe="recipe" style="margin-top: 4px; position: relative; z-index: 3;"></keywords-component>
|
||||
</p>
|
||||
<transition name="fade" mode="in-out">
|
||||
<div class="row mt-3" v-if="show_detail">
|
||||
|
@ -206,11 +206,11 @@ export default {
|
||||
this.$bvModal.show(`shopping_${this.modal_id}`)
|
||||
},
|
||||
copyToNew: function () {
|
||||
let recipename = window.prompt(this.$t("copy_to_new"), this.$t("recipe_name"))
|
||||
let recipe_name = window.prompt(this.$t("copy_to_new"), this.$t("recipe_name"))
|
||||
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.retrieveRecipe(this.recipe.id).then((results) => {
|
||||
let recipe = { ...results.data, ...{ id: undefined, name: recipename } }
|
||||
let recipe = { ...results.data, ...{ id: undefined, name: recipe_name } }
|
||||
recipe.steps = recipe.steps.map((step) => {
|
||||
return {
|
||||
...step,
|
||||
@ -222,12 +222,14 @@ export default {
|
||||
},
|
||||
}
|
||||
})
|
||||
console.log(recipe)
|
||||
if (recipe.nutrition !== null){
|
||||
delete recipe.nutrition.id
|
||||
}
|
||||
apiClient
|
||||
.createRecipe(recipe)
|
||||
.then((newrecipe) => {
|
||||
.then((new_recipe) => {
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||
window.open(this.resolveDjangoUrl("view_recipe", newrecipe.data.id))
|
||||
window.open(this.resolveDjangoUrl("view_recipe", new_recipe.data.id))
|
||||
})
|
||||
.catch((error) => {
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
||||
|
@ -4,6 +4,7 @@
|
||||
"err_creating_resource": "There was an error creating a resource!",
|
||||
"err_updating_resource": "There was an error updating a resource!",
|
||||
"err_deleting_resource": "There was an error deleting a resource!",
|
||||
"err_deleting_protected_resource": "The object you are trying to delete is still used and can't be deleted.",
|
||||
"err_moving_resource": "There was an error moving a resource!",
|
||||
"err_merging_resource": "There was an error merging a resource!",
|
||||
"success_fetching_resource": "Successfully fetched a resource!",
|
||||
@ -63,6 +64,7 @@
|
||||
"Make_Ingredient": "Make Ingredient",
|
||||
"Enable_Amount": "Enable Amount",
|
||||
"Disable_Amount": "Disable Amount",
|
||||
"Ingredient Editor": "Ingredient Editor",
|
||||
"Add_Step": "Add Step",
|
||||
"Keywords": "Keywords",
|
||||
"Books": "Books",
|
||||
@ -87,6 +89,7 @@
|
||||
"Note": "Note",
|
||||
"Success": "Success",
|
||||
"Failure": "Failure",
|
||||
"Protected": "Protected",
|
||||
"Ingredients": "Ingredients",
|
||||
"Supermarket": "Supermarket",
|
||||
"Categories": "Categories",
|
||||
|
@ -285,5 +285,99 @@
|
||||
"shopping_add_onhand": "Automatisch op voorraad",
|
||||
"related_recipes": "Gerelateerde recepten",
|
||||
"today_recipes": "Recepten van vandaag",
|
||||
"Search Settings": "Zoekinstellingen"
|
||||
"Search Settings": "Zoekinstellingen",
|
||||
"enable_expert": "Schakel expertmodus in",
|
||||
"expert_mode": "Expertmodus",
|
||||
"simple_mode": "Eenvoudige modus",
|
||||
"advanced": "Geavanceerd",
|
||||
"fields": "Velden",
|
||||
"show_keywords": "Toon etiketten",
|
||||
"show_foods": "Toon ingrediënten",
|
||||
"show_books": "Toon boeken",
|
||||
"show_rating": "Toon waardering",
|
||||
"show_units": "Toon eenheden",
|
||||
"show_filters": "Toon filters",
|
||||
"not": "niet",
|
||||
"save_filter": "Bewaar filter",
|
||||
"filter_name": "Naam filter",
|
||||
"Custom Filter": "Aangepast filter",
|
||||
"shared_with": "Gedeeld met",
|
||||
"sort_by": "Sorteer op",
|
||||
"asc": "Oplopend",
|
||||
"desc": "Aflopend",
|
||||
"date_viewed": "Laatst bekeken",
|
||||
"last_cooked": "Laatst bereid",
|
||||
"times_cooked": "Keren bereid",
|
||||
"date_created": "Datum aangemaakt",
|
||||
"show_sortby": "Toon gesorteerd op",
|
||||
"search_rank": "Zoekrang",
|
||||
"make_now": "Maak nu",
|
||||
"recipe_filter": "Receptenfilter",
|
||||
"book_filter_help": "Voeg naast handmatig toegewezen recepten ook recepten uit het receptfilter toe.",
|
||||
"copy_to_new": "Kopieer naar nieuw recept",
|
||||
"recipe_name": "Naam recept",
|
||||
"paste_ingredients_placeholder": "Plak ingrediëntenlijst hier...",
|
||||
"paste_ingredients": "Plak ingrediënten",
|
||||
"ingredient_list": "Ingrediëntenlijst",
|
||||
"explain": "Leg uit",
|
||||
"filter": "Filter",
|
||||
"search_no_recipes": "Er zijn geen recepten gevonden!",
|
||||
"search_import_help_text": "Importeer een recept van een externe website of applicatie.",
|
||||
"search_create_help_text": "Maak direct een nieuw recept in Tandoor.",
|
||||
"warning_duplicate_filter": "Waarschuwing: door technische beperkingen kan het hebben van meerdere filters of dezelfde combinatie (en/of/niet) tot onverwachte resultaten leiden.",
|
||||
"reset_children": "Overerving van kinderen resetten",
|
||||
"reset_children_help": "Overschrijf alle kinderen met waarden van overgeërfde velden. Overgeërfde velden van kinderen worden ingesteld als velden erven tenzij kinderen erven velden ingesteld is.",
|
||||
"substitute_help": "Vervangers worden overwogen bij het zoeken naar recepten die kunnen worden gemaakt met beschikbare ingrediënten.",
|
||||
"substitute_siblings_help": "Alle ingrediënten die een ouder delen met dit ingrediënt worden als vervangers beschouwd.",
|
||||
"substitute_siblings": "Vervangers",
|
||||
"substitute_children": "Vervang kinderen",
|
||||
"ChildInheritFields_help": "Standaard erven kinderen deze velden.",
|
||||
"last_viewed": "Laatst bekeken",
|
||||
"created_on": "Aangemaakt op",
|
||||
"updatedon": "Geüpdatet op",
|
||||
"advanced_search_settings": "Geavanceerde zoekinstellingen",
|
||||
"nothing_planned_today": "Je hebt niks gepland voor vandaag!",
|
||||
"Planned": "Gepland",
|
||||
"Pinned": "Vastgepind",
|
||||
"Quick actions": "Snelle acties",
|
||||
"Ratings": "Waardering",
|
||||
"Units": "Eenheden",
|
||||
"Random Recipes": "Willekeurige recepten",
|
||||
"parameter_count": "Parameter {count}",
|
||||
"select_keyword": "Selecteer etiket",
|
||||
"add_keyword": "Voeg etiket toe",
|
||||
"select_file": "Selecteer bestand",
|
||||
"select_recipe": "Selecteer recept",
|
||||
"select_unit": "Selecteer eenheid",
|
||||
"select_food": "Selecteer ingrediënt",
|
||||
"remove_selection": "Deselecteren",
|
||||
"empty_list": "Lijst is leeg.",
|
||||
"Select": "Selecteer",
|
||||
"Supermarkets": "Supermarkten",
|
||||
"User": "Gebruiker",
|
||||
"Keyword": "Etiket",
|
||||
"Advanced": "Geavanceerd",
|
||||
"Page": "Pagina",
|
||||
"left_handed": "Linkshandige modus",
|
||||
"Pin": "Pin",
|
||||
"shopping_category_help": "Supermarkten kunnen gesorteerd en gefilterd worden per boodschappencategorie conform the indeling van de gangpaden.",
|
||||
"Foods": "Ingrediënten",
|
||||
"OnHand_help": "Ingrediënt is op voorraad en wordt niet automatisch aan een boodschappenlijstje toegevoegd. Voorraadstatus is gedeeld tussen gebruikers.",
|
||||
"ignore_shopping_help": "Voeg ingrediënt nooit toe aan boodschappenlijstjes (bijv. water)",
|
||||
"view_recipe": "Bekijk recept",
|
||||
"review_shopping": "Beoordeel items op het boodschappenlijstje voor opslaan",
|
||||
"and_down": "& omlaag",
|
||||
"remember_hours": "Te onthouden uren",
|
||||
"food_recipe_help": "Hier een recept koppelen voegt het gekoppelde recept toe in elk ander recept dat dit ingrediënt gebruikt",
|
||||
"left_handed_help": "Optimaliseert de gebruikersinterface voor linkshandig gebruik.",
|
||||
"substitute_children_help": "Alle ingrediënten die kinderen zijn van dit ingrediënt worden beschouwd als vervangers.",
|
||||
"SubstituteOnHand": "Je hebt een vervanger op voorraad.",
|
||||
"ChildInheritFields": "Kinderen erven velden",
|
||||
"InheritFields_help": "De waarden van deze velden worden geërfd van een ouder (uitzondering: lege boodschappencategorieën)",
|
||||
"no_pinned_recipes": "Je hebt geen vastgepinde recepten!",
|
||||
"Internal": "Interne",
|
||||
"Reset": "Herstel",
|
||||
"remember_search": "Onthoud zoekopdracht",
|
||||
"tree_select": "Gebruik boomselectie",
|
||||
"sql_debug": "SQL Debug"
|
||||
}
|
||||
|
@ -290,12 +290,98 @@
|
||||
"Foods": "Żywność",
|
||||
"view_recipe": "Zobacz przepis",
|
||||
"left_handed": "Tryb dla leworęcznych",
|
||||
"OnHand_help": "Żywność jest w spiżarni i nie zostanie automatycznie dodana do listy zakupów.",
|
||||
"OnHand_help": "Żywność jest w spiżarni i nie zostanie automatycznie dodana do listy zakupów. Status podręczny jest współdzielony z użytkownikami robiącymi zakupy.",
|
||||
"ignore_shopping_help": "Nigdy nie dodawaj żywności do listy zakupów (np. wody)",
|
||||
"shopping_category_help": "Z supermarketów można zamawiać i filtrować według kategorii zakupów zgodnie z układem alejek.",
|
||||
"review_shopping": "Przejrzyj wpisy zakupów przed zapisaniem",
|
||||
"sql_debug": "Debugowanie SQL",
|
||||
"remember_search": "Zapamiętaj wyszukiwanie",
|
||||
"remember_hours": "Godziny do zapamiętania",
|
||||
"tree_select": "Użyj drzewa wyboru"
|
||||
"tree_select": "Użyj drzewa wyboru",
|
||||
"Custom Filter": "Filtr niestandardowy",
|
||||
"date_viewed": "Ostatnio oglądane",
|
||||
"book_filter_help": "Uwzględnij przepisy z filtra przepisów oprócz ręcznie przypisanych.",
|
||||
"search_import_help_text": "Zaimportuj przepis z zewnętrznej strony internetowej lub aplikacji.",
|
||||
"warning_duplicate_filter": "Ostrzeżenie: Ze względu na ograniczenia techniczne posiadanie wielu filtrów o tej samej kombinacji (i/lub/nie) może dać nieoczekiwane wyniki.",
|
||||
"reset_children_help": "Zastąp wszystkie potomne wartościami z pól dziedziczonych. Dziedziczone pola potomnych zostaną ustawione na Dziedzicz pola, chyba że pole potomne jest ustawione.",
|
||||
"and_down": "& Dół",
|
||||
"enable_expert": "Włącz tryb eksperta",
|
||||
"expert_mode": "Tryb eksperta",
|
||||
"simple_mode": "Tryb prosty",
|
||||
"advanced": "Zaawansowany",
|
||||
"fields": "Pola",
|
||||
"show_keywords": "Pokaż słowa kluczowe",
|
||||
"show_foods": "Pokaż jedzenie",
|
||||
"show_books": "Pokaż książki",
|
||||
"show_rating": "Pokaż ocenę",
|
||||
"show_units": "Pokaż jednostki",
|
||||
"show_filters": "Pokaż filtry",
|
||||
"not": "nie",
|
||||
"save_filter": "Zapisz filtr",
|
||||
"filter_name": "Nazwa filtra",
|
||||
"shared_with": "Współdzielone z",
|
||||
"sort_by": "Sortuj według",
|
||||
"asc": "Rosnąco",
|
||||
"desc": "Malejąco",
|
||||
"last_cooked": "Ostatnio gotowane",
|
||||
"times_cooked": "Ile razy gotowano",
|
||||
"date_created": "Data utworzenia",
|
||||
"show_sortby": "Pokaż Sortuj według",
|
||||
"search_rank": "Szukaj w rankingu",
|
||||
"make_now": "Zrób teraz",
|
||||
"recipe_filter": "Filtr przepisów",
|
||||
"copy_to_new": "Kopiuj do nowego przepisu",
|
||||
"recipe_name": "Nazwa przepisu",
|
||||
"paste_ingredients_placeholder": "Tutaj wklej listę składników...",
|
||||
"paste_ingredients": "Wklej składniki",
|
||||
"ingredient_list": "Lista składników",
|
||||
"explain": "Wyjaśnij",
|
||||
"filter": "Filtr",
|
||||
"search_no_recipes": "Nie udało się znaleźć żadnych przepisów!",
|
||||
"search_create_help_text": "Utwórz nowy przepis bezpośrednio w Tandoor.",
|
||||
"reset_children": "Zresetuj dziedziczenie potomne (Child)",
|
||||
"substitute_help": "Zamienniki są brane pod uwagę przy wyszukiwaniu przepisów, które można przygotować z posiadanych składników.",
|
||||
"InheritFields_help": "Wartości tych pól będą dziedziczone z nadrzędnego (Wyjątek: puste kategorie zakupowe nie są dziedziczone)",
|
||||
"substitute_siblings_help": "Wszystkie produkty, które współdzielą rodzica tego produktu, uważane są za zamienniki.",
|
||||
"substitute_children_help": "Wszystkie produkty, które są potomnymi tego produktu, uważane są za zamienniki.",
|
||||
"substitute_siblings": "Bliźniacze zamienniki",
|
||||
"substitute_children": "Potomne zamienniki",
|
||||
"SubstituteOnHand": "Masz pod ręką zamienniki.",
|
||||
"ChildInheritFields": "Potomne dziedziczą pola",
|
||||
"ChildInheritFields_help": "Potomne domyślnie odziedziczą te pola.",
|
||||
"last_viewed": "Ostatnio oglądane",
|
||||
"created_on": "Utworzono dnia",
|
||||
"updatedon": "Zaktualizowano dnia",
|
||||
"advanced_search_settings": "Zaawansowane ustawienia wyszukiwania",
|
||||
"nothing_planned_today": "Na dziś nic nie planujesz!",
|
||||
"no_pinned_recipes": "Nie masz przypiętych przepisów!",
|
||||
"Planned": "Zaplanowane",
|
||||
"Pinned": "Przypięte",
|
||||
"Quick actions": "Szybkie akcje",
|
||||
"Ratings": "Oceny",
|
||||
"Internal": "Wewnętrzne",
|
||||
"Units": "Jednostki",
|
||||
"Random Recipes": "Losowe przepisy",
|
||||
"parameter_count": "Parametr {count}",
|
||||
"select_keyword": "Wybierz słowo kluczowe",
|
||||
"add_keyword": "Dodaj słowo kluczowe",
|
||||
"select_file": "Wybierz plik",
|
||||
"select_recipe": "Wybierz przepis",
|
||||
"select_unit": "Wybierz jednostkę",
|
||||
"select_food": "Wybierz jedzenie/produkt",
|
||||
"remove_selection": "Odznacz",
|
||||
"empty_list": "Lista jest pusta.",
|
||||
"Select": "Zaznacz",
|
||||
"Supermarkets": "Supermarkety",
|
||||
"User": "Użytkownik",
|
||||
"Keyword": "Słowo kluczowe",
|
||||
"Advanced": "Zaawansowany",
|
||||
"Page": "Strona",
|
||||
"Reset": "Resetowanie",
|
||||
"Create Food": "Twórz jedzenie",
|
||||
"create_food_desc": "Stwórz jedzenie i połącz je z tym przepisem.",
|
||||
"additional_options": "Opcje dodatkowe",
|
||||
"err_deleting_protected_resource": "Obiekt, który próbujesz usunąć, jest nadal używany i nie można go usunąć.",
|
||||
"Protected": "Chroniony",
|
||||
"Ingredient Editor": "Edytor składników"
|
||||
}
|
||||
|
@ -351,30 +351,32 @@
|
||||
"last_viewed": "",
|
||||
"created_on": "Criado em",
|
||||
"updatedon": "Atualizado em",
|
||||
"advanced_search_settings": "",
|
||||
"nothing_planned_today": "",
|
||||
"no_pinned_recipes": "",
|
||||
"Planned": "",
|
||||
"Pinned": "",
|
||||
"Quick actions": "",
|
||||
"Ratings": "",
|
||||
"advanced_search_settings": "Configurações Avançadas de Pesquisa",
|
||||
"nothing_planned_today": "Não Tem nada planeado para hoje!",
|
||||
"no_pinned_recipes": "Não Tem nenhuma receita marcada!",
|
||||
"Planned": "Planeado",
|
||||
"Pinned": "Marcado",
|
||||
"Quick actions": "Acções Rápidas",
|
||||
"Ratings": "Avaliações",
|
||||
"Internal": "Interno",
|
||||
"Units": "",
|
||||
"Random Recipes": "",
|
||||
"parameter_count": "",
|
||||
"select_keyword": "",
|
||||
"add_keyword": "",
|
||||
"select_file": "",
|
||||
"select_recipe": "",
|
||||
"select_unit": "",
|
||||
"select_food": "",
|
||||
"remove_selection": "",
|
||||
"empty_list": "",
|
||||
"Select": "",
|
||||
"Supermarkets": "",
|
||||
"Units": "Unidades",
|
||||
"Random Recipes": "Receitas Aleatórias",
|
||||
"parameter_count": "Parametro {count}",
|
||||
"select_keyword": "Selecionar Palavra Chave",
|
||||
"add_keyword": "Adicionar Palavra Chave",
|
||||
"select_file": "Selecionar Ficheiro",
|
||||
"select_recipe": "Selecionar Receita",
|
||||
"select_unit": "Selecionar Unidade",
|
||||
"select_food": "Selecionar Comida",
|
||||
"remove_selection": "Deselecionar",
|
||||
"empty_list": "Lista está Vazia.",
|
||||
"Select": "Selecionar",
|
||||
"Supermarkets": "Supermercados",
|
||||
"User": "Utilizador",
|
||||
"Keyword": "",
|
||||
"Advanced": "",
|
||||
"Page": "",
|
||||
"Reset": ""
|
||||
"Keyword": "Palavra Chave",
|
||||
"Advanced": "Avançado",
|
||||
"Page": "Página",
|
||||
"Reset": "Reiniciar",
|
||||
"Create Food": "Criar Comida",
|
||||
"create_food_desc": "Criar a comida e ligar a esta receita."
|
||||
}
|
||||
|
@ -64,7 +64,7 @@
|
||||
"Fats": "Жиры",
|
||||
"Carbohydrates": "Углеводы",
|
||||
"Calories": "Каллории",
|
||||
"Energy": "",
|
||||
"Energy": "Энергетическая ценность",
|
||||
"Nutrition": "Питательность",
|
||||
"Date": "Дата",
|
||||
"Share": "Поделиться",
|
||||
@ -87,107 +87,107 @@
|
||||
"Category": "Категория",
|
||||
"Selected": "Выбрать",
|
||||
"min": "мин",
|
||||
"Servings": "",
|
||||
"Waiting": "",
|
||||
"Preparation": "",
|
||||
"External": "",
|
||||
"Size": "",
|
||||
"Files": "",
|
||||
"File": "",
|
||||
"Edit": "",
|
||||
"Image": "",
|
||||
"Delete": "",
|
||||
"Open": "",
|
||||
"Ok": "",
|
||||
"Save": "",
|
||||
"Step": "",
|
||||
"Search": "",
|
||||
"Import": "",
|
||||
"Print": "",
|
||||
"Settings": "",
|
||||
"or": "",
|
||||
"and": "",
|
||||
"Information": "",
|
||||
"Download": "",
|
||||
"Create": "",
|
||||
"Servings": "Порции",
|
||||
"Waiting": "Ожидание",
|
||||
"Preparation": "Приготовление",
|
||||
"External": "Внешний",
|
||||
"Size": "Размер",
|
||||
"Files": "Файлы",
|
||||
"File": "Файл",
|
||||
"Edit": "Редактировать",
|
||||
"Image": "Изображение",
|
||||
"Delete": "Удалить",
|
||||
"Open": "Открыть",
|
||||
"Ok": "Открыть",
|
||||
"Save": "Сохранить",
|
||||
"Step": "Шаг",
|
||||
"Search": "Поиск",
|
||||
"Import": "Импорт",
|
||||
"Print": "Распечатать",
|
||||
"Settings": "Настройки",
|
||||
"or": "или",
|
||||
"and": "и",
|
||||
"Information": "Информация",
|
||||
"Download": "Загрузить",
|
||||
"Create": "Создать",
|
||||
"Advanced Search Settings": "",
|
||||
"View": "",
|
||||
"Recipes": "",
|
||||
"Move": "",
|
||||
"Merge": "",
|
||||
"Parent": "",
|
||||
"delete_confirmation": "",
|
||||
"move_confirmation": "",
|
||||
"merge_confirmation": "",
|
||||
"create_rule": "",
|
||||
"move_selection": "",
|
||||
"merge_selection": "",
|
||||
"Root": "",
|
||||
"Ignore_Shopping": "",
|
||||
"Shopping_Category": "",
|
||||
"Edit_Food": "",
|
||||
"Move_Food": "",
|
||||
"New_Food": "",
|
||||
"Hide_Food": "",
|
||||
"Food_Alias": "",
|
||||
"Unit_Alias": "",
|
||||
"Keyword_Alias": "",
|
||||
"Delete_Food": "",
|
||||
"No_ID": "",
|
||||
"Meal_Plan_Days": "",
|
||||
"merge_title": "",
|
||||
"move_title": "",
|
||||
"Food": "",
|
||||
"Recipe_Book": "",
|
||||
"del_confirmation_tree": "",
|
||||
"delete_title": "",
|
||||
"create_title": "",
|
||||
"edit_title": "",
|
||||
"Name": "",
|
||||
"Type": "",
|
||||
"Description": "",
|
||||
"Recipe": "",
|
||||
"tree_root": "",
|
||||
"Icon": "",
|
||||
"Unit": "",
|
||||
"No_Results": "",
|
||||
"New_Unit": "",
|
||||
"Create_New_Shopping Category": "",
|
||||
"Create_New_Food": "",
|
||||
"Create_New_Keyword": "",
|
||||
"Create_New_Unit": "",
|
||||
"Create_New_Meal_Type": "",
|
||||
"and_up": "",
|
||||
"Instructions": "",
|
||||
"Unrated": "",
|
||||
"Automate": "",
|
||||
"Empty": "",
|
||||
"Key_Ctrl": "",
|
||||
"Key_Shift": "",
|
||||
"Time": "",
|
||||
"Text": "",
|
||||
"Shopping_list": "",
|
||||
"Create_Meal_Plan_Entry": "",
|
||||
"Edit_Meal_Plan_Entry": "",
|
||||
"Title": "",
|
||||
"Week": "",
|
||||
"Month": "",
|
||||
"Year": "",
|
||||
"Planner": "",
|
||||
"Planner_Settings": "",
|
||||
"Period": "",
|
||||
"Plan_Period_To_Show": "",
|
||||
"Periods": "",
|
||||
"Plan_Show_How_Many_Periods": "",
|
||||
"Starting_Day": "",
|
||||
"Meal_Types": "",
|
||||
"Meal_Type": "",
|
||||
"Clone": "",
|
||||
"Drag_Here_To_Delete": "",
|
||||
"Meal_Type_Required": "",
|
||||
"Title_or_Recipe_Required": "",
|
||||
"Color": "",
|
||||
"New_Meal_Type": "",
|
||||
"View": "Просмотр",
|
||||
"Recipes": "Рецепты",
|
||||
"Move": "Переместить",
|
||||
"Merge": "Объединить",
|
||||
"Parent": "Родитель",
|
||||
"delete_confirmation": "Вы уверены что хотите удалить {source}?",
|
||||
"move_confirmation": "Переместить <i>{child}</i> к родителю <i>{parent}</i>",
|
||||
"merge_confirmation": "Заменить <i>{source}</i> с <i>{target}</i>",
|
||||
"create_rule": "и создать автоматически",
|
||||
"move_selection": "Выбрать родителя {type} для перемещения в {source} .",
|
||||
"merge_selection": "Замените все вхождения {source} выбранным {type}.",
|
||||
"Root": "Корневой элемент",
|
||||
"Ignore_Shopping": "Игнорировать Покупки",
|
||||
"Shopping_Category": "Категория покупок",
|
||||
"Edit_Food": "Редактировать еду",
|
||||
"Move_Food": "Переместить еду",
|
||||
"New_Food": "Новая еда",
|
||||
"Hide_Food": "Скрыть еду",
|
||||
"Food_Alias": "Наименование еды",
|
||||
"Unit_Alias": "Единицы измерения",
|
||||
"Keyword_Alias": "Ключевые слова",
|
||||
"Delete_Food": "Удалить элемент",
|
||||
"No_ID": "ID не найден, удаление не возможно.",
|
||||
"Meal_Plan_Days": "Планы питания на будущее",
|
||||
"merge_title": "Объединить {type}",
|
||||
"move_title": "Переместить {type}",
|
||||
"Food": "Еда",
|
||||
"Recipe_Book": "Книга рецептов",
|
||||
"del_confirmation_tree": "Вы уверены что хотите удалить {source} и все его элементы?",
|
||||
"delete_title": "Удалить {type}",
|
||||
"create_title": "Новый {type}",
|
||||
"edit_title": "Редактировать {type}",
|
||||
"Name": "Наименование",
|
||||
"Type": "Тип",
|
||||
"Description": "Описание",
|
||||
"Recipe": "Рецепт",
|
||||
"tree_root": "Главный элемент",
|
||||
"Icon": "Иконка",
|
||||
"Unit": "Единица измерения",
|
||||
"No_Results": "Результаты отсутствуют",
|
||||
"New_Unit": "Новая единица",
|
||||
"Create_New_Shopping Category": "Создание новой категории покупок",
|
||||
"Create_New_Food": "Добавить новую еду",
|
||||
"Create_New_Keyword": "Добавить ключевое слово",
|
||||
"Create_New_Unit": "Добавить единицу измерения",
|
||||
"Create_New_Meal_Type": "Добавить тип еды",
|
||||
"and_up": "Вверх",
|
||||
"Instructions": "Инструкции",
|
||||
"Unrated": "Без рейтинга",
|
||||
"Automate": "Автоматизировать",
|
||||
"Empty": "Пустой",
|
||||
"Key_Ctrl": "Ctrl",
|
||||
"Key_Shift": "Shift",
|
||||
"Time": "Время",
|
||||
"Text": "Текст",
|
||||
"Shopping_list": "Лист покупок",
|
||||
"Create_Meal_Plan_Entry": "Создать плана питания",
|
||||
"Edit_Meal_Plan_Entry": "Редактировать план питания",
|
||||
"Title": "Заголовок",
|
||||
"Week": "Неделя",
|
||||
"Month": "Месяц",
|
||||
"Year": "Год",
|
||||
"Planner": "Планировщик",
|
||||
"Planner_Settings": "Настройки Планировщика",
|
||||
"Period": "Период",
|
||||
"Plan_Period_To_Show": "Показать недели, месяца или годы",
|
||||
"Periods": "Периоды",
|
||||
"Plan_Show_How_Many_Periods": "Сколько периодов показать",
|
||||
"Starting_Day": "Начальный день недели",
|
||||
"Meal_Types": "Типы питания",
|
||||
"Meal_Type": "Тип питания",
|
||||
"Clone": "Клонировать",
|
||||
"Drag_Here_To_Delete": "Переместить для удаления",
|
||||
"Meal_Type_Required": "Тип питания обязателен",
|
||||
"Title_or_Recipe_Required": "Требуется выбор названия или рецепта",
|
||||
"Color": "Цвет",
|
||||
"New_Meal_Type": "Новый тип питания",
|
||||
"Week_Numbers": "",
|
||||
"Show_Week_Numbers": "",
|
||||
"Export_As_ICal": "",
|
||||
@ -199,5 +199,24 @@
|
||||
"Previous_Period": "",
|
||||
"Current_Period": "",
|
||||
"Next_Day": "",
|
||||
"Previous_Day": ""
|
||||
"Previous_Day": "",
|
||||
"Add_nutrition_recipe": "Добавьте питательные вещества в рецепт",
|
||||
"and_down": "Вниз",
|
||||
"Added_by": "Добавлено",
|
||||
"Added_on": "Добавлено на",
|
||||
"AddToShopping": "Добавить в лист покупок",
|
||||
"IngredientInShopping": "Этот ингредиент в вашем списке покупок.",
|
||||
"OnHand": "В Наличии",
|
||||
"FoodOnHand": "{food} у вас в наличии.",
|
||||
"FoodNotOnHand": "{food} отсутствует в наличии.",
|
||||
"Undefined": "Неизвестно",
|
||||
"AddFoodToShopping": "Добавить {food} в ваш список покупок",
|
||||
"success_moving_resource": "Успешное перемещение ресурса!",
|
||||
"success_merging_resource": "Ресурс успешно присоединен!",
|
||||
"Shopping_Categories": "Категории покупок",
|
||||
"Search Settings": "Искать настройки",
|
||||
"err_merging_resource": "Произошла ошибка при перемещении ресурса!",
|
||||
"Remove_nutrition_recipe": "Уберите питательные вещества из рецепта",
|
||||
"err_moving_resource": "Ошибка при перемещении ресурса!",
|
||||
"NotInShopping": "{food} отсутствует в вашем списке покупок."
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
"External_Recipe_Image": "Extern receptbild",
|
||||
"Add_to_Book": "Lägg till i kokbok",
|
||||
"Add_to_Shopping": "Lägg till i inköpslista",
|
||||
"Add_to_Plan": "Lägg till i matsedel",
|
||||
"Add_to_Plan": "Lägg till i måltidsplan",
|
||||
"Step_start_time": "Steg starttid",
|
||||
"Select_Book": "Välj kokbok",
|
||||
"Recipe_Image": "Receptbild",
|
||||
@ -128,7 +128,7 @@
|
||||
"Disable_Amount": "Inaktivera belopp",
|
||||
"move_title": "Flytta {type}",
|
||||
"merge_title": "Slå samman {type}",
|
||||
"Food": "Mat",
|
||||
"Food": "Livsmedel",
|
||||
"Key_Shift": "Shift",
|
||||
"Instructions": "Instruktioner",
|
||||
"and_down": "& up",
|
||||
@ -217,5 +217,168 @@
|
||||
"Meal_Plan_Days": "Framtida måltidsplaner",
|
||||
"Automate": "Automatisera",
|
||||
"Shopping_Categories": "Shopping kategorier",
|
||||
"Unit_Alias": "Enhetsalias"
|
||||
"Unit_Alias": "Enhetsalias",
|
||||
"search_import_help_text": "Importera ett recept från en extern webbplats eller applikation.",
|
||||
"warning_duplicate_filter": "Varning: På grund av tekniska begränsningar kan flera filter av samma kombination (och/eller/inte) ge oväntade resultat.",
|
||||
"merge_confirmation": "Ersätt <i>{source}</i> med <i>{target}</i>",
|
||||
"Unrated": "Ej betygsatt",
|
||||
"New_Cookbook": "Ny kokbok",
|
||||
"Hide_Keyword": "Dölj nyckelord",
|
||||
"Clear": "Rensa",
|
||||
"Coming_Soon": "Kommer snart",
|
||||
"Shopping_list": "Inköpslista",
|
||||
"Added_on": "Tillagd på",
|
||||
"AddToShopping": "Lägg till i inköpslista",
|
||||
"IngredientInShopping": "Denna ingrediens finns i din inköpslista.",
|
||||
"NotInShopping": "{food} finns inte i din inköpslista.",
|
||||
"OnHand": "För närvarande till hands",
|
||||
"FoodOnHand": "Du har {food} hemma.",
|
||||
"FoodNotOnHand": "Du har inte {food} hemma.",
|
||||
"Planner": "Planerare",
|
||||
"Planner_Settings": "Planerare inställningar",
|
||||
"IgnoredFood": "{food} är inställd på att ignorera inköp.",
|
||||
"Add_Servings_to_Shopping": "Lägg till {servings} portioner till inköp",
|
||||
"Cannot_Add_Notes_To_Shopping": "Anteckningar kan inte läggas till inköpslistan",
|
||||
"Added_To_Shopping_List": "Lades till i inköpslistan",
|
||||
"Shopping_List_Empty": "Din inköpslista är för närvarande tom, du kan lägga till varor via snabbmenyn för en måltidsplan (högerklicka på kortet eller vänsterklicka på menyikonen)",
|
||||
"ShowUncategorizedFood": "Visa odefinierad",
|
||||
"MoveCategory": "Flytta till: ",
|
||||
"CountMore": "...+{count} fler",
|
||||
"IgnoreThis": "Lägg aldrig till {mat} automatiskt i inköpslista",
|
||||
"DelayFor": "Fördröjning på {hours} timmar",
|
||||
"ShowDelayed": "Visa fördröjda artiklar",
|
||||
"Completed": "Avslutad",
|
||||
"OfflineAlert": "Du är offline, inköpslistan kanske inte synkroniseras.",
|
||||
"shopping_share": "Dela inköpslista",
|
||||
"shopping_auto_sync": "Autosynk",
|
||||
"mealplan_autoinclude_related": "Lägg till relaterade recept",
|
||||
"default_delay": "Standardfördröjningstimmar",
|
||||
"err_move_self": "Kan inte flytta objektet till sig självt",
|
||||
"nothing": "Inget att göra",
|
||||
"err_merge_self": "Kan inte slå samman objektet med sig självt",
|
||||
"show_sql": "Visa SQL",
|
||||
"CategoryName": "Kategorinamn",
|
||||
"SupermarketName": "Mataffärens namn",
|
||||
"filter_to_supermarket": "Filter till mataffär",
|
||||
"download_pdf": "Ladda ner PDF",
|
||||
"download_csv": "Ladda ner CSV",
|
||||
"csv_delim_help": "Avgränsare att använda för CSV-export.",
|
||||
"csv_delim_label": "CSV-avgränsare",
|
||||
"SuccessClipboard": "Inköpslista kopierad till urklipp",
|
||||
"copy_to_clipboard": "Kopiera till urklipp",
|
||||
"csv_prefix_label": "Listprefix",
|
||||
"copy_markdown_table": "Kopiera som Markdown-tabell",
|
||||
"in_shopping": "I inköpslistan",
|
||||
"DelayUntil": "Fördröjning till",
|
||||
"enable_expert": "Aktivera expertläge",
|
||||
"expert_mode": "Expertläge",
|
||||
"simple_mode": "Enkelt läge",
|
||||
"advanced": "Avancerat",
|
||||
"fields": "Fält",
|
||||
"show_keywords": "Visa nyckelord",
|
||||
"show_books": "Visa böcker",
|
||||
"show_rating": "Visa betyg",
|
||||
"show_filters": "Visa filter",
|
||||
"not": "inte",
|
||||
"save_filter": "Spara filter",
|
||||
"filter_name": "Filternamn",
|
||||
"left_handed": "Vänsterhänt läge",
|
||||
"left_handed_help": "Kommer att optimera användargränssnittet för användning med din vänstra hand.",
|
||||
"Custom Filter": "Anpassat filter",
|
||||
"shared_with": "Delad med",
|
||||
"copy_to_new": "Kopiera till nytt recept",
|
||||
"recipe_name": "Receptnamn",
|
||||
"paste_ingredients_placeholder": "Klistra in ingredienslistan här...",
|
||||
"paste_ingredients": "Klistra in ingredienser",
|
||||
"ingredient_list": "Ingredienslista",
|
||||
"explain": "Förklara",
|
||||
"filter": "Filter",
|
||||
"search_no_recipes": "Hittade inga recept!",
|
||||
"search_create_help_text": "Skapa ett nytt recept direkt i Tandoor.",
|
||||
"substitute_help": "Ersättningar övervägs när man söker efter recept som kan göras med tillgängliga ingredienser.",
|
||||
"sort_by": "Sortera efter",
|
||||
"asc": "Stigande",
|
||||
"desc": "Fallande",
|
||||
"date_viewed": "Senast visad",
|
||||
"last_cooked": "Senast tillagad",
|
||||
"date_created": "Skapat datum",
|
||||
"show_sortby": "Visa sortera efter",
|
||||
"search_rank": "Sök rank",
|
||||
"make_now": "Gör nu",
|
||||
"recipe_filter": "Receptfilter",
|
||||
"created_on": "Skapat den",
|
||||
"updatedon": "Uppdaterad den",
|
||||
"advanced_search_settings": "Avancerade sökinställningar",
|
||||
"nothing_planned_today": "Du har ingenting planerat för idag!",
|
||||
"Planned": "Planerad",
|
||||
"Quick actions": "Snabba åtgärder",
|
||||
"Ratings": "Betyg",
|
||||
"Internal": "Intern",
|
||||
"Random Recipes": "Slumpmässiga recept",
|
||||
"parameter_count": "Parameter {count}",
|
||||
"ignore_shopping_help": "Lägg aldrig till ingrediens på inköpslistan (t.ex. vatten)",
|
||||
"review_shopping": "Granska inköpsposter innan du sparar",
|
||||
"view_recipe": "Visa recept",
|
||||
"del_confirmation_tree": "Är du säker på att du vill ta bort {source} och alla dess underordnade?",
|
||||
"today_recipes": "Dagens recept",
|
||||
"move_confirmation": "Flytta<i>{child}</i> till förälder <i>{parent}</i>",
|
||||
"create_shopping_new": "Lägg till i ny inköpslista",
|
||||
"csv_prefix_help": "Prefix att lägga till när listan kopieras till urklipp.",
|
||||
"show_units": "Visa enheter",
|
||||
"remember_search": "Kom ihåg sökning",
|
||||
"sql_debug": "SQL felsökning",
|
||||
"Create_New_Food": "Lägg till nytt livsmedel",
|
||||
"Pin": "Pin",
|
||||
"Edit_Food": "Redigera livsmedel",
|
||||
"Move_Food": "Flytta livsmedel",
|
||||
"Create_Meal_Plan_Entry": "Skapa en måltidsplan",
|
||||
"Edit_Meal_Plan_Entry": "Redigera matplansinlägg",
|
||||
"FoodInherit": "Ärftliga livsmedels fält",
|
||||
"SupermarketCategoriesOnly": "Endast mataffärskategorier",
|
||||
"InheritWarning": "{food} är inställd på att ärva, ändringar kanske inte kvarstår.",
|
||||
"mealplan_autoadd_shopping": "Lägg till måltidsplan automatiskt",
|
||||
"mealplan_autoexclude_onhand": "Uteslut livsmedel till hands",
|
||||
"shopping_share_desc": "Användare kommer att se alla varor du lägger till i din inköpslista. De måste lägga till dig för att se objekt på sin lista.",
|
||||
"shopping_auto_sync_desc": "Inställning satt till 0 inaktiverar automatisk synkronisering. När du tittar på en inköpslista uppdateras listan varje sekund för att synkronisera ändringar som någon annan kan ha gjort. Användbart när du handlar med flera personer men kommer att använda mobildata.",
|
||||
"mealplan_autoadd_shopping_desc": "Lägg automatiskt till måltidsplanens ingredienser till inköpslistan.",
|
||||
"mealplan_autoexclude_onhand_desc": "När du lägger till en måltidsplan till inköpslistan (manuellt eller automatiskt), uteslut ingredienser som för närvarande finns till hands.",
|
||||
"mealplan_autoinclude_related_desc": "När du lägger till en måltidsplan till inköpslistan (manuellt eller automatiskt), inkludera alla relaterade recept.",
|
||||
"default_delay_desc": "Förinställt antal timmar för att fördröja en inköpslista.",
|
||||
"filter_to_supermarket_desc": "Filtrera inköpslistan som standard så att den endast inkluderar kategorier för utvalda mataffärer.",
|
||||
"CategoryInstruction": "Dra kategorier för att ändra den ordning som kategorierna visas i inköpslistan.",
|
||||
"shopping_recent_days_desc": "Dagar av senaste inköpslistorna att visa.",
|
||||
"shopping_recent_days": "Senaste dagarna",
|
||||
"Foods": "Livsmedel",
|
||||
"show_foods": "Visa livsmedel",
|
||||
"times_cooked": "Antal gånger som tillagats",
|
||||
"book_filter_help": "Inkludera recept från receptfilter utöver de manuellt tilldelade.",
|
||||
"reset_children_help": "Skriv över alla underordnade med värden från ärvda fält. Ärvda fält av underordnade fält kommer att ställas in på ärvda fält om inte ärvda fält för underordnade är inställda.",
|
||||
"reset_children": "Återställ underordnades arv",
|
||||
"substitute_siblings_help": "All mat som delar en överordnad till detta livsmedel anses vara substitut.",
|
||||
"substitute_children_help": "All mat som är underordnat till detta livsmedel anses vara substitut.",
|
||||
"substitute_siblings": "Ersättande syskon",
|
||||
"substitute_children": "Ersättande underordnade",
|
||||
"SubstituteOnHand": "Du har ett substitut till hands.",
|
||||
"ChildInheritFields": "Underordnade ärver fält",
|
||||
"ChildInheritFields_help": "Underordnade kommer att ärva dessa fält som standard.",
|
||||
"InheritFields_help": "Värdena i dessa fält kommer att ärvas från förälder (Undantag: tomma shoppingkategorier ärvs inte)",
|
||||
"no_pinned_recipes": "Du har inga nålade recept!",
|
||||
"Pinned": "Nålad",
|
||||
"OnHand_help": "Livsmedel som finns i lager kommer inte automatiskt att läggas till på en inköpslista. Onhand-status delas med shoppinganvändare.",
|
||||
"shopping_category_help": "Mataffärer kan sorteras och filtreras efter Shopping-kategori enligt gångarnas layout.",
|
||||
"food_recipe_help": "Om du länkar ett recept här kommer det länkade receptet att inkluderas i alla andra recept som använder detta livsmedel",
|
||||
"New_Food": "Nytt livsmedel",
|
||||
"Hide_Food": "Dölj livsmedel",
|
||||
"Food_Alias": "Alias för livsmedel",
|
||||
"Delete_Food": "Ta bort livsmedel",
|
||||
"mark_complete": "Markera som färdig",
|
||||
"QuickEntry": "Snabbt inlägg",
|
||||
"shopping_add_onhand_desc": "Markera livsmedlet som \"till hands\" när den blir avbockad i inköpslistan.",
|
||||
"shopping_add_onhand": "Automatisk Till hands",
|
||||
"related_recipes": "Relaterade recept",
|
||||
"Create Food": "Skapa livsmedel",
|
||||
"create_food_desc": "Skapa ett livsmedel och länka det till det här receptet.",
|
||||
"additional_options": "Ytterligare alternativ",
|
||||
"remember_hours": "Timmar att komma ihåg",
|
||||
"tree_select": "Använd trädval"
|
||||
}
|
||||
|
@ -612,6 +612,18 @@ export class Models {
|
||||
list: {
|
||||
params: ["filter_list"],
|
||||
},
|
||||
create: {
|
||||
params: [["name",]],
|
||||
form: {
|
||||
name: {
|
||||
form_field: true,
|
||||
type: "text",
|
||||
field: "name",
|
||||
label: "Name",
|
||||
placeholder: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static MEAL_PLAN = {
|
||||
|
@ -46,6 +46,7 @@ export class StandardToasts {
|
||||
static FAIL_FETCH = "FAIL_FETCH"
|
||||
static FAIL_UPDATE = "FAIL_UPDATE"
|
||||
static FAIL_DELETE = "FAIL_DELETE"
|
||||
static FAIL_DELETE_PROTECTED = "FAIL_DELETE_PROTECTED"
|
||||
static FAIL_MOVE = "FAIL_MOVE"
|
||||
static FAIL_MERGE = "FAIL_MERGE"
|
||||
|
||||
@ -81,6 +82,9 @@ export class StandardToasts {
|
||||
case StandardToasts.FAIL_DELETE:
|
||||
makeToast(i18n.tc("Failure"), i18n.tc("err_deleting_resource") + (err_details ? "\n" + err_details : ""), "danger")
|
||||
break
|
||||
case StandardToasts.FAIL_DELETE_PROTECTED:
|
||||
makeToast(i18n.tc("Protected"), i18n.tc("err_deleting_protected_resource"), "danger")
|
||||
break
|
||||
case StandardToasts.FAIL_MOVE:
|
||||
makeToast(i18n.tc("Failure"), i18n.tc("err_moving_resource") + (err_details ? "\n" + err_details : ""), "danger")
|
||||
break
|
||||
@ -187,6 +191,10 @@ export function calculateAmount(amount, factor) {
|
||||
let return_string = ""
|
||||
let fraction = frac(amount * factor, 10, true)
|
||||
|
||||
if (fraction[0] === 0 && fraction[1] === 0 && fraction[2] === 1) {
|
||||
return roundDecimals(amount * factor)
|
||||
}
|
||||
|
||||
if (fraction[0] > 0) {
|
||||
return_string += fraction[0]
|
||||
}
|
||||
|
@ -49,6 +49,10 @@ const pages = {
|
||||
entry: "./src/apps/MealPlanView/main.js",
|
||||
chunks: ["chunk-vendors"],
|
||||
},
|
||||
ingredient_editor_view: {
|
||||
entry: "./src/apps/IngredientEditorView/main.js",
|
||||
chunks: ["chunk-vendors"],
|
||||
},
|
||||
shopping_list_view: {
|
||||
entry: "./src/apps/ShoppingListView/main.js",
|
||||
chunks: ["chunk-vendors"],
|
||||
|
Loading…
Reference in New Issue
Block a user