Fix after rebase

This commit is contained in:
smilerz
2021-11-01 12:32:19 -05:00
parent 60d7e63da8
commit 5a9543b4d8
17 changed files with 27946 additions and 679 deletions

View File

@ -1,158 +1,164 @@
<template>
<div id="app" class="col-12 col-xl-8 col-lg-10 offset-xl-2 offset-lg-1 offset">
<div class="col-12 col-xl-8 col-lg-10 offset-xl-2 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 mt-3 mb-3">
<b-input-group>
<b-input class="form-control form-control-lg form-control-borderless form-control-search"
v-model="search"
v-bind:placeholder="$t('Search')"></b-input>
<b-input-group-append>
<b-button variant="primary"
v-b-tooltip.hover :title="$t('Create')"
@click="createNew">
<i class="fas fa-plus"></i>
</b-button>
</b-input-group-append>
</b-input-group>
</div>
</div>
</div>
</div>
</div>
<div class="mb-3" v-for="book in filteredBooks" :key="book.id">
<div class="row">
<div class="col-md-12">
<b-card class="d-flex flex-column" v-hover
v-on:click="openBook(book.id)">
<b-row no-gutters style="height:inherit;">
<b-col no-gutters md="2" style="height:inherit;">
<h3>{{ book.icon }}</h3>
</b-col>
<b-col no-gutters md="10" style="height:inherit;">
<b-card-body class="m-0 py-0" style="height:inherit;">
<b-card-text class="h-100 my-0 d-flex flex-column" style="text-overflow: ellipsis">
<h5 class="m-0 mt-1 text-truncate">{{ book.name }} <span class="float-right"><i
class="fa fa-book"></i></span></h5>
<div class="m-0 text-truncate">{{ book.description }}</div>
<div class="mt-auto mb-1 d-flex flex-row justify-content-end">
<div id="app" class="col-12 col-xl-8 col-lg-10 offset-xl-2 offset-lg-1 offset">
<div class="col-12 col-xl-8 col-lg-10 offset-xl-2 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">
<b-input-group>
<b-input
class="form-control form-control-lg form-control-borderless form-control-search"
v-model="search"
v-bind:placeholder="$t('Search')"
></b-input>
<b-input-group-append>
<b-button variant="primary" v-b-tooltip.hover :title="$t('Create')" @click="createNew">
<i class="fas fa-plus"></i>
</b-button>
</b-input-group-append>
</b-input-group>
</div>
</div>
</b-card-text>
</b-card-body>
</b-col>
</b-row>
</b-card>
</div>
</div>
</div>
</div>
<div class="mb-3" v-for="book in filteredBooks" :key="book.id">
<div class="row">
<div class="col-md-12">
<b-card class="d-flex flex-column" v-hover v-on:click="openBook(book.id)">
<b-row no-gutters style="height:inherit;">
<b-col no-gutters md="2" style="height:inherit;">
<h3>{{ book.icon }}</h3>
</b-col>
<b-col no-gutters md="10" style="height:inherit;">
<b-card-body class="m-0 py-0" style="height:inherit;">
<b-card-text class="h-100 my-0 d-flex flex-column" style="text-overflow: ellipsis">
<h5 class="m-0 mt-1 text-truncate">
{{ book.name }} <span class="float-right"><i class="fa fa-book"></i></span>
</h5>
<div class="m-0 text-truncate">{{ book.description }}</div>
<div class="mt-auto mb-1 d-flex flex-row justify-content-end"></div>
</b-card-text>
</b-card-body>
</b-col>
</b-row>
</b-card>
</div>
</div>
<loading-spinner v-if="current_book === book.id && loading"></loading-spinner>
<transition name="slide-fade">
<cookbook-slider :recipes="recipes" :book="book" :key="`slider_${book.id}`"
v-if="current_book === book.id && !loading" v-on:refresh="refreshData"></cookbook-slider>
</transition>
<loading-spinner v-if="current_book === book.id && loading"></loading-spinner>
<transition name="slide-fade">
<cookbook-slider
:recipes="recipes"
:book="book"
:key="`slider_${book.id}`"
v-if="current_book === book.id && !loading"
v-on:refresh="refreshData"
></cookbook-slider>
</transition>
</div>
</div>
</div>
</template>
<script>
import Vue from 'vue'
import {BootstrapVue} from 'bootstrap-vue'
import Vue from "vue"
import { BootstrapVue } from "bootstrap-vue"
import 'bootstrap-vue/dist/bootstrap-vue.css'
import {ApiApiFactory} from "@/utils/openapi/api.ts";
import CookbookSlider from "../../components/CookbookSlider";
import LoadingSpinner from "../../components/LoadingSpinner";
import {StandardToasts} from "../../utils/utils";
import "bootstrap-vue/dist/bootstrap-vue.css"
import { ApiApiFactory } from "@/utils/openapi/api"
import CookbookSlider from "@/components/CookbookSlider"
import LoadingSpinner from "@/components/LoadingSpinner"
import { StandardToasts } from "@/utils/utils"
Vue.use(BootstrapVue)
export default {
name: 'CookbookView',
mixins: [],
components: {LoadingSpinner, CookbookSlider},
data() {
return {
cookbooks: [],
book_background: window.IMAGE_BOOK,
recipes: [],
current_book: undefined,
loading: false,
search: ''
}
},
computed: {
filteredBooks: function () {
return this.cookbooks.filter(book => {
return book.name.toLowerCase().includes(this.search.toLowerCase())
})
}
},
mounted() {
this.refreshData()
this.$i18n.locale = window.CUSTOM_LOCALE
},
methods: {
refreshData: function () {
let apiClient = new ApiApiFactory()
apiClient.listRecipeBooks().then(result => {
this.cookbooks = result.data
})
name: "CookbookView",
mixins: [],
components: { LoadingSpinner, CookbookSlider },
data() {
return {
cookbooks: [],
book_background: window.IMAGE_BOOK,
recipes: [],
current_book: undefined,
loading: false,
search: "",
}
},
openBook: function (book) {
if (book === this.current_book) {
this.current_book = undefined
this.recipes = []
return
}
this.loading = true
let apiClient = new ApiApiFactory()
this.current_book = book
apiClient.listRecipeBookEntrys({query: {book: book}}).then(result => {
this.recipes = result.data
this.loading = false
})
computed: {
filteredBooks: function() {
return this.cookbooks.filter((book) => {
return book.name.toLowerCase().includes(this.search.toLowerCase())
})
},
},
createNew: function () {
let apiClient = new ApiApiFactory()
apiClient.createRecipeBook({name: this.$t('New_Cookbook'), description: '', icon: '', shared: []}).then(result => {
let new_book = result.data
mounted() {
this.refreshData()
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
}).catch(error => {
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
})
}
},
directives: {
hover: {
inserted: function (el) {
el.addEventListener('mouseenter', () => {
el.classList.add("shadow")
});
el.addEventListener('mouseleave', () => {
el.classList.remove("shadow")
});
}
}
}
}
this.$i18n.locale = window.CUSTOM_LOCALE
},
methods: {
refreshData: function() {
let apiClient = new ApiApiFactory()
apiClient.listRecipeBooks().then((result) => {
this.cookbooks = result.data
})
},
openBook: function(book) {
if (book === this.current_book) {
this.current_book = undefined
this.recipes = []
return
}
this.loading = true
let apiClient = new ApiApiFactory()
this.current_book = book
apiClient.listRecipeBookEntrys({ query: { book: book } }).then((result) => {
this.recipes = result.data
this.loading = false
})
},
createNew: function() {
let apiClient = new ApiApiFactory()
apiClient
.createRecipeBook({ name: "New Book", description: "", icon: "", shared: [] })
.then((result) => {
let new_book = result.data
this.refreshData()
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
})
.catch((error) => {
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
})
},
},
directives: {
hover: {
inserted: function(el) {
el.addEventListener("mouseenter", () => {
el.classList.add("shadow")
})
el.addEventListener("mouseleave", () => {
el.classList.remove("shadow")
})
},
},
},
}
</script>
<style>
.slide-fade-enter-active {
transition: all .6s ease;
transition: all 0.6s ease;
}
.slide-fade-enter, .slide-fade-leave-to
/* .slide-fade-leave-active below version 2.1.8 */
{
transform: translateX(10px);
opacity: 0;
{
transform: translateX(10px);
opacity: 0;
}
</style>

View File

@ -166,6 +166,7 @@
>
<a class="dropdown-item p-2" href="#"><i class="fas fa-shopping-cart"></i> {{ $t("Add_to_Shopping") }}</a>
</ContextMenuItem>
<!-- TODO: Add new shopping Modal -->
<ContextMenuItem
@click="
$refs.menu.close()
@ -250,22 +251,24 @@
</template>
<script>
import Vue from "vue"
import { BootstrapVue } from "bootstrap-vue"
import "bootstrap-vue/dist/bootstrap-vue.css"
import ContextMenu from "@/components/ContextMenu/ContextMenu"
import ContextMenuItem from "@/components/ContextMenu/ContextMenuItem"
import { CalendarView, CalendarMathMixin } from "vue-simple-calendar/src/components/bundle"
import Vue from "vue"
import { ApiApiFactory } from "@/utils/openapi/api"
import MealPlanCard from "../../components/MealPlanCard"
import moment from "moment"
import { ApiMixin, StandardToasts } from "@/utils/utils"
import MealPlanEditModal from "@/components/Modals/MealPlanEditModal"
import VueCookies from "vue-cookies"
import MealPlanCard from "@/components/MealPlanCard"
import MealPlanEditModal from "@/components/MealPlanEditModal"
import MealPlanCalenderHeader from "@/components/MealPlanCalenderHeader"
import EmojiInput from "../../components/Modals/EmojiInput"
import EmojiInput from "@/components/Modals/EmojiInput"
import moment from "moment"
import draggable from "vuedraggable"
import VueCookies from "vue-cookies"
import { ApiMixin, StandardToasts } from "@/utils/utils"
import { CalendarView, CalendarMathMixin } from "vue-simple-calendar/src/components/bundle"
import { ApiApiFactory } from "@/utils/openapi/api"
const { makeToast } = require("@/utils/utils")

View File

@ -25,8 +25,34 @@
</div>
<div style="text-align: center">
<keywords :recipe="recipe"></keywords>
<keywords-component :recipe="recipe"></keywords-component>
</div>
<hr/>
<div class="row">
<div class="col col-md-3">
<div class="row d-flex" style="padding-left: 16px">
<div class="my-auto" style="padding-right: 4px">
<i class="fas fa-user-clock fa-2x text-primary"></i>
</div>
<div class="my-auto" style="padding-right: 4px">
<span class="text-primary"><b>{{ $t('Preparation') }}</b></span><br/>
{{ recipe.working_time }} {{ $t('min') }}
</div>
<div class="row text-center">
<div class="col col-md-12">
<recipe-rating :recipe="recipe"></recipe-rating>
<last-cooked :recipe="recipe" class="mt-2"></last-cooked>
</div>
</div>
<div class="my-auto">
<div class="col-12" style="text-align: center">
<i>{{ recipe.description }}</i>
</div>
</div>
<hr />
<div class="row">
@ -92,38 +118,32 @@
</div>
</div>
<div class="row" style="margin-top: 2vh; margin-bottom: 2vh">
<div class="col-12">
<Nutrition :recipe="recipe" :ingredient_factor="ingredient_factor"></Nutrition>
</div>
</div>
</div>
</div>
<template v-if="!recipe.internal">
<div v-if="recipe.file_path.includes('.pdf')">
<PdfViewer :recipe="recipe"></PdfViewer>
</div>
<div v-if="recipe.file_path.includes('.png') || recipe.file_path.includes('.jpg') || recipe.file_path.includes('.jpeg') || recipe.file_path.includes('.gif')">
<ImageViewer :recipe="recipe"></ImageViewer>
</div>
</template>
<div v-for="(s, index) in recipe.steps" v-bind:key="s.id" style="margin-top: 1vh">
<Step
:recipe="recipe"
:step="s"
:ingredient_factor="ingredient_factor"
:index="index"
:start_time="start_time"
@update-start-time="updateStartTime"
@checked-state-changed="updateIngredientCheckedState"
></Step>
<div class="row" style="margin-top: 2vh; margin-bottom: 2vh">
<div class="col-12">
<Nutrition-component :recipe="recipe" :ingredient_factor="ingredient_factor"></Nutrition-component>
</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" v-if="share_uid !== 'None'">
<div class="col col-md-12">
<a :href="resolveDjangoUrl('view_report_share_abuse', share_uid)">{{ $t("Report Abuse") }}</a>
</div>
</div>
</template>
<div v-for="(s, index) in recipe.steps" v-bind:key="s.id" style="margin-top: 1vh">
<step-component :recipe="recipe" :step="s" :ingredient_factor="ingredient_factor" :index="index" :start_time="start_time"
@update-start-time="updateStartTime" @checked-state-changed="updateIngredientCheckedState"></step-component>
</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" v-if="share_uid !== 'None'">
<div class="col col-md-12">
<a :href="resolveDjangoUrl('view_report_share_abuse', share_uid)">{{ $t("Report Abuse") }}</a>
@ -139,17 +159,17 @@ import "bootstrap-vue/dist/bootstrap-vue.css"
import { apiLoadRecipe } from "@/utils/api"
import Step from "@/components/Step"
import StepComponent from "@/components/StepComponent";
import RecipeContextMenu from "@/components/ContextMenu/RecipeContextMenu"
import { ResolveUrlMixin, ToastMixin } from "@/utils/utils"
import PdfViewer from "@/components/PdfViewer"
import ImageViewer from "@/components/ImageViewer"
import Nutrition from "@/components/Nutrition"
import IngredientsCard from "@/components/IngredientsCard"
import moment from "moment"
import Keywords from "@/components/Keywords"
import KeywordsComponent from "@/components/KeywordsComponent";
import NutritionComponent from "@/components/NutritionComponent";
import LoadingSpinner from "@/components/LoadingSpinner"
import AddRecipeToBook from "@/components/Modals/AddRecipeToBook"
import RecipeRating from "@/components/RecipeRating"
@ -168,10 +188,10 @@ export default {
PdfViewer,
ImageViewer,
IngredientsCard,
Step,
StepComponent,
RecipeContextMenu,
Nutrition,
Keywords,
NutritionComponent,
KeywordsComponent,
LoadingSpinner,
AddRecipeToBook,
},

View File

@ -1,217 +0,0 @@
<template>
<tr>
<template v-if="ingredient.is_header">
<td colspan="5" @click="done">
<b>{{ ingredient.note }}</b>
</td>
</template>
<template v-else>
<td class="d-print-non" v-if="detailed && !add_shopping_mode" @click="done">
<i class="far fa-check-circle text-success" v-if="ingredient.checked"></i>
<i class="far fa-check-circle text-primary" v-if="!ingredient.checked"></i>
</td>
<td class="text-nowrap" @click="done">
<span v-if="ingredient.amount !== 0" v-html="calculateAmount(ingredient.amount)"></span>
</td>
<td @click="done">
<span v-if="ingredient.unit !== null && !ingredient.no_amount">{{ ingredient.unit.name }}</span>
</td>
<td @click="done">
<template v-if="ingredient.food !== null">
<!-- <i
v-if="show_shopping && !add_shopping_mode"
class="far fa-edit fa-sm px-1"
@click="editFood()"
></i> -->
<a :href="resolveDjangoUrl('view_recipe', ingredient.food.recipe.id)" v-if="ingredient.food.recipe !== null" target="_blank" rel="noopener noreferrer">{{
ingredient.food.name
}}</a>
<span v-if="ingredient.food.recipe === null">{{ ingredient.food.name }}</span>
</template>
</td>
<td v-if="detailed && !show_shopping">
<div v-if="ingredient.note">
<span v-b-popover.hover="ingredient.note" class="d-print-none touchable">
<i class="far fa-comment"></i>
</span>
<!-- v-if="ingredient.note.length > 15" -->
<!-- <span v-else>-->
<!-- {{ ingredient.note }}-->
<!-- </span>-->
<div class="d-none d-print-block"><i class="far fa-comment-alt d-print-none"></i> {{ ingredient.note }}</div>
</div>
</td>
<td v-else-if="show_shopping" class="text-right text-nowrap">
<!-- in shopping mode and ingredient is not ignored -->
<div v-if="!ingredient.food.ignore_shopping">
<b-button
class="btn text-decoration-none fas fa-shopping-cart px-2 user-select-none"
variant="link"
v-b-popover.hover.click.blur.html.top="{ title: ShoppingPopover, variant: 'outline-dark' }"
:class="{
'text-success': shopping_status === true,
'text-muted': shopping_status === false,
'text-warning': shopping_status === null,
}"
/>
<span class="px-2">
<input type="checkbox" class="align-middle" v-model="shop" @change="changeShopping" />
</span>
<on-hand-badge :item="ingredient.food" />
</div>
<div v-else>
<!-- or in shopping mode and food is ignored: Shopping Badge bypasses linking ingredient to Recipe which would get ignored -->
<shopping-badge :item="ingredient.food" :override_ignore="true" class="px-1" />
<span class="px-2">
<input type="checkbox" class="align-middle" disabled v-b-popover.hover.click.blur :title="$t('IgnoredFood', { food: ingredient.food.name })" />
</span>
<on-hand-badge :item="ingredient.food" />
</div>
</td>
</template>
</tr>
</template>
<script>
import { calculateAmount, ResolveUrlMixin, ApiMixin } from "@/utils/utils"
import OnHandBadge from "@/components/Badges/OnHand"
import ShoppingBadge from "@/components/Badges/Shopping"
export default {
name: "Ingredient",
components: { OnHandBadge, ShoppingBadge },
props: {
ingredient: Object,
ingredient_factor: { type: Number, default: 1 },
detailed: { type: Boolean, default: true },
recipe_list: { type: Number }, // ShoppingListRecipe ID, to filter ShoppingStatus
show_shopping: { type: Boolean, default: false },
add_shopping_mode: { type: Boolean, default: false },
shopping_list: {
type: Array,
default() {
return []
},
}, // list of unchecked ingredients in shopping list
},
mixins: [ResolveUrlMixin, ApiMixin],
data() {
return {
checked: false,
shopping_status: null,
shopping_items: [],
shop: false,
dirty: undefined,
}
},
watch: {
ShoppingListAndFilter: {
immediate: true,
handler(newVal, oldVal) {
let filtered_list = this.shopping_list
// if a recipe list is provided, filter the shopping list
if (this.recipe_list) {
filtered_list = filtered_list.filter((x) => x.list_recipe == this.recipe_list)
}
// how many ShoppingListRecipes are there for this recipe?
let count_shopping_recipes = [...new Set(filtered_list.map((x) => x.list_recipe))].length
let count_shopping_ingredient = filtered_list.filter((x) => x.ingredient == this.ingredient.id).length
if (count_shopping_recipes > 1) {
this.shop = false // don't check any boxes until user selects a shopping list to edit
if (count_shopping_ingredient >= 1) {
this.shopping_status = true
} else if (this.ingredient.food.shopping) {
this.shopping_status = null // food is in the shopping list, just not for this ingredient/recipe
} else {
this.shopping_status = false // food is not in any shopping list
}
} else {
// mark checked if the food is in the shopping list for this ingredient/recipe
if (count_shopping_ingredient >= 1) {
// ingredient is in this shopping list
this.shop = true
this.shopping_status = true
} else if (count_shopping_ingredient == 0 && this.ingredient.food.shopping) {
// food is in the shopping list, just not for this ingredient/recipe
this.shop = false
this.shopping_status = null
} else {
// the food is not in any shopping list
this.shop = false
this.shopping_status = false
}
}
// if we are in add shopping mode start with all checks marked
if (this.add_shopping_mode) {
this.shop = !this.ingredient.food.on_hand && !this.ingredient.food.ignore_shopping && !this.ingredient.food.recipe
}
},
},
},
mounted() {},
computed: {
ShoppingListAndFilter() {
// hack to watch the shopping list and the recipe list at the same time
return this.shopping_list.map((x) => x.id).join(this.recipe_list)
},
ShoppingPopover() {
if (this.shopping_status == false) {
return this.$t("NotInShopping", { food: this.ingredient.food.name })
} else {
let list = this.shopping_list.filter((x) => x.food.id == this.ingredient.food.id)
let category = this.$t("Category") + ": " + this.ingredient?.food?.supermarket_category?.name ?? this.$t("Undefined")
let popover = []
list.forEach((x) => {
popover.push(
[
"<tr style='border-bottom: 1px solid #ccc'>",
"<td style='padding: 3px;'><em>",
x?.recipe_mealplan?.name ?? "",
"</em></td>",
"<td style='padding: 3px;'>",
x?.amount ?? "",
"</td>",
"<td style='padding: 3px;'>",
x?.unit?.name ?? "" + "</td>",
"<td style='padding: 3px;'>",
x?.food?.name ?? "",
"</td></tr>",
].join("")
)
})
return "<table class='table-small'><th colspan='4'>" + category + "</th>" + popover.join("") + "</table>"
}
},
},
methods: {
calculateAmount: function(x) {
return calculateAmount(x, this.ingredient_factor)
},
// sends parent recipe ingredient to notify complete has been toggled
done: function() {
this.$emit("checked-state-changed", this.ingredient)
},
// sends true/false to parent to save all ingredient shopping updates as a batch
changeShopping: function() {
this.$emit("add-to-shopping", { item: this.ingredient, add: this.shop })
},
editFood: function() {
console.log("edit the food")
},
},
}
</script>
<style scoped>
/* increase size of hover/touchable space without changing spacing */
.touchable {
padding-right: 2em;
padding-left: 2em;
margin-right: -2em;
margin-left: -2em;
}
</style>

View File

@ -19,11 +19,6 @@
</td>
<td @click="done">
<template v-if="ingredient.food !== null">
<!-- <i
v-if="show_shopping && !add_shopping_mode"
class="far fa-edit fa-sm px-1"
@click="editFood()"
></i> -->
<a :href="resolveDjangoUrl('view_recipe', ingredient.food.recipe.id)" v-if="ingredient.food.recipe !== null" target="_blank" rel="noopener noreferrer">{{
ingredient.food.name
}}</a>
@ -35,10 +30,6 @@
<span v-b-popover.hover="ingredient.note" class="d-print-none touchable">
<i class="far fa-comment"></i>
</span>
<!-- v-if="ingredient.note.length > 15" -->
<!-- <span v-else>-->
<!-- {{ ingredient.note }}-->
<!-- </span>-->
<div class="d-none d-print-block"><i class="far fa-comment-alt d-print-none"></i> {{ ingredient.note }}</div>
</div>
@ -80,7 +71,7 @@ import OnHandBadge from "@/components/Badges/OnHand"
import ShoppingBadge from "@/components/Badges/Shopping"
export default {
name: "Ingredient",
name: "IngredientComponent",
components: { OnHandBadge, ShoppingBadge },
props: {
ingredient: Object,

View File

@ -12,6 +12,17 @@
<b-badge pill variant="secondary" class="mt-1 font-weight-normal"><i class="fa fa-pause"></i> {{ recipe.waiting_time }} {{ $t("min") }} </b-badge>
</div>
</a>
</div>
<div class="card-img-overlay w-50 d-flex flex-column justify-content-left float-left text-left pt-2"
v-if="recipe.working_time !== 0 || recipe.waiting_time !== 0">
<b-badge pill variant="light" class="mt-1 font-weight-normal" v-if="recipe.working_time !== 0"><i class="fa fa-clock"></i>
{{ recipe.working_time }} {{ $t('min') }}
</b-badge>
<b-badge pill variant="secondary" class="mt-1 font-weight-normal" v-if="recipe.waiting_time !== 0"><i class="fa fa-pause"></i>
{{ recipe.waiting_time }} {{ $t('min') }}
</b-badge>
</div>
</a>
<b-card-body class="p-4">
<h6>
@ -34,7 +45,7 @@
</template>
<p class="mt-1">
<last-cooked :recipe="recipe"></last-cooked>
<keywords :recipe="recipe" style="margin-top: 4px"></keywords>
<keywords-component :recipe="recipe" style="margin-top: 4px"></keywords-component>
</p>
<transition name="fade" mode="in-out">
<div class="row mt-3" v-if="detailed">
@ -47,10 +58,6 @@
</transition>
<b-badge pill variant="info" v-if="!recipe.internal">{{ $t("External") }}</b-badge>
<!-- <b-badge pill variant="success"
v-if="Date.parse(recipe.created_at) > new Date(Date.now() - (7 * (1000 * 60 * 60 * 24)))">
{{ $t('New') }}
</b-badge> -->
</template>
<template v-else>{{ meal_plan.note }}</template>
</b-card-text>
@ -62,7 +69,7 @@
<script>
import RecipeContextMenu from "@/components/ContextMenu/RecipeContextMenu"
import Keywords from "@/components/Keywords"
import KeywordsComponent from "@/components/KeywordsComponent";
import { resolveDjangoUrl, ResolveUrlMixin } from "@/utils/utils"
import RecipeRating from "@/components/RecipeRating"
import moment from "moment/moment"
@ -75,7 +82,7 @@ Vue.prototype.moment = moment
export default {
name: "RecipeCard",
mixins: [ResolveUrlMixin],
components: { LastCooked, RecipeRating, Keywords, RecipeContextMenu, IngredientsCard },
components: { LastCooked, RecipeRating, KeywordsComponent, RecipeContextMenu, IngredientsCard },
props: {
recipe: Object,
meal_plan: Object,

View File

@ -1,217 +1,200 @@
<template>
<div>
<hr />
<div>
<hr />
<template v-if="step.type === 'TEXT' || step.type === 'RECIPE'">
<div class="row" v-if="recipe.steps.length > 1">
<div class="col col-md-8">
<h5 class="text-primary">
<template v-if="step.name">{{ step.name }}</template>
<template v-else>{{ $t('Step') }} {{ index + 1 }}</template>
<small style="margin-left: 4px" class="text-muted" v-if="step.time !== 0"><i class="fas fa-user-clock"></i>
{{ step.time }} {{ $t('min') }}
</small>
<small v-if="start_time !== ''" class="d-print-none">
<b-link :id="`id_reactive_popover_${step.id}`" @click="openPopover" href="#">
{{ moment(start_time).add(step.time_offset, 'minutes').format('HH:mm') }}
</b-link>
</small>
</h5>
</div>
<div class="col col-md-4" style="text-align: right">
<b-button @click="details_visible = !details_visible" style="border: none; background: none"
class="shadow-none d-print-none"
:class="{ 'text-primary': details_visible, 'text-success': !details_visible}">
<i class="far fa-check-circle"></i>
</b-button>
</div>
</div>
</template>
<template v-if="step.type === 'TEXT'">
<b-collapse id="collapse-1" v-model="details_visible">
<div class="row">
<div class="col col-md-4"
v-if="step.ingredients.length > 0 && (recipe.steps.length > 1 || force_ingredients)">
<table class="table table-sm">
<ingredients-card
:steps="[step]"
:ingredient_factor="ingredient_factor"
@checked-state-changed="$emit('checked-state-changed', $event)"
/>
</table>
</div>
<div class="col" :class="{ 'col-md-8': recipe.steps.length > 1, 'col-md-12': recipe.steps.length <= 1,}">
<compile-component :code="step.ingredients_markdown"
:ingredient_factor="ingredient_factor"></compile-component>
</div>
</div>
</b-collapse>
</template>
<template v-if="step.type === 'TIME' || step.type === 'FILE'">
<div class="row">
<div class="col-md-8 offset-md-2" style="text-align: center">
<h4 class="text-primary">
<template v-if="step.name">{{ step.name }}</template>
<template v-else>{{ $t('Step') }} {{ index + 1 }}</template>
</h4>
<span style="margin-left: 4px" class="text-muted" v-if="step.time !== 0"><i class="fa fa-stopwatch"></i>
{{ step.time }} {{ $t('min') }}</span>
<b-link class="d-print-none" :id="`id_reactive_popover_${step.id}`" @click="openPopover" href="#"
v-if="start_time !== ''">
{{ moment(start_time).add(step.time_offset, 'minutes').format('HH:mm') }}
</b-link>
</div>
<div class="col-md-2" style="text-align: right">
<b-button @click="details_visible = !details_visible" style="border: none; background: none"
class="shadow-none d-print-none"
:class="{ 'text-primary': details_visible, 'text-success': !details_visible}">
<i class="far fa-check-circle"></i>
</b-button>
</div>
</div>
<b-collapse id="collapse-1" v-model="details_visible">
<div class="row" v-if="step.instruction !== ''">
<div class="col col-md-12" style="text-align: center">
<compile-component :code="step.ingredients_markdown"
:ingredient_factor="ingredient_factor"></compile-component>
</div>
</div>
</b-collapse>
</template>
<div class="row" style="text-align: center">
<div class="col col-md-12">
<template v-if="step.file !== null">
<div
v-if="step.file.file.includes('.png') || recipe.file_path.includes('.jpg') || recipe.file_path.includes('.jpeg') || recipe.file_path.includes('.gif')">
<img :src="step.file.file" style="max-width: 50vw; max-height: 50vh">
</div>
<div v-else>
<a :href="step.file.file" target="_blank" rel="noreferrer nofollow">{{ $t('Download') }} {{
$t('File')
}}</a>
</div>
<template v-if="step.type === 'TEXT' || step.type === 'RECIPE'">
<div class="row" v-if="recipe.steps.length > 1">
<div class="col col-md-8">
<h5 class="text-primary">
<template v-if="step.name">{{ step.name }}</template>
<template v-else>{{ $t("Step") }} {{ index + 1 }}</template>
<small style="margin-left: 4px" class="text-muted" v-if="step.time !== 0"><i class="fas fa-user-clock"></i> {{ step.time }} {{ $t("min") }} </small>
<small v-if="start_time !== ''" class="d-print-none">
<b-link :id="`id_reactive_popover_${step.id}`" @click="openPopover" href="#">
{{
moment(start_time)
.add(step.time_offset, "minutes")
.format("HH:mm")
}}
</b-link>
</small>
</h5>
</div>
<div class="col col-md-4" style="text-align: right">
<b-button
@click="details_visible = !details_visible"
style="border: none; background: none"
class="shadow-none d-print-none"
:class="{ 'text-primary': details_visible, 'text-success': !details_visible }"
>
<i class="far fa-check-circle"></i>
</b-button>
</div>
</div>
</template>
</div>
</div>
<template v-if="step.type === 'TEXT'">
<b-collapse id="collapse-1" v-model="details_visible">
<div class="row">
<div class="col col-md-4" v-if="step.ingredients.length > 0 && (recipe.steps.length > 1 || force_ingredients)">
<table class="table table-sm">
<ingredients-card :steps="[step]" :ingredient_factor="ingredient_factor" @checked-state-changed="$emit('checked-state-changed', $event)" />
</table>
</div>
<div class="col" :class="{ 'col-md-8': recipe.steps.length > 1, 'col-md-12': recipe.steps.length <= 1 }">
<compile-component :code="step.ingredients_markdown" :ingredient_factor="ingredient_factor"></compile-component>
</div>
</div>
</b-collapse>
</template>
<div class="card" v-if="step.type === 'RECIPE' && step.step_recipe_data !== null">
<b-collapse id="collapse-1" v-model="details_visible">
<div class="card-body">
<h2 class="card-title">
<a :href="resolveDjangoUrl('view_recipe',step.step_recipe_data.id)">{{ step.step_recipe_data.name }}</a>
</h2>
<div v-for="(sub_step, index) in step.step_recipe_data.steps" v-bind:key="`substep_${sub_step.id}`">
<Step :recipe="step.step_recipe_data" :step="sub_step" :ingredient_factor="ingredient_factor" :index="index"
:start_time="start_time" :force_ingredients="true"></Step>
<template v-if="step.type === 'TIME' || step.type === 'FILE'">
<div class="row">
<div class="col-md-8 offset-md-2" style="text-align: center">
<h4 class="text-primary">
<template v-if="step.name">{{ step.name }}</template>
<template v-else>{{ $t("Step") }} {{ index + 1 }}</template>
</h4>
<span style="margin-left: 4px" class="text-muted" v-if="step.time !== 0"><i class="fa fa-stopwatch"></i> {{ step.time }} {{ $t("min") }}</span>
<b-link class="d-print-none" :id="`id_reactive_popover_${step.id}`" @click="openPopover" href="#" v-if="start_time !== ''">
{{
moment(start_time)
.add(step.time_offset, "minutes")
.format("HH:mm")
}}
</b-link>
</div>
<div class="col-md-2" style="text-align: right">
<b-button
@click="details_visible = !details_visible"
style="border: none; background: none"
class="shadow-none d-print-none"
:class="{ 'text-primary': details_visible, 'text-success': !details_visible }"
>
<i class="far fa-check-circle"></i>
</b-button>
</div>
</div>
<b-collapse id="collapse-1" v-model="details_visible">
<div class="row" v-if="step.instruction !== ''">
<div class="col col-md-12" style="text-align: center">
<compile-component :code="step.ingredients_markdown" :ingredient_factor="ingredient_factor"></compile-component>
</div>
</div>
</b-collapse>
</template>
<div class="row" style="text-align: center">
<div class="col col-md-12">
<template v-if="step.file !== null">
<div v-if="step.file.file.includes('.png') || recipe.file_path.includes('.jpg') || recipe.file_path.includes('.jpeg') || recipe.file_path.includes('.gif')">
<img :src="step.file.file" style="max-width: 50vw; max-height: 50vh" />
</div>
<div v-else>
<a :href="step.file.file" target="_blank" rel="noreferrer nofollow">{{ $t("Download") }} {{ $t("File") }}</a>
</div>
</template>
</div>
</div>
</div>
</b-collapse>
</div>
<div v-if="start_time !== ''">
<b-popover
:target="`id_reactive_popover_${step.id}`"
triggers="click"
placement="bottom"
:ref="`id_reactive_popover_${step.id}`"
:title="$t('Step start time')">
<div>
<b-form-group
label="Time"
label-for="popover-input-1"
label-cols="3"
class="mb-1">
<b-form-input
type="datetime-local"
id="popover-input-1"
v-model.datetime-local="set_time_input"
size="sm"
></b-form-input>
</b-form-group>
<div class="card" v-if="step.type === 'RECIPE' && step.step_recipe_data !== null">
<b-collapse id="collapse-1" v-model="details_visible">
<div class="card-body">
<h2 class="card-title">
<a :href="resolveDjangoUrl('view_recipe', step.step_recipe_data.id)">{{ step.step_recipe_data.name }}</a>
</h2>
<div v-for="(sub_step, index) in step.step_recipe_data.steps" v-bind:key="`substep_${sub_step.id}`">
<Step
:recipe="step.step_recipe_data"
:step="sub_step"
:ingredient_factor="ingredient_factor"
:index="index"
:start_time="start_time"
:force_ingredients="true"
></Step>
</div>
</div>
</b-collapse>
</div>
<div class="row" style="margin-top: 1vh">
<div class="col-12" style="text-align: right">
<b-button @click="closePopover" size="sm" variant="secondary" style="margin-right:8px">Cancel</b-button>
<b-button @click="updateTime" size="sm" variant="primary">Ok</b-button>
</div>
</div>
</b-popover>
</div>
</div>
<div v-if="start_time !== ''">
<b-popover :target="`id_reactive_popover_${step.id}`" triggers="click" placement="bottom" :ref="`id_reactive_popover_${step.id}`" :title="$t('Step start time')">
<div>
<b-form-group label="Time" label-for="popover-input-1" label-cols="3" class="mb-1">
<b-form-input type="datetime-local" id="popover-input-1" v-model.datetime-local="set_time_input" size="sm"></b-form-input>
</b-form-group>
</div>
<div class="row" style="margin-top: 1vh">
<div class="col-12" style="text-align: right">
<b-button @click="closePopover" size="sm" variant="secondary" style="margin-right:8px">Cancel</b-button>
<b-button @click="updateTime" size="sm" variant="primary">Ok</b-button>
</div>
</div>
</b-popover>
</div>
</div>
</template>
<script>
import { calculateAmount } from "@/utils/utils"
import {calculateAmount} from "@/utils/utils";
import { GettextMixin } from "@/utils/utils"
import {GettextMixin} from "@/utils/utils";
import CompileComponent from "@/components/CompileComponent";
import IngredientsCard from "@/components/IngredientsCard";
import Vue from "vue";
import moment from "moment";
import {ResolveUrlMixin} from "@/utils/utils";
import IngredientComponent from "@/components/IngredientComponent";
import CompileComponent from "@/components/CompileComponent"
import IngredientsCard from "@/components/IngredientsCard"
import Vue from "vue"
import moment from "moment"
import { ResolveUrlMixin } from "@/utils/utils"
Vue.prototype.moment = moment
export default {
name: 'StepComponent',
mixins: [
GettextMixin,
ResolveUrlMixin,
],
components: { CompileComponent, IngredientsCard},
props: {
step: Object,
ingredient_factor: Number,
index: Number,
recipe: Object,
start_time: String,
force_ingredients: {
type: Boolean,
default: false
}
},
data() {
return {
details_visible: true,
set_time_input: '',
}
},
mounted() {
this.set_time_input = moment(this.start_time).add(this.step.time_offset, 'minutes').format('yyyy-MM-DDTHH:mm')
},
methods: {
calculateAmount: function (x) {
// used by the jinja2 template
return calculateAmount(x, this.ingredient_factor)
name: "StepComponent",
mixins: [GettextMixin, ResolveUrlMixin],
components: { CompileComponent, IngredientsCard },
props: {
step: Object,
ingredient_factor: Number,
index: Number,
recipe: Object,
start_time: String,
force_ingredients: {
type: Boolean,
default: false,
},
},
updateTime: function () {
let new_start_time = moment(this.set_time_input).add(this.step.time_offset * -1, 'minutes').format('yyyy-MM-DDTHH:mm')
data() {
return {
details_visible: true,
set_time_input: "",
}
},
mounted() {
this.set_time_input = moment(this.start_time)
.add(this.step.time_offset, "minutes")
.format("yyyy-MM-DDTHH:mm")
},
methods: {
calculateAmount: function(x) {
// used by the jinja2 template
return calculateAmount(x, this.ingredient_factor)
},
updateTime: function() {
let new_start_time = moment(this.set_time_input)
.add(this.step.time_offset * -1, "minutes")
.format("yyyy-MM-DDTHH:mm")
this.$emit('update-start-time', new_start_time)
this.closePopover()
this.$emit("update-start-time", new_start_time)
this.closePopover()
},
closePopover: function() {
this.$refs[`id_reactive_popover_${this.step.id}`].$emit("close")
},
openPopover: function() {
this.$refs[`id_reactive_popover_${this.step.id}`].$emit("open")
},
},
closePopover: function () {
this.$refs[`id_reactive_popover_${this.step.id}`].$emit('close')
},
openPopover: function () {
this.$refs[`id_reactive_popover_${this.step.id}`].$emit('open')
}
}
}
</script>

View File

@ -209,6 +209,18 @@
"DeleteShoppingConfirm": "Are you sure that you want to remove all {food} from the shopping list?",
"IgnoredFood": "{food} is set to ignore shopping.",
"Add_Servings_to_Shopping": "Add {servings} Servings to Shopping",
"Week_Numbers": "Week numbers",
"Show_Week_Numbers": "Show week numbers ?",
"Export_As_ICal": "Export current period to iCal format",
"Export_To_ICal": "Export .ics",
"Cannot_Add_Notes_To_Shopping": "Notes cannot be added to the shopping list",
"Added_To_Shopping_List": "Added to shopping list",
"Shopping_List_Empty": "Your shopping list is currently empty, you can add items via the context menu of a meal plan entry (right click on the card or left click the menu icon)",
"Next_Period": "Next Period",
"Previous_Period": "Previous Period",
"Current_Period": "Current Period",
"Next_Day": "Next Day",
"Previous_Day": "Previous Day",
"Inherit": "Inherit",
"IgnoreInherit": "Do Not Inherit Fields",
"FoodInherit": "Food Inheritable Fields",
@ -238,18 +250,6 @@
"mealplan_autoinclude_related_desc": "When adding a meal plan to the shopping list (manually or automatically), include all related recipes.",
"default_delay_desc": "Default number of hours to delay a shopping list entry.",
"filter_to_supermarket": "Filter to Supermarket",
"Week_Numbers": "Week numbers",
"Show_Week_Numbers": "Show week numbers ?",
"Export_As_ICal": "Export current period to iCal format",
"Export_To_ICal": "Export .ics",
"Cannot_Add_Notes_To_Shopping": "Notes cannot be added to the shopping list",
"Added_To_Shopping_List": "Added to shopping list",
"Shopping_List_Empty": "Your shopping list is currently empty, you can add items via the context menu of a meal plan entry (right click on the card or left click the menu icon)",
"Next_Period": "Next Period",
"Previous_Period": "Previous Period",
"Current_Period": "Current Period",
"Next_Day": "Next Day",
"Previous_Day": "Previous Day",
"Coming_Soon": "Coming-Soon",
"Auto_Planner": "Auto-Planner",
"New_Cookbook": "New cookbook",

View File

@ -2283,24 +2283,12 @@ export interface ShoppingListRecipe {
* @memberof ShoppingListRecipe
*/
name?: string;
/**
*
* @type {FoodSupermarketCategory}
* @memberof ShoppingListEntry
*/
unit?: FoodSupermarketCategory | null;
/**
*
* @type {number}
* @memberof ShoppingListRecipe
*/
ingredient?: number | null;
/**
*
* @type {string}
* @memberof ShoppingListEntry
*/
ingredient_note?: string;
recipe?: number | null;
/**
*
* @type {number}
@ -2353,13 +2341,7 @@ export interface ShoppingListRecipeMealplan {
/**
*
* @type {number}
* @memberof ShoppingListRecipe
*/
mealplan?: number | null;
/**
*
* @type {string}
* @memberof ShoppingListRecipe
* @memberof ShoppingListRecipeMealplan
*/
mealplan?: number | null;
/**
@ -2408,13 +2390,7 @@ export interface ShoppingListRecipes {
/**
*
* @type {number}
* @memberof ShoppingListRecipeMealplan
*/
mealplan?: number | null;
/**
*
* @type {string}
* @memberof ShoppingListRecipeMealplan
* @memberof ShoppingListRecipes
*/
mealplan?: number | null;
/**