Fix after rebase
This commit is contained in:
parent
f245aa8b4f
commit
dcfe4de61f
@ -83,6 +83,11 @@ class Migration(migrations.Migration):
|
||||
name='ingredient',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.ingredient'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='shoppinglistrecipe',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, default='', max_length=32),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='shoppinglistentry',
|
||||
name='unit',
|
||||
|
@ -822,7 +822,7 @@ class ShoppingListRecipe(ExportModelOperationsMixin('shopping_list_recipe'), mod
|
||||
|
||||
|
||||
class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'), models.Model, PermissionModelMixin):
|
||||
list_recipe = models.ForeignKey(ShoppingListRecipe, on_delete=models.CASCADE, null=True, blank=True) # TODO remove when shoppinglist is deprecated
|
||||
list_recipe = models.ForeignKey(ShoppingListRecipe, on_delete=models.CASCADE, null=True, blank=True, related_name='entries')
|
||||
food = models.ForeignKey(Food, on_delete=models.CASCADE)
|
||||
unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
ingredient = models.ForeignKey(Ingredient, on_delete=models.CASCADE, null=True, blank=True)
|
||||
|
@ -3,10 +3,11 @@ from rest_framework.schemas.utils import is_list_view
|
||||
|
||||
|
||||
class QueryParam(object):
|
||||
def __init__(self, name, description=None, qtype='string'):
|
||||
def __init__(self, name, description=None, qtype='string', required=False):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.qtype = qtype
|
||||
self.required = required
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.name}, {self.qtype}, {self.description}'
|
||||
@ -19,7 +20,7 @@ class QueryParamAutoSchema(AutoSchema):
|
||||
parameters = super().get_path_parameters(path, method)
|
||||
for q in self.view.query_params:
|
||||
parameters.append({
|
||||
"name": q.name, "in": "query", "required": False,
|
||||
"name": q.name, "in": "query", "required": q.required,
|
||||
"description": q.description,
|
||||
'schema': {'type': q.qtype, },
|
||||
})
|
||||
|
@ -36,12 +36,17 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
|
||||
except KeyError:
|
||||
api_serializer = None
|
||||
# extended values are computationally expensive and not needed in normal circumstances
|
||||
if self.context.get('request', False) and bool(int(self.context['request'].query_params.get('extended', False))) and self.__class__ == api_serializer:
|
||||
return fields
|
||||
else:
|
||||
try:
|
||||
if bool(int(self.context['request'].query_params.get('extended', False))) and self.__class__ == api_serializer:
|
||||
return fields
|
||||
except (AttributeError, KeyError) as e:
|
||||
pass
|
||||
try:
|
||||
del fields['image']
|
||||
del fields['numrecipe']
|
||||
return fields
|
||||
except KeyError:
|
||||
pass
|
||||
return fields
|
||||
|
||||
def get_image(self, obj):
|
||||
# TODO add caching
|
||||
@ -286,8 +291,9 @@ class KeywordSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
|
||||
class Meta:
|
||||
model = Keyword
|
||||
fields = (
|
||||
'id', 'name', 'icon', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe')
|
||||
read_only_fields = ('id', 'label', 'image', 'parent', 'numchild', 'numrecipe')
|
||||
'id', 'name', 'icon', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe', 'created_at',
|
||||
'updated_at')
|
||||
read_only_fields = ('id', 'label', 'numchild', 'parent', 'image')
|
||||
|
||||
|
||||
class UnitSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
|
||||
@ -368,15 +374,6 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
||||
def get_shopping_status(self, obj):
|
||||
return ShoppingListEntry.objects.filter(space=obj.space, food=obj, checked=False).count() > 0
|
||||
|
||||
def get_fields(self, *args, **kwargs):
|
||||
fields = super().get_fields(*args, **kwargs)
|
||||
print('food', self.__class__, self.parent.__class__)
|
||||
# extended values are computationally expensive and not needed in normal circumstances
|
||||
if not bool(int(self.context['request'].query_params.get('extended', False))) or not self.parent:
|
||||
del fields['image']
|
||||
del fields['numrecipe']
|
||||
return fields
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['name'] = validated_data['name'].strip()
|
||||
validated_data['space'] = self.context['request'].space
|
||||
@ -633,6 +630,7 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
|
||||
read_only_fields = ('created_by',)
|
||||
|
||||
|
||||
# TODO deprecate
|
||||
class ShoppingListRecipeSerializer(serializers.ModelSerializer):
|
||||
name = serializers.SerializerMethodField('get_name') # should this be done at the front end?
|
||||
recipe_name = serializers.ReadOnlyField(source='recipe.name')
|
||||
@ -698,8 +696,8 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
|
||||
def run_validation(self, data):
|
||||
if (
|
||||
data.get('checked', False)
|
||||
and not (id := data.get('id', None))
|
||||
and not ShoppingListEntry.objects.get(id=id).checked
|
||||
and self.root.instance
|
||||
and not self.root.instance.checked
|
||||
):
|
||||
# if checked flips from false to true set completed datetime
|
||||
data['completed_at'] = timezone.now()
|
||||
@ -711,20 +709,6 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
|
||||
if 'completed_at' in data:
|
||||
del data['completed_at']
|
||||
|
||||
############################################################
|
||||
# temporary while old and new shopping lists are both in use
|
||||
try:
|
||||
# this serializer is the parent serializer for the API
|
||||
api_serializer = self.context['view'].serializer_class
|
||||
except Exception:
|
||||
# this serializer is probably nested or a foreign key
|
||||
api_serializer = None
|
||||
if self.context['request'].method == 'POST' and not self.__class__ == api_serializer:
|
||||
data['space'] = self.context['request'].space.id
|
||||
data['created_by'] = self.context['request'].user.id
|
||||
############################################################
|
||||
if self.context['request'].method == 'POST' and self.__class__ == api_serializer:
|
||||
data['created_by'] = {'id': self.context['request'].user.id}
|
||||
return super().run_validation(data)
|
||||
|
||||
def create(self, validated_data):
|
||||
|
@ -119,8 +119,13 @@ def test_delete(u1_s1, u1_s2, obj_1):
|
||||
assert r.status_code == 204
|
||||
|
||||
|
||||
# test sharing
|
||||
# test completed entries still visible if today, but not yesterday
|
||||
# test create shopping list from recipe
|
||||
# test create shopping list from mealplan
|
||||
# test create shopping list from recipe, excluding ingredients
|
||||
# TODO test sharing
|
||||
# TODO test completed entries still visible if today, but not yesterday
|
||||
# TODO test create shopping list from recipe
|
||||
# TODO test delete shopping list from recipe - include created by, shared with and not shared with
|
||||
# TODO test create shopping list from food
|
||||
# TODO test delete shopping list from food - include created by, shared with and not shared with
|
||||
# TODO test create shopping list from mealplan
|
||||
# TODO test create shopping list from recipe, excluding ingredients
|
||||
# TODO test auto creating shopping list from meal plan
|
||||
# TODO test excluding on-hand when auto creating shopping list
|
||||
|
27502
vue/package-lock.json
generated
Normal file
27502
vue/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -84,4 +84,5 @@
|
||||
"@vue/cli-plugin-pwa/workbox-webpack-plugin": "^5.1.3",
|
||||
"coa": "2.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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>
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
/**
|
||||
|
@ -47,7 +47,7 @@ module.exports = {
|
||||
pages: pages,
|
||||
filenameHashing: false,
|
||||
productionSourceMap: false,
|
||||
publicPath: process.env.NODE_ENV === "production" ? "/static/vue" : "http://localhost:8080/",
|
||||
publicPath: process.env.NODE_ENV === "production" ? "" : "http://localhost:8080/",
|
||||
outputDir: "../cookbook/static/vue/",
|
||||
runtimeCompiler: true,
|
||||
pwa: {
|
||||
|
Loading…
Reference in New Issue
Block a user