Merge branch 'TandoorRecipes:develop' into Auto-Planner

This commit is contained in:
AquaticLava
2023-01-05 16:27:27 -07:00
committed by GitHub
141 changed files with 28710 additions and 13837 deletions

View File

@@ -8,6 +8,7 @@
<select class="form-control" v-model="recipe_app">
<option value="DEFAULT">Default</option>
<option value="SAFFRON">Saffron</option>
<option value="NEXTCLOUD">Nextcloud Cookbook</option>
<option value="RECIPESAGE">Recipe Sage</option>
<option value="PDF">PDF (experimental)</option>
</select>

View File

@@ -204,7 +204,7 @@
v-if="!import_multiple">
<recipe-card :recipe="recipe_json" :detailed="false"
:show_context_menu="false"
:show_context_menu="false" :use_plural="use_plural"
></recipe-card>
</b-col>
<b-col>
@@ -461,6 +461,7 @@ export default {
recent_urls: [],
source_data: '',
recipe_json: undefined,
use_plural: false,
// recipe_html: undefined,
// recipe_tree: undefined,
recipe_images: [],
@@ -490,6 +491,10 @@ export default {
this.INTEGRATIONS.forEach((int) => {
int.icon = this.getRandomFoodIcon()
})
let apiClient = new ApiApiFactory()
apiClient.retrieveSpace(window.ACTIVE_SPACE_ID).then(r => {
this.use_plural = r.data.use_plural
})
},
methods: {
/**

View File

@@ -1,148 +1,182 @@
<template>
<div v-if="recipe_json !== undefined" class="mt-2 mt-md-0">
<h5>Steps</h5>
<div class="row">
<div class="col col-md-12 text-center">
<b-button @click="splitAllSteps('\n')" variant="secondary" v-b-tooltip.hover :title="$t('Split_All_Steps')"><i
class="fas fa-expand-arrows-alt"></i> {{ $t('All') }}
</b-button>
<b-button @click="mergeAllSteps()" variant="primary" class="ml-1" v-b-tooltip.hover :title="$t('Combine_All_Steps')"><i
class="fas fa-compress-arrows-alt"></i> {{ $t('All') }}
</b-button>
</div>
</div>
<div class="row mt-2" v-for="(s, index) in recipe_json.steps"
v-bind:key="index">
<div class="col col-md-4 d-none d-md-block">
<draggable :list="s.ingredients" group="ingredients"
:empty-insert-threshold="10">
<b-list-group-item v-for="i in s.ingredients"
v-bind:key="i.original_text"><i
class="fas fa-arrows-alt"></i> {{ i.original_text }}
</b-list-group-item>
</draggable>
</div>
<div class="col col-md-8 col-12">
<b-input-group>
<b-textarea
style="white-space: pre-wrap" v-model="s.instruction"
max-rows="10"></b-textarea>
<b-input-group-append>
<b-button variant="secondary" @click="splitStep(s,'\n')"><i
class="fas fa-expand-arrows-alt"></i></b-button>
<b-button variant="danger"
@click="recipe_json.steps.splice(recipe_json.steps.findIndex(x => x === s),1)">
<i class="fas fa-trash-alt"></i>
</b-button>
</b-input-group-append>
</b-input-group>
<div class="text-center mt-1">
<b-button @click="mergeStep(s)" variant="primary"
v-if="index + 1 < recipe_json.steps.length"><i
class="fas fa-compress-arrows-alt"></i>
</b-button>
<b-button variant="success"
@click="recipe_json.steps.splice(recipe_json.steps.findIndex(x => x === s) +1,0,{ingredients:[], instruction: ''})">
<i class="fas fa-plus"></i>
</b-button>
</div>
</div>
</div>
<div v-if="recipe_json !== undefined" class="mt-2 mt-md-0">
<h5>Steps</h5>
<div class="row">
<div class="col col-md-12 text-center">
<b-button @click="autoSortIngredients()" variant="secondary" v-b-tooltip.hover v-if="recipe_json.steps.length > 1"
:title="$t('Auto_Sort_Help')"><i class="fas fa-random"></i> {{ $t('Auto_Sort') }}
</b-button>
<b-button @click="splitAllSteps('\n')" variant="secondary" class="ml-1" v-b-tooltip.hover
:title="$t('Split_All_Steps')"><i
class="fas fa-expand-arrows-alt"></i> {{ $t('All') }}
</b-button>
<b-button @click="mergeAllSteps()" variant="primary" class="ml-1" v-b-tooltip.hover
:title="$t('Combine_All_Steps')"><i
class="fas fa-compress-arrows-alt"></i> {{ $t('All') }}
</b-button>
</div>
</div>
<div class="row mt-2" v-for="(s, index) in recipe_json.steps"
v-bind:key="index">
<div class="col col-md-4 d-none d-md-block">
<draggable :list="s.ingredients" group="ingredients"
:empty-insert-threshold="10">
<b-list-group-item v-for="i in s.ingredients"
v-bind:key="i.original_text"><i
class="fas fa-arrows-alt"></i> {{ i.original_text }}
</b-list-group-item>
</draggable>
</div>
<div class="col col-md-8 col-12">
<b-input-group>
<b-textarea
style="white-space: pre-wrap" v-model="s.instruction"
max-rows="10"></b-textarea>
<b-input-group-append>
<b-button variant="secondary" @click="splitStep(s,'\n')"><i
class="fas fa-expand-arrows-alt"></i></b-button>
<b-button variant="danger"
@click="recipe_json.steps.splice(recipe_json.steps.findIndex(x => x === s),1)">
<i class="fas fa-trash-alt"></i>
</b-button>
</b-input-group-append>
</b-input-group>
<div class="text-center mt-1">
<b-button @click="mergeStep(s)" variant="primary"
v-if="index + 1 < recipe_json.steps.length"><i
class="fas fa-compress-arrows-alt"></i>
</b-button>
<b-button variant="success"
@click="recipe_json.steps.splice(recipe_json.steps.findIndex(x => x === s) +1,0,{ingredients:[], instruction: ''})">
<i class="fas fa-plus"></i>
</b-button>
</div>
</div>
</div>
</div>
</template>
<script>
import draggable from "vuedraggable";
import stringSimilarity from "string-similarity"
export default {
name: "ImportViewStepEditor",
components: {
draggable
},
props: {
recipe: undefined
},
data() {
return {
recipe_json: undefined
}
},
watch: {
recipe_json: function () {
this.$emit('change', this.recipe_json)
},
},
mounted() {
this.recipe_json = this.recipe
},
methods: {
/**
* utility function used by splitAllSteps and splitStep to split a single step object into multiple step objects
* @param step: single step
* @param split_character: character to split steps at
* @return array of step objects
*/
splitStepObject: function (step, split_character) {
let steps = []
step.instruction.split(split_character).forEach(part => {
if (part.trim() !== '') {
steps.push({'instruction': part, 'ingredients': []})
}
})
steps[0].ingredients = step.ingredients // put all ingredients from the original step in the ingredients of the first step of the split step list
return steps
},
/**
* Splits all steps of a given recipe_json at the split character (e.g. \n or \n\n)
* @param split_character: character to split steps at
*/
splitAllSteps: function (split_character) {
let steps = []
this.recipe_json.steps.forEach(step => {
steps = steps.concat(this.splitStepObject(step, split_character))
})
this.recipe_json.steps = steps
},
/**
* Splits the given step at the split character (e.g. \n or \n\n)
* @param step: step ingredients to split
* @param split_character: character to split steps at
*/
splitStep: function (step, split_character) {
let old_index = this.recipe_json.steps.findIndex(x => x === step)
let new_steps = this.splitStepObject(step, split_character)
this.recipe_json.steps.splice(old_index, 1, ...new_steps)
},
/**
* Merge all steps of a given recipe_json into one
*/
mergeAllSteps: function () {
let step = {'instruction': '', 'ingredients': []}
this.recipe_json.steps.forEach(s => {
step.instruction += s.instruction + '\n'
step.ingredients = step.ingredients.concat(s.ingredients)
})
this.recipe_json.steps = [step]
},
/**
* Merge two steps (the given and next one)
*/
mergeStep: function (step) {
let step_index = this.recipe_json.steps.findIndex(x => x === step)
let removed_steps = this.recipe_json.steps.splice(step_index, 2)
this.recipe_json.steps.splice(step_index, 0, {
'instruction': removed_steps.flatMap(x => x.instruction).join('\n'),
'ingredients': removed_steps.flatMap(x => x.ingredients)
})
},
name: "ImportViewStepEditor",
components: {
draggable
},
props: {
recipe: undefined
},
data() {
return {
recipe_json: undefined
}
},
watch: {
recipe_json: function () {
this.$emit('change', this.recipe_json)
},
},
mounted() {
this.recipe_json = this.recipe
},
methods: {
/**
* utility function used by splitAllSteps and splitStep to split a single step object into multiple step objects
* @param step: single step
* @param split_character: character to split steps at
* @return array of step objects
*/
splitStepObject: function (step, split_character) {
let steps = []
step.instruction.split(split_character).forEach(part => {
if (part.trim() !== '') {
steps.push({'instruction': part, 'ingredients': []})
}
})
steps[0].ingredients = step.ingredients // put all ingredients from the original step in the ingredients of the first step of the split step list
return steps
},
/**
* Splits all steps of a given recipe_json at the split character (e.g. \n or \n\n)
* @param split_character: character to split steps at
*/
splitAllSteps: function (split_character) {
let steps = []
this.recipe_json.steps.forEach(step => {
steps = steps.concat(this.splitStepObject(step, split_character))
})
this.recipe_json.steps = steps
},
/**
* Splits the given step at the split character (e.g. \n or \n\n)
* @param step: step ingredients to split
* @param split_character: character to split steps at
*/
splitStep: function (step, split_character) {
let old_index = this.recipe_json.steps.findIndex(x => x === step)
let new_steps = this.splitStepObject(step, split_character)
this.recipe_json.steps.splice(old_index, 1, ...new_steps)
},
/**
* Merge all steps of a given recipe_json into one
*/
mergeAllSteps: function () {
let step = {'instruction': '', 'ingredients': []}
this.recipe_json.steps.forEach(s => {
step.instruction += s.instruction + '\n'
step.ingredients = step.ingredients.concat(s.ingredients)
})
this.recipe_json.steps = [step]
},
/**
* Merge two steps (the given and next one)
*/
mergeStep: function (step) {
let step_index = this.recipe_json.steps.findIndex(x => x === step)
let removed_steps = this.recipe_json.steps.splice(step_index, 2)
this.recipe_json.steps.splice(step_index, 0, {
'instruction': removed_steps.flatMap(x => x.instruction).join('\n'),
'ingredients': removed_steps.flatMap(x => x.ingredients)
})
},
/**
* automatically assign ingredients to steps based on text matching
*/
autoSortIngredients: function () {
let ingredients = this.recipe_json.steps.flatMap(s => s.ingredients)
this.recipe_json.steps.forEach(s => s.ingredients = [])
ingredients.forEach(i => {
let found = false
this.recipe_json.steps.forEach(s => {
if (s.instruction.includes(i.food.name.trim()) && !found) {
found = true
s.ingredients.push(i)
}
})
if (!found) {
let best_match = {rating: 0, step: this.recipe_json.steps[0]}
this.recipe_json.steps.forEach(s => {
let match = stringSimilarity.findBestMatch(i.food.name.trim(), s.instruction.split(' '))
if (match.bestMatch.rating > best_match.rating) {
best_match = {rating: match.bestMatch.rating, step: s}
}
})
best_match.step.ingredients.push(i)
found = true
}
})
}
}
}
</script>

View File

@@ -23,8 +23,7 @@
<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
>
</b-button> </span >
<!-- TODO add proper field to model config to determine if create should be available or not -->
</h3>
</div>
@@ -42,6 +41,7 @@
<!-- 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"
:use_plural="use_plural"
:model="this_model" @item-action="startAction($event, 'left')"
@finish-action="finishAction"/>
</div>
@@ -51,6 +51,7 @@
<template v-slot:cards>
<generic-horizontal-card v-for="i in items_left" v-bind:key="i.id" :item="i"
:model="this_model"
:use_plural="use_plural"
@item-action="startAction($event, 'left')"
@finish-action="finishAction"/>
</template>
@@ -62,6 +63,7 @@
<template v-slot:cards>
<generic-horizontal-card v-for="i in items_right" v-bind:key="i.id" :item="i"
:model="this_model"
:use_plural="use_plural"
@item-action="startAction($event, 'right')"
@finish-action="finishAction"/>
</template>
@@ -120,6 +122,7 @@ export default {
show_split: false,
paginated: false,
header_component_name: undefined,
use_plural: false,
}
},
computed: {
@@ -145,6 +148,17 @@ export default {
}
})
this.$i18n.locale = window.CUSTOM_LOCALE
let apiClient = new ApiApiFactory()
apiClient.retrieveSpace(window.ACTIVE_SPACE_ID).then(r => {
this.use_plural = r.data.use_plural
if (!this.use_plural && this.this_model !== null && this.this_model.create.params[0] !== null && this.this_model.create.params[0].includes('plural_name')) {
let index = this.this_model.create.params[0].indexOf('plural_name')
if (index > -1){
this.this_model.create.params[0].splice(index, 1)
}
delete this.this_model.create.form.plural_name
}
})
},
methods: {
// this.genericAPI inherited from ApiMixin

View File

@@ -308,7 +308,7 @@
size="sm"
class="ml-1 mb-1 mb-md-0"
@click="
paste_step = step.id
paste_step = step
$bvModal.show('id_modal_paste_ingredients')
"
>
@@ -546,7 +546,7 @@
<button type="button" class="dropdown-item"
v-if="!ingredient.is_header"
@click="ingredient.is_header = true">
@click="ingredient.is_header = true; ingredient.food=null; ingredient.amount=0; ingredient.unit=null">
<i class="fas fa-heading fa-fw"></i>
{{ $t("Make_Header") }}
</button>
@@ -571,11 +571,59 @@
<i class="fas fa-balance-scale-right fa-fw"></i>
{{ $t("Enable_Amount") }}
</button>
<template v-if="use_plural">
<button type="button" class="dropdown-item"
v-if="!ingredient.always_use_plural_unit"
@click="ingredient.always_use_plural_unit = true">
<i class="fas fa-filter fa-fw"></i>
{{ $t("Use_Plural_Unit_Always") }}
</button>
<button type="button" class="dropdown-item"
v-if="ingredient.always_use_plural_unit"
@click="ingredient.always_use_plural_unit = false">
<i class="fas fa-filter fa-fw"></i>
{{ $t("Use_Plural_Unit_Simple") }}
</button>
<button type="button" class="dropdown-item"
v-if="!ingredient.always_use_plural_food"
@click="ingredient.always_use_plural_food = true">
<i class="fas fa-filter fa-fw"></i>
{{ $t("Use_Plural_Food_Always") }}
</button>
<button type="button" class="dropdown-item"
v-if="ingredient.always_use_plural_food"
@click="ingredient.always_use_plural_food = false">
<i class="fas fa-filter fa-fw"></i>
{{ $t("Use_Plural_Food_Simple") }}
</button>
</template>
<button type="button" class="dropdown-item"
@click="copyTemplateReference(index, ingredient)">
<i class="fas fa-code"></i>
{{ $t("Copy_template_reference") }}
</button>
<button type="button" class="dropdown-item"
@click="duplicateIngredient(step, ingredient, index + 1)">
<i class="fas fa-copy"></i>
{{ $t("Copy") }}
</button>
<button type="button" class="dropdown-item"
v-if="index > 0"
@click="moveIngredient(step, ingredient, index-1)">
<i class="fas fa-arrow-up"></i>
{{ $t("Up") }}
</button>
<button type="button" class="dropdown-item"
v-if="index !== step.ingredients.length - 1"
@click="moveIngredient(step, ingredient, index+1)">
<i class="fas fa-arrow-down"></i>
{{ $t("Down") }}
</button>
</div>
</div>
</div>
@@ -648,17 +696,18 @@
v-if="recipe !== undefined">
<div class="col-3 col-md-6 mb-1 mb-md-0 pr-2 pl-2">
<a :href="resolveDjangoUrl('delete_recipe', recipe.id)"
class="d-block d-md-none btn btn-block btn-danger shadow-none"><i class="fa fa-trash fa-lg"></i></a>
class="d-block d-md-none btn btn-block btn-danger shadow-none btn-sm"><i
class="fa fa-trash fa-lg"></i></a>
<a :href="resolveDjangoUrl('delete_recipe', recipe.id)"
class="d-none d-md-block btn btn-block btn-danger shadow-none">{{ $t("Delete") }}</a>
class="d-none d-md-block btn btn-block btn-danger shadow-none btn-sm">{{ $t("Delete") }}</a>
</div>
<div class="col-3 col-md-6 mb-1 mb-md-0 pr-2 pl-2">
<a :href="resolveDjangoUrl('view_recipe', recipe.id)"
class="d-block d-md-none btn btn-block btn-primary shadow-none">
class="d-block d-md-none btn btn-block btn-primary shadow-none btn-sm">
<i class="fa fa-eye fa-lg"></i>
</a>
<a :href="resolveDjangoUrl('view_recipe', recipe.id)"
class="d-none d-md-block btn btn-block btn-primary shadow-none">
class="d-none d-md-block btn btn-block btn-primary shadow-none btn-sm">
{{ $t("View") }}
</a>
</div>
@@ -705,7 +754,7 @@
<b-modal
id="id_modal_paste_ingredients"
v-bind:title="$t('ingredient_list')"
@ok="appendIngredients"
@ok="appendIngredients(paste_step)"
@cancel="paste_ingredients = paste_step = undefined"
@close="paste_ingredients = paste_step = undefined"
>
@@ -775,6 +824,7 @@ export default {
paste_step: undefined,
show_file_create: false,
step_for_file_create: undefined,
use_plural: false,
additional_visible: false,
create_food: undefined,
md_editor_toolbars: {
@@ -822,6 +872,10 @@ export default {
this.searchRecipes("")
this.$i18n.locale = window.CUSTOM_LOCALE
let apiClient = new ApiApiFactory()
apiClient.retrieveSpace(window.ACTIVE_SPACE_ID).then(r => {
this.use_plural = r.data.use_plural
})
},
created() {
window.addEventListener("keydown", this.keyboardListener)
@@ -1019,6 +1073,8 @@ export default {
order: 0,
is_header: false,
no_amount: false,
always_use_plural_unit: false,
always_use_plural_food: false,
original_text: null,
})
this.sortIngredients(step)
@@ -1039,6 +1095,12 @@ export default {
this.recipe.steps.splice(new_index < 0 ? 0 : new_index, 0, step)
this.sortSteps()
},
moveIngredient: function (step, ingredient, new_index) {
step.ingredients.splice(step.ingredients.indexOf(ingredient), 1)
step.ingredients.splice(new_index < 0 ? 0 : new_index, 0, ingredient)
this.sortIngredients(step)
},
addFoodType: function (tag, index) {
let [tmp, step, id] = index.split("_")
@@ -1188,30 +1250,38 @@ export default {
energy: function () {
return energyHeading()
},
appendIngredients: function () {
appendIngredients: function (step) {
let ing_list = this.paste_ingredients.split(/\r?\n/)
let step = this.recipe.steps.findIndex((x) => x.id == this.paste_step)
let order = Math.max(...this.recipe.steps[step].ingredients.map((x) => x.order), -1) + 1
this.recipe.steps[step].ingredients_visible = true
step.ingredients_visible = true
let parsed_ing_list = []
let promises = []
ing_list.forEach((ing) => {
if (ing.trim() !== "") {
this.genericPostAPI("api_ingredient_from_string", {text: ing}).then((result) => {
promises.push(this.genericPostAPI("api_ingredient_from_string", {text: ing}).then((result) => {
let unit = null
if (result.data.unit !== "" && result.data.unit !== null) {
unit = {name: result.data.unit}
}
this.recipe.steps[step].ingredients.splice(order, 0, {
parsed_ing_list.push({
amount: result.data.amount,
unit: unit,
food: {name: result.data.food},
note: result.data.note,
original_text: ing,
})
})
order++
}))
}
})
Promise.allSettled(promises).then(() => {
ing_list.forEach(ing => {
step.ingredients.push(parsed_ing_list.find(x => x.original_text === ing))
})
})
},
duplicateIngredient: function (step, ingredient, new_index) {
delete ingredient.id
step.ingredients.splice(new_index < 0 ? 0 : new_index, 0, ingredient)
}
},
}
</script>

View File

@@ -2,11 +2,11 @@
<div id="app" style="margin-bottom: 4vh">
<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="col-12 col-xl-10 col-lg-10 offset-xl-1 offset-lg-1">
<div class="row">
<div class="col col-md-12">
<div class="row justify-content-center">
<div class="col-12 col-lg-10 col-xl-8 mt-3 mb-3">
<div class="col-12 col-lg-10 col-xl-10 mt-2">
<b-input-group>
<b-input
class="form-control form-control-lg form-control-borderless form-control-search"
@@ -16,16 +16,10 @@
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()">
<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'">
<!-- 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>
<i class="fas fa-sliders-h"></i>
</b-button>
</b-input-group-append>
</b-input-group>
@@ -799,50 +793,62 @@
</div>
</div>
<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 mt-2">
<div class="col-12 col-xl-10 col-lg-10 offset-xl-1 offset-lg-1">
<div style="overflow-x:visible; overflow-y: hidden;white-space: nowrap;">
<b-dropdown id="sortby" :text="sortByLabel" variant="outline-primary" size="sm" style="overflow-y: visible; overflow-x: visible; position: static"
class="shadow-none" toggle-class="text-decoration-none" >
<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 class="text-muted">
{{ $t("Page") }} {{
>
<span>{{ o.text }}</span>
</b-dropdown-item>
</div>
</b-dropdown>
<b-button variant="outline-primary" size="sm" class="shadow-none ml-1"
@click="resetSearch()"><i class="fas fa-file-alt"></i> {{
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>
}}/{{ Math.ceil(pagination_count / ui.page_size) }} {{ $t("Reset") }} <i
class="fas fa-times-circle"></i>
</b-button>
<b-button variant="outline-primary" size="sm" class="shadow-none ml-1"
@click="openRandom()"><i class="fas fa-dice-five"></i> {{ $t("Random") }}
</b-button>
</div>
</div>
</div>
<div v-if="recipes.length > 0">
<div v-if="recipes.length > 0" class="mt-4">
<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">
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-gap: 0.4rem">
<template v-if="!searchFiltered()">
<recipe-card
v-bind:key="`mp_${m.id}`"
v-for="m in meal_plans"
:recipe="m.recipe"
:meal_plan="m"
:use_plural="use_plural"
:footer_text="m.meal_type_name"
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>
:footer_icon="isRecentOrNew(r)[1]"
:use_plural="use_plural">
</recipe-card>
</div>
</div>
</div>
@@ -910,6 +916,7 @@ import LoadingSpinner from "@/components/LoadingSpinner" // TODO: is this deprec
import RecipeCard from "@/components/RecipeCard"
import GenericMultiselect from "@/components/GenericMultiselect"
import RecipeSwitcher from "@/components/Buttons/RecipeSwitcher"
import { ApiApiFactory } from "@/utils/openapi/api"
Vue.use(VueCookies)
Vue.use(BootstrapVue)
@@ -930,7 +937,7 @@ export default {
meal_plans: [],
last_viewed_recipes: [],
sortMenu: false,
use_plural: false,
search: {
advanced_search_visible: false,
explain_visible: false,
@@ -1115,6 +1122,9 @@ export default {
},
},
mounted() {
this.recipes = Array(this.ui.page_size).fill({loading: true})
this.$nextTick(function () {
if (this.$cookies.isKey(UI_COOKIE_NAME)) {
this.ui = Object.assign({}, this.ui, this.$cookies.get(UI_COOKIE_NAME))
@@ -1154,6 +1164,10 @@ export default {
this.loadMealPlan()
this.refreshData(false)
})
let apiClient = new ApiApiFactory()
apiClient.retrieveSpace(window.ACTIVE_SPACE_ID).then(r => {
this.use_plural = r.data.use_plural
})
this.$i18n.locale = window.CUSTOM_LOCALE
this.debug = localStorage.getItem("DEBUG") == "True" || false
},
@@ -1213,6 +1227,7 @@ export default {
// this.genericAPI inherited from ApiMixin
refreshData: _debounce(function (random) {
this.recipes_loading = true
this.recipes = Array(this.ui.page_size).fill({loading: true})
let params = this.buildParams(random)
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params)
.then((result) => {
@@ -1500,10 +1515,10 @@ export default {
this.genericAPI(this.Models.CUSTOM_FILTER, this.Actions.CREATE, params)
.then((result) => {
this.search.search_filter = result.data
StandardToasts.makeStandardToast(this,StandardToasts.SUCCESS_CREATE)
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_CREATE)
})
.catch((err) => {
StandardToasts.makeStandardToast(this,StandardToasts.FAIL_CREATE, err)
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_CREATE, err)
})
},
addField: function (field, count) {
@@ -1563,4 +1578,5 @@ export default {
.vue-treeselect__control-arrow-container {
width: 30px;
}
</style>

View File

@@ -38,7 +38,7 @@
</div>
<div class="my-auto mr-1">
<span class="text-primary"><b>{{ $t("Preparation") }}</b></span><br/>
{{ recipe.working_time }} {{ $t("min") }}
{{ working_time }}
</div>
</div>
</div>
@@ -50,7 +50,7 @@
</div>
<div class="my-auto mr-1">
<span class="text-primary"><b>{{ $t("Waiting") }}</b></span><br/>
{{ recipe.waiting_time }} {{ $t("min") }}
{{ waiting_time }}
</div>
</div>
</div>
@@ -75,7 +75,8 @@
</div>
<div class="col col-md-2 col-2 mt-2 mt-md-0 text-right">
<recipe-context-menu v-bind:recipe="recipe" :servings="servings"></recipe-context-menu>
<recipe-context-menu v-bind:recipe="recipe" :servings="servings"
:disabled_options="{print:false}"></recipe-context-menu>
</div>
</div>
<hr/>
@@ -89,6 +90,7 @@
:ingredient_factor="ingredient_factor"
:servings="servings"
:header="true"
:use_plural="use_plural"
id="ingredient_container"
@checked-state-changed="updateIngredientCheckedState"
@change-servings="servings = $event"
@@ -103,13 +105,6 @@
:style="{ 'max-height': ingredient_height }"/>
</div>
</div>
<div class="row" style="margin-top: 2vh; margin-bottom: 2vh">
<div class="col-12">
<Nutrition-component :recipe="recipe" id="nutrition_container"
:ingredient_factor="ingredient_factor"></Nutrition-component>
</div>
</div>
</div>
</div>
@@ -129,6 +124,7 @@
:step="s"
:ingredient_factor="ingredient_factor"
:index="index"
:use_plural="use_plural"
:start_time="start_time"
@update-start-time="updateStartTime"
@checked-state-changed="updateIngredientCheckedState"
@@ -137,10 +133,19 @@
<div v-if="recipe.source_url !== null">
<h6 class="d-print-none"><i class="fas fa-file-import"></i> {{ $t("Imported_From") }}</h6>
<span class="text-muted mt-1"><a style="overflow-wrap: break-word;" :href="recipe.source_url">{{ recipe.source_url }}</a></span>
<span class="text-muted mt-1"><a style="overflow-wrap: break-word;"
:href="recipe.source_url">{{ recipe.source_url }}</a></span>
</div>
<div class="row" style="margin-top: 2vh; ">
<div class="col-lg-6 offset-lg-3 col-12">
<Nutrition-component :recipe="recipe" id="nutrition_container"
:ingredient_factor="ingredient_factor"></Nutrition-component>
</div>
</div>
</div>
<add-recipe-to-book :recipe="recipe"></add-recipe-to-book>
<div class="row text-center d-print-none" style="margin-top: 3vh; margin-bottom: 3vh"
@@ -160,7 +165,7 @@ import "bootstrap-vue/dist/bootstrap-vue.css"
import {apiLoadRecipe} from "@/utils/api"
import RecipeContextMenu from "@/components/RecipeContextMenu"
import {ResolveUrlMixin, ToastMixin} from "@/utils/utils"
import {ResolveUrlMixin, ToastMixin, calculateHourMinuteSplit} from "@/utils/utils"
import PdfViewer from "@/components/PdfViewer"
import ImageViewer from "@/components/ImageViewer"
@@ -176,6 +181,7 @@ import KeywordsComponent from "@/components/KeywordsComponent"
import NutritionComponent from "@/components/NutritionComponent"
import RecipeSwitcher from "@/components/Buttons/RecipeSwitcher"
import CustomInputSpinButton from "@/components/CustomInputSpinButton"
import {ApiApiFactory} from "@/utils/openapi/api";
Vue.prototype.moment = moment
@@ -206,9 +212,16 @@ export default {
ingredient_count() {
return this.recipe?.steps.map((x) => x.ingredients).flat().length
},
working_time: function () {
return calculateHourMinuteSplit(this.recipe.working_time)
},
waiting_time: function () {
return calculateHourMinuteSplit(this.recipe.waiting_time)
},
},
data() {
return {
use_plural: false,
loading: true,
recipe: undefined,
rootrecipe: undefined,
@@ -217,7 +230,7 @@ export default {
start_time: "",
share_uid: window.SHARE_UID,
wake_lock: null,
ingredient_height: '250'
ingredient_height: '250',
}
},
watch: {
@@ -230,6 +243,11 @@ export default {
this.$i18n.locale = window.CUSTOM_LOCALE
this.requestWakeLock()
window.addEventListener('resize', this.handleResize);
let apiClient = new ApiApiFactory()
apiClient.retrieveSpace(window.ACTIVE_SPACE_ID).then(r => {
this.use_plural = r.data.use_plural
})
},
beforeUnmount() {
this.destroyWakeLock()

View File

@@ -151,6 +151,9 @@
<b-form-checkbox v-model="space.show_facet_count"> Facet Count</b-form-checkbox>
<span class="text-muted small">{{ $t('facet_count_info') }}</span><br/>
<b-form-checkbox v-model="space.use_plural">Use Plural form</b-form-checkbox>
<span class="text-muted small">{{ $t('plural_usage_info') }}</span><br/>
<label>{{ $t('FoodInherit') }}</label>
<generic-multiselect :initial_selection="space.food_inherit"
:model="Models.FOOD_INHERIT_FIELDS"
@@ -204,7 +207,7 @@ Vue.use(VueClipboard)
Vue.use(BootstrapVue)
export default {
name: "SupermarketView",
name: "SpaceManageView",
mixins: [ResolveUrlMixin, ToastMixin, ApiMixin],
components: {GenericMultiselect, GenericModalForm},
data() {
@@ -225,7 +228,7 @@ export default {
this.$i18n.locale = window.CUSTOM_LOCALE
let apiFactory = new ApiApiFactory()
apiFactory.retrieveSpace(this.ACTIVE_SPACE_ID).then(r => {
apiFactory.retrieveSpace(window.ACTIVE_SPACE_ID).then(r => {
this.space = r.data
})
apiFactory.listUserSpaces().then(r => {
@@ -249,7 +252,7 @@ export default {
},
updateSpace: function () {
let apiFactory = new ApiApiFactory()
apiFactory.partialUpdateSpace(this.ACTIVE_SPACE_ID, this.space).then(r => {
apiFactory.partialUpdateSpace(window.ACTIVE_SPACE_ID, this.space).then(r => {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)