Merge remote-tracking branch 'origin/develop' into Auto-Planner

# Conflicts:
#	vue/src/apps/MealPlanView/MealPlanView.vue
This commit is contained in:
AquaticLava
2023-05-17 21:22:26 -06:00
208 changed files with 27215 additions and 28548 deletions

View File

@@ -0,0 +1,100 @@
<template>
<!-- bottom button nav -->
<div class="fixed-bottom p-1 pt-2 pl-2 pr-2 border-top text-center d-lg-none" style="background: white">
<div class="d-flex flex-row justify-content-around">
<div class="flex-column" v-if="show_button_1">
<slot name="button_1">
<a class="nav-link bottom-nav-link p-0" v-bind:href="resolveDjangoUrl('view_search')">
<i class="fas fa-fw fa-book " style="font-size: 1.5em"></i><br/><small>{{ $t('Recipes') }}</small></a> <!-- TODO localize -->
</slot>
</div>
<div class="flex-column" v-if="show_button_2">
<slot name="button_2">
<a class="nav-link bottom-nav-link p-0" v-bind:href="resolveDjangoUrl('view_plan')">
<i class="fas fa-calendar-alt" style="font-size: 1.5em"></i><br/><small>{{ $t('Meal_Plan') }}</small></a>
</slot>
</div>
<div class="flex-column" v-if="show_button_create">
<slot name="button_create">
<div class="dropup">
<a class="nav-link bottom-nav-link p-0" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false"><i class="fas fa-plus-circle fa-2x bottom-nav-link"></i>
</a>
<div class="dropdown-menu center-dropup" aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item" v-bind:href="resolveDjangoUrl('new_recipe')"><i
class="fas fa-fw fa-plus"></i> {{ $t('Create Recipe') }}</a>
<a class="dropdown-item" v-bind:href="resolveDjangoUrl('data_import_url')"><i
class="fas fa-fw fa-file-import"></i> {{ $t('Import Recipe') }}</a>
<div class="dropdown-divider" v-if="create_links.length > 0"></div>
<slot name="custom_create_functions">
</slot>
<a class="dropdown-item" v-bind:href="cl.url" v-for="cl in create_links" v-bind:key="cl.label">
<i :class="cl.icon + ' fa-fw'"></i> {{ cl.label }}
</a>
</div>
</div>
</slot>
</div>
<div class="flex-column" v-if="show_button_3">
<slot name="button_3">
<a class="nav-link bottom-nav-link p-0" v-bind:href="resolveDjangoUrl('view_shopping')">
<i class="fas fa-shopping-cart" style="font-size: 1.5em"></i><br/><small>{{ $t('Shopping_list') }}</small></a>
</slot>
</div>
<div class="flex-column">
<slot name="button_4" v-if="show_button_4">
<a class="nav-link bottom-nav-link p-0" v-bind:href="resolveDjangoUrl('view_books')">
<i class="fas fa-book-open" style="font-size: 1.5em"></i><br/><small>{{ $t('Books') }}</small></a> <!-- TODO localize -->
</slot>
</div>
</div>
</div>
</template>
<script>
import {ResolveUrlMixin} from "@/utils/utils";
export default {
name: "BottomNavigationBar",
mixins: [ResolveUrlMixin],
props: {
create_links: {
type: Array, default() {
return []
}
},
show_button_1: {type: Boolean, default: true},
show_button_2: {type: Boolean, default: true},
show_button_3: {type: Boolean, default: true},
show_button_4: {type: Boolean, default: true},
show_button_create: {type: Boolean, default: true},
}
}
</script>
<style scoped>
.bottom-nav-link {
color: #666666
}
.center-dropup {
right: auto;
left: 50%;
-webkit-transform: translate(-50%, 0);
-o-transform: translate(-50%, 0);
transform: translate(-50%, 0);
}
</style>

View File

@@ -95,7 +95,7 @@ export default {
<style scoped>
.context-menu {
position: fixed;
z-index: 999;
z-index: 5000;
overflow: hidden;
background: #fff;
border-radius: 4px;

View File

@@ -12,7 +12,7 @@
<cookbook-edit-card :book="book" v-if="current_page === 1" v-on:editing="cookbook_editing = $event" v-on:refresh="$emit('refresh')" @reload="$emit('reload')"></cookbook-edit-card>
</transition>
<transition name="flip" mode="out-in">
<recipe-card :recipe="display_recipes[0].recipe_content" v-if="current_page > 1" :key="display_recipes[0].recipe" :use_plural="use_plural"></recipe-card>
<recipe-card :recipe="display_recipes[0].recipe_content" v-if="current_page > 1" :key="display_recipes[0].recipe" ></recipe-card>
</transition>
</div>
<div class="col-md-5">

View File

@@ -23,9 +23,9 @@
<b-card-body class="m-0 py-0">
<b-card-text class="h-100 my-0 d-flex flex-column" style="text-overflow: ellipsis">
<h5 class="m-0 mt-1 text-truncate">{{ item[title] }}</h5>
<template v-if="use_plural">
<div v-if="item[plural] !== '' && item[plural] !== null" class="m-0 text-truncate">({{ $t("plural_short") }}: {{ item[plural] }})</div>
</template>
<div v-if="item[plural]!== '' && item[plural] !== null && item[plural] !== undefined" class="m-0 text-truncate">({{ $t("plural_short") }}: {{ item[plural] }})</div>
<div class="m-0 text-truncate">{{ item[subtitle] }}</div>
<div class="m-0 text-truncate small text-muted" v-if="getFullname">{{ getFullname }}</div>

View File

@@ -155,8 +155,9 @@ export default {
pageSize: this.limit,
query: query,
limit: this.limit,
options: {query: {simple: 1}}, // for API endpoints that support a simple view
}
console.log(query, options)
this.genericAPI(this.model, this.Actions.LIST, options).then((result) => {
this.objects = this.sticky_options.concat(result.data?.results ?? result.data)
if (this.nothingSelected && this.objects.length > 0) {

View File

@@ -12,21 +12,17 @@
<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 && !ingredient.no_amount"
v-html="calculateAmount(ingredient.amount)"></span>
<span v-if="ingredient.amount !== 0 && !ingredient.no_amount" v-html="calculateAmount(ingredient.amount)"></span>
</td>
<td @click="done">
<template v-if="ingredient.unit !== null && !ingredient.no_amount">
<template v-if="!use_plural">
<span>{{ ingredient.unit.name }}</span>
</template>
<template v-else>
<template>
<template v-if="ingredient.unit.plural_name === '' || ingredient.unit.plural_name === null">
<span>{{ ingredient.unit.name }}</span>
</template>
<template v-else>
<span v-if="ingredient.always_use_plural_unit">{{ ingredient.unit.plural_name}}</span>
<span v-else-if="(ingredient.amount * this.ingredient_factor) > 1">{{ ingredient.unit.plural_name }}</span>
<span v-if="ingredient.always_use_plural_unit">{{ ingredient.unit.plural_name }}</span>
<span v-else-if="ingredient.amount * this.ingredient_factor > 1">{{ ingredient.unit.plural_name }}</span>
<span v-else>{{ ingredient.unit.name }}</span>
</template>
</template>
@@ -34,21 +30,18 @@
</td>
<td @click="done">
<template v-if="ingredient.food !== null">
<a :href="resolveDjangoUrl('view_recipe', ingredient.food.recipe.id)"
v-if="ingredient.food.recipe !== null" target="_blank"
rel="noopener noreferrer">{{ ingredient.food.name }}</a>
<a :href="resolveDjangoUrl('view_recipe', ingredient.food.recipe.id)" v-if="ingredient.food.recipe !== null" target="_blank" rel="noopener noreferrer">{{
ingredient.food.name
}}</a>
<template v-if="ingredient.food.recipe === null">
<template v-if="!use_plural">
<span>{{ ingredient.food.name }}</span>
</template>
<template v-else>
<template>
<template v-if="ingredient.food.plural_name === '' || ingredient.food.plural_name === null">
<span>{{ ingredient.food.name }}</span>
</template>
<template v-else>
<span v-if="ingredient.always_use_plural_food">{{ ingredient.food.plural_name }}</span>
<span v-else-if="ingredient.no_amount">{{ ingredient.food.name }}</span>
<span v-else-if="(ingredient.amount * this.ingredient_factor) > 1">{{ ingredient.food.plural_name }}</span>
<span v-else-if="ingredient.amount * this.ingredient_factor > 1">{{ ingredient.food.plural_name }}</span>
<span v-else>{{ ingredient.food.name }}</span>
</template>
</template>
@@ -56,36 +49,32 @@
</template>
</td>
<td v-if="detailed">
<div v-if="ingredient.note">
<span v-b-popover.hover="ingredient.note" class="d-print-none touchable p-0 pl-md-2 pr-md-2">
<template v-if="ingredient.note">
<span v-b-popover.hover="ingredient.note" class="d-print-none touchable py-0 px-2">
<i class="far fa-comment"></i>
</span>
<div class="d-none d-print-block"><i class="far fa-comment-alt d-print-none"></i> {{
ingredient.note
}}
</div>
</div>
<div class="d-none d-print-block"><i class="far fa-comment-alt d-print-none"></i> {{ ingredient.note }}</div>
</template>
</td>
</template>
</tr>
</template>
<script>
import {calculateAmount, ResolveUrlMixin} from "@/utils/utils"
import { calculateAmount, ResolveUrlMixin } from "@/utils/utils"
import Vue from "vue"
import VueSanitize from "vue-sanitize";
import VueSanitize from "vue-sanitize"
Vue.use(VueSanitize);
Vue.use(VueSanitize)
export default {
name: "IngredientComponent",
props: {
ingredient: Object,
ingredient_factor: {type: Number, default: 1},
use_plural:{type: Boolean, default: false},
detailed: {type: Boolean, default: true},
ingredient_factor: { type: Number, default: 1 },
detailed: { type: Boolean, default: true },
},
mixins: [ResolveUrlMixin],
data() {
@@ -94,9 +83,7 @@ export default {
}
},
watch: {},
mounted() {
},
mounted() {},
methods: {
calculateAmount: function (x) {
return this.$sanitize(calculateAmount(x, this.ingredient_factor))
@@ -112,9 +99,9 @@ export default {
<style scoped>
/* increase size of hover/touchable space without changing spacing */
.touchable {
padding-right: 2em;
padding-left: 2em;
margin-right: -2em;
margin-left: -2em;
/* padding-right: 2em;
padding-left: 2em; */
margin-right: -1em;
margin-left: -1em;
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div v-if="recipe.keywords.length > 0">
<span :key="k.id" v-for="k in recipe.keywords.filter((kk) => { return kk.show || kk.show === undefined })" class="pl-1">
<span :key="k.id" v-for="k in recipe.keywords.slice(0,keyword_splice).filter((kk) => { return kk.show || kk.show === undefined })" class="pl-1">
<a :href="`${resolveDjangoUrl('view_search')}?keyword=${k.id}`"><b-badge pill variant="light"
class="font-weight-normal">{{ k.label }}</b-badge></a>
@@ -17,6 +17,15 @@ export default {
mixins: [ResolveUrlMixin],
props: {
recipe: Object,
limit: Number,
},
computed: {
keyword_splice: function (){
if(this.limit){
return this.limit
}
return this.recipe.keywords.lenght
}
}
}
</script>

View File

@@ -1,114 +1,148 @@
<template>
<b-modal :id="modal_id" size="lg" :title="modal_title" hide-footer aria-label="" @show="showModal">
<div class="row">
<div class="col col-md-12">
<div class="row">
<div class="col col-md-12">
<div class="row">
<div class="col-6 col-lg-9">
<b-input-group>
<b-form-input id="TitleInput" v-model="entryEditing.title" :placeholder="entryEditing.title_placeholder" @change="missing_recipe = false"></b-form-input>
<b-input-group-append class="d-none d-lg-block">
<b-button variant="primary" @click="entryEditing.title = ''"><i class="fa fa-eraser"></i></b-button>
</b-input-group-append>
</b-input-group>
<span class="text-danger" v-if="missing_recipe">{{ $t("Title_or_Recipe_Required") }}</span>
<small tabindex="-1" class="form-text text-muted" v-if="!missing_recipe">{{ $t("Title") }}</small>
<div>
<b-modal :id="modal_id" size="lg" :title="modal_title" hide-footer aria-label="" @show="showModal">
<div class="row" v-if="entryEditing !== null">
<div class="col col-md-12">
<div class="row">
<div class="col col-md-12">
<div class="row">
<div class="col-6 col-lg-9">
<b-input-group>
<b-form-input id="TitleInput" v-model="entryEditing.title"
:placeholder="entryEditing.title_placeholder"
@change="missing_recipe = false"></b-form-input>
<b-input-group-append class="d-none d-lg-block">
<b-button variant="primary" @click="entryEditing.title = ''"><i
class="fa fa-eraser"></i></b-button>
</b-input-group-append>
</b-input-group>
<span class="text-danger" v-if="missing_recipe">{{
$t("Title_or_Recipe_Required")
}}</span>
<small tabindex="-1" class="form-text text-muted" v-if="!missing_recipe">{{
$t("Title")
}}</small>
</div>
<div class="col-6 col-lg-3">
<input type="date" id="DateInput" class="form-control" v-model="entryEditing.date"/>
<small tabindex="-1" class="form-text text-muted">{{ $t("Date") }}</small>
</div>
</div>
<div class="col-6 col-lg-3">
<input type="date" id="DateInput" class="form-control" v-model="entryEditing.date" />
<small tabindex="-1" class="form-text text-muted">{{ $t("Date") }}</small>
<div class="row mt-3">
<div class="col-12 col-lg-6 col-xl-6">
<b-form-group>
<generic-multiselect
@change="selectRecipe"
:initial_single_selection="entryEditing.recipe"
:label="'name'"
:model="Models.RECIPE"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
v-bind:placeholder="$t('Recipe')"
:limit="10"
:multiple="false"
></generic-multiselect>
<small tabindex="-1" class="form-text text-muted">{{ $t("Recipe") }}</small>
</b-form-group>
<b-form-group class="mt-3">
<generic-multiselect
required
@change="selectMealType"
:label="'name'"
:model="Models.MEAL_TYPE"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
v-bind:placeholder="$t('Meal_Type')"
:limit="10"
:multiple="false"
:initial_single_selection="entryEditing.meal_type"
:allow_create="true"
:create_placeholder="$t('Create_New_Meal_Type')"
></generic-multiselect>
<span class="text-danger" v-if="missing_meal_type">{{
$t("Meal_Type_Required")
}}</span>
<small tabindex="-1" class="form-text text-muted"
v-if="!missing_meal_type">{{ $t("Meal_Type") }}</small>
</b-form-group>
<b-form-group label-for="NoteInput" :description="$t('Note')" class="mt-3">
<textarea class="form-control" id="NoteInput" v-model="entryEditing.note"
:placeholder="$t('Note')"></textarea>
</b-form-group>
<b-input-group>
<b-form-input id="ServingsInput" v-model="entryEditing.servings"
:placeholder="$t('Servings')"></b-form-input>
</b-input-group>
<small tabindex="-1" class="form-text text-muted">{{ $t("Servings") }}</small>
<b-form-group class="mt-3">
<generic-multiselect
required
@change="entryEditing.shared = $event.val"
parent_variable="entryEditing.shared"
:label="'display_name'"
:model="Models.USER_NAME"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
v-bind:placeholder="$t('Share')"
:limit="10"
:multiple="true"
:initial_selection="entryEditing.shared"
></generic-multiselect>
<small tabindex="-1" class="form-text text-muted">{{ $t("Share") }}</small>
</b-form-group>
<b-input-group v-if="!autoMealPlan">
<b-form-checkbox id="AddToShopping" v-model="mealplan_settings.addshopping"/>
<small tabindex="-1" class="form-text text-muted">{{
$t("AddToShopping")
}}</small>
</b-input-group>
<b-input-group v-if="mealplan_settings.addshopping && !autoMealPlan">
<b-form-checkbox id="reviewShopping"
v-model="mealplan_settings.reviewshopping"/>
<small tabindex="-1" class="form-text text-muted">{{
$t("review_shopping")
}}</small>
</b-input-group>
</div>
<div class="col-lg-6 d-none d-lg-block d-xl-block">
<recipe-card v-if="entryEditing.recipe" :recipe="entryEditing.recipe"
:detailed="false"></recipe-card>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12 col-lg-6 col-xl-6">
<b-form-group>
<generic-multiselect
@change="selectRecipe"
:initial_single_selection="entryEditing.recipe"
:label="'name'"
:model="Models.RECIPE"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
v-bind:placeholder="$t('Recipe')"
:limit="10"
:multiple="false"
></generic-multiselect>
<small tabindex="-1" class="form-text text-muted">{{ $t("Recipe") }}</small>
</b-form-group>
<b-form-group class="mt-3">
<generic-multiselect
required
@change="selectMealType"
:label="'name'"
:model="Models.MEAL_TYPE"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
v-bind:placeholder="$t('Meal_Type')"
:limit="10"
:multiple="false"
:initial_single_selection="entryEditing.meal_type"
:allow_create="true"
:create_placeholder="$t('Create_New_Meal_Type')"
></generic-multiselect>
<span class="text-danger" v-if="missing_meal_type">{{ $t("Meal_Type_Required") }}</span>
<small tabindex="-1" class="form-text text-muted" v-if="!missing_meal_type">{{ $t("Meal_Type") }}</small>
</b-form-group>
<b-form-group label-for="NoteInput" :description="$t('Note')" class="mt-3">
<textarea class="form-control" id="NoteInput" v-model="entryEditing.note" :placeholder="$t('Note')"></textarea>
</b-form-group>
<b-input-group>
<b-form-input id="ServingsInput" v-model="entryEditing.servings" :placeholder="$t('Servings')"></b-form-input>
</b-input-group>
<small tabindex="-1" class="form-text text-muted">{{ $t("Servings") }}</small>
<b-form-group class="mt-3">
<generic-multiselect
required
@change="entryEditing.shared = $event.val"
parent_variable="entryEditing.shared"
:label="'display_name'"
:model="Models.USER_NAME"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
v-bind:placeholder="$t('Share')"
:limit="10"
:multiple="true"
:initial_selection="entryEditing.shared"
></generic-multiselect>
<small tabindex="-1" class="form-text text-muted">{{ $t("Share") }}</small>
</b-form-group>
<b-input-group v-if="!autoMealPlan">
<b-form-checkbox id="AddToShopping" v-model="mealplan_settings.addshopping" />
<small tabindex="-1" class="form-text text-muted">{{ $t("AddToShopping") }}</small>
</b-input-group>
<b-input-group v-if="mealplan_settings.addshopping">
<b-form-checkbox id="reviewShopping" v-model="mealplan_settings.reviewshopping" />
<small tabindex="-1" class="form-text text-muted">{{ $t("review_shopping") }}</small>
</b-input-group>
</div>
<div class="col-lg-6 d-none d-lg-block d-xl-block">
<recipe-card v-if="entryEditing.recipe" :recipe="entryEditing.recipe" :detailed="false" :use_plural="use_plural"></recipe-card>
</div>
</div>
<div class="row mt-3 mb-3">
<div class="col-12">
<b-button variant="danger" @click="deleteEntry" v-if="allow_delete">{{ $t("Delete") }} </b-button>
<b-button class="float-right" variant="primary" @click="editEntry">{{ $t("Save") }}</b-button>
<div class="row mt-3 mb-3">
<div class="col-12">
<b-button variant="danger" @click="deleteEntry" v-if="allow_delete">{{
$t("Delete")
}}
</b-button>
<b-button class="float-right" variant="primary" @click="editEntry">{{
$t("Save")
}}
</b-button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</b-modal>
</b-modal>
<shopping-modal :recipe="last_created_plan.recipe" :servings="last_created_plan.servings" :modal_id="999999"
:mealplan="last_created_plan" v-if="last_created_plan !== null && last_created_plan.recipe !== null"/>
</div>
</template>
<script>
import Vue from "vue"
import VueCookies from "vue-cookies"
import { BootstrapVue } from "bootstrap-vue"
import {BootstrapVue} from "bootstrap-vue"
import GenericMultiselect from "@/components/GenericMultiselect"
import { ApiMixin, getUserPreference } from "@/utils/utils"
import {ApiMixin, getUserPreference} from "@/utils/utils"
const { ApiApiFactory } = require("@/utils/openapi/api")
const { StandardToasts } = require("@/utils/utils")
const {ApiApiFactory} = require("@/utils/openapi/api")
const {StandardToasts} = require("@/utils/utils")
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import {useMealPlanStore} from "@/stores/MealPlanStore";
import ShoppingModal from "@/components/Modals/ShoppingModal.vue";
Vue.use(BootstrapVue)
Vue.use(VueCookies)
@@ -118,11 +152,11 @@ export default {
name: "MealPlanEditModal",
props: {
entry: Object,
entryEditing_inital_servings: Number,
create_date: String,
modal_title: String,
modal_id: {
type: String,
default: "edit-modal",
default: "id_meal_plan_edit_modal",
},
allow_delete: {
type: Boolean,
@@ -133,10 +167,11 @@ export default {
components: {
GenericMultiselect,
RecipeCard: () => import("@/components/RecipeCard.vue"),
ShoppingModal,
},
data() {
return {
entryEditing: {},
entryEditing: null,
missing_recipe: false,
missing_meal_type: false,
default_plan_share: [],
@@ -144,22 +179,19 @@ export default {
addshopping: false,
reviewshopping: false,
},
use_plural: false,
last_created_plan: null,
}
},
watch: {
entry: {
handler() {
this.entryEditing = Object.assign({}, this.entry)
if (this.entryEditing_inital_servings) {
this.entryEditing.servings = this.entryEditing_inital_servings
}
},
deep: true,
},
entryEditing: {
handler(newVal) {},
handler(newVal) {
},
deep: true,
},
mealplan_settings: {
@@ -168,19 +200,13 @@ export default {
},
deep: true,
},
entryEditing_inital_servings: function (newVal) {
this.entryEditing.servings = newVal
},
},
mounted: function () {
let apiClient = new ApiApiFactory()
apiClient.retrieveSpace(window.ACTIVE_SPACE_ID).then(r => {
this.use_plural = r.data.use_plural
})
useUserPreferenceStore().updateIfStaleOrEmpty()
},
computed: {
autoMealPlan: function () {
return getUserPreference("mealplan_autoadd_shopping")
return useUserPreferenceStore().getStaleData()?.mealplan_autoadd_shopping
},
},
methods: {
@@ -188,35 +214,53 @@ export default {
if (this.$cookies.isKey(MEALPLAN_COOKIE_NAME)) {
this.mealplan_settings = Object.assign({}, this.mealplan_settings, this.$cookies.get(MEALPLAN_COOKIE_NAME))
}
let apiClient = new ApiApiFactory()
apiClient.listUserPreferences().then((result) => {
if (this.entry.id === -1) {
this.entryEditing.shared = result.data[0].plan_share
if (this.entry === null) {
this.entryEditing = Object.assign({}, useMealPlanStore().empty_meal_plan, null)
} else {
this.entryEditing = Object.assign({}, this.entry, null)
}
if (this.create_date) {
this.entryEditing.date = this.create_date
}
useUserPreferenceStore().getData().then(userPreference => {
if (this.entryEditing.id === -1) {
this.entryEditing.shared = userPreference.plan_share
}
})
},
editEntry() {
this.missing_meal_type = false
this.missing_recipe = false
let cancel = false
if (this.entryEditing.meal_type == null) {
this.missing_meal_type = true
cancel = true
return;
}
if (this.entryEditing.recipe == null && this.entryEditing.title === "") {
this.missing_recipe = true
cancel = true
return
}
if (!cancel) {
this.$bvModal.hide(`edit-modal`)
this.$emit("save-entry", { ...this.mealplan_settings, ...this.entryEditing, ...{ addshopping: this.mealplan_settings.addshopping && !this.autoMealPlan } })
//TODO properly validate
this.$bvModal.hide(this.modal_id)
// only set addshopping if review is not enabled
this.$set(this.entryEditing, 'addshopping', (this.mealplan_settings.addshopping && !this.mealplan_settings.reviewshopping))
if (!('id' in this.entryEditing) || this.entryEditing.id === -1) {
useMealPlanStore().createObject(this.entryEditing).then((r) => {
this.last_created_plan = r.data
if (r.data.recipe && this.mealplan_settings.addshopping && !this.autoMealPlan && this.mealplan_settings.reviewshopping) {
this.$nextTick(function () {
this.$bvModal.show(`shopping_999999`)
})
}
})
} else {
useMealPlanStore().updateObject(this.entryEditing)
}
},
deleteEntry() {
this.$bvModal.hide(`edit-modal`)
this.$emit("delete-entry", this.entryEditing)
this.$bvModal.hide(this.modal_id)
useMealPlanStore().deleteObject(this.entryEditing)
},
selectMealType(event) {
this.missing_meal_type = false

View File

@@ -3,69 +3,52 @@
<b-form-group
v-bind:label="label"
class="mb-3">
<twemoji-textarea
:ref="'_edit_' + id"
:initialContent="value"
:emojiData="emojiDataAll"
:emojiGroups="emojiGroups"
triggerType="click"
:recentEmojisFeat="true"
recentEmojisStorage="local"
@contentChanged="setIcon"
/>
<input class="form-control" v-model="new_value">
<Picker :data="emojiIndex" :ref="'_edit_' + id" :native="true"
@select="setIcon"/>
</b-form-group>
</div>
</template>
<script>
import {TwemojiTextarea} from '@kevinfaguiar/vue-twemoji-picker';
// TODO add localization
import EmojiAllData from '@kevinfaguiar/vue-twemoji-picker/emoji-data/en/emoji-all-groups.json';
import EmojiGroups from '@kevinfaguiar/vue-twemoji-picker/emoji-data/emoji-groups.json';
import data from "emoji-mart-vue-fast/data/all.json";
import "emoji-mart-vue-fast/css/emoji-mart.css";
import {Picker, EmojiIndex} from "emoji-mart-vue-fast";
let emojiIndex = new EmojiIndex(data);
export default {
name: 'EmojiInput',
components: {TwemojiTextarea},
props: {
field: {type: String, default: 'You Forgot To Set Field Name'},
label: {type: String, default: ''},
value: {type: String, default: ''},
},
data() {
return {
new_value: undefined,
id: null
name: 'EmojiInput',
components: {Picker},
props: {
field: {type: String, default: 'You Forgot To Set Field Name'},
label: {type: String, default: ''},
value: {type: String, default: ''},
},
data() {
return {
new_value: undefined,
id: null,
emojiIndex: emojiIndex,
emojisOutput: ""
}
},
watch: {
'new_value': function () {
this.$root.$emit('change', this.field, this.new_value ?? null)
},
},
mounted() {
this.id = this._uid
},
methods: {
setIcon: function (icon) {
console.log(icon)
this.new_value = icon.native
},
}
},
computed: {
// modelName() {
// return this?.model?.name ?? this.$t('Search')
// },
emojiDataAll() {
return EmojiAllData;
},
emojiGroups() {
return EmojiGroups;
}
},
watch: {
'new_value': function () {
this.$root.$emit('change', this.field, this.new_value ?? null)
},
},
mounted() {
this.id = this._uid
},
methods: {
prepareEmoji: function() {
this.$refs['_edit_' + this.id].addText(this.this_item.icon || '');
this.$refs['_edit_' + this.id].blur()
document.getElementById('btn-emoji-default').disabled = true;
},
setIcon: function(icon) {
this.new_value = icon
},
}
}
</script>

View File

@@ -15,6 +15,7 @@
<file-input v-if="visibleCondition(f, 'file')" :label="f.label" :value="f.value" :field="f.field" @change="storeValue" />
<small-text v-if="visibleCondition(f, 'smalltext')" :value="f.value" />
<date-input v-if="visibleCondition(f, 'date')" :label="f.label" :value="f.value" :field="f.field" :help="showHelp && f.help" :subtitle="f.subtitle" />
<number-input v-if="visibleCondition(f, 'number')" :label="f.label" :value="f.value" :field="f.field" :placeholder="f.placeholder" :help="showHelp && f.help" :subtitle="f.subtitle" />
</div>
<template v-slot:modal-footer>
<div class="row w-100">
@@ -49,10 +50,11 @@ import ChoiceInput from "@/components/Modals/ChoiceInput"
import FileInput from "@/components/Modals/FileInput"
import SmallText from "@/components/Modals/SmallText"
import HelpBadge from "@/components/Badges/Help"
import NumberInput from "@/components/Modals/NumberInput.vue";
export default {
name: "GenericModalForm",
components: { FileInput, CheckboxInput, LookupInput, TextInput, EmojiInput, ChoiceInput, SmallText, HelpBadge,DateInput },
components: { FileInput, CheckboxInput, LookupInput, TextInput, EmojiInput, ChoiceInput, SmallText, HelpBadge,DateInput, NumberInput },
mixins: [ApiMixin, ToastMixin],
props: {
model: { required: true, type: Object },

View File

@@ -0,0 +1,101 @@
<template>
<div>
<b-button v-b-modal.id_import_tandoor_modal>{{ $t("Import into Tandoor") }}</b-button>
<b-modal class="modal" id="id_import_tandoor_modal" :title="$t('Import')" hide-footer>
<p>Tandoor ist eine OpenSource Rezeptverwaltungs Plattform</p>
<p>Bitte wähle aus ob du deinen eigenen Tandoor Server hast oder tandoor.dev nutzt.
</p>
<div class="justify-content-center text-center">
<b-form-group v-slot="{ ariaDescribedby }">
<b-form-radio-group
id="btn-radios-1"
v-model="import_mode"
:options="options"
:aria-describedby="ariaDescribedby"
name="radios-btn-default"
buttons
></b-form-radio-group>
</b-form-group>
</div>
<div v-if="import_mode === 'tandoor'">
<ol>
<li><a href="https://app.tandoor.dev/accounts/signup/" target="_blank" ref="nofollow">Hier</a> einen
Account anlegen<br/></li>
<li>
<b-button @click="importTandoor()">Import</b-button>
</li>
</ol>
</div>
<div v-if="import_mode === 'selfhosted'">
Deine Server URL (z.B. <code>https://tandoor.mydomain.com/</code>)
<b-input v-model="selfhosted_url"></b-input>
<b-button class="mt-2" :disabled="selfhosted_url === ''" @click="importSelfHosted()">Import</b-button>
</div>
<div class="row mt-3 text-left mb-3">
<p>Alternativ kannst du den Link zum Rezept in den Importer in deiner Tandoor Instanz kopieren.</p>
<a href="https://tandoor.dev" target="_blank" rel="nofollow">Jetzt mehr über Tandoor erfahren</a>
</div>
</b-modal>
</div>
</template>
<script>
import Vue from "vue";
import {BootstrapVue} from "bootstrap-vue";
import {ApiApiFactory} from "@/utils/openapi/api";
Vue.use(BootstrapVue)
export default {
name: 'ImportTandoor',
components: {},
props: {
recipe: Object,
},
data() {
return {
import_mode: 'tandoor',
options: [
{text: 'Tandoor.dev', value: 'tandoor'},
{text: 'Self-Hosted', value: 'selfhosted'},
],
selfhosted_url: '',
}
},
watch: {
selfhosted_url: function (newVal) {
window.localStorage.setItem('MY_TANDOOR_URL', newVal)
},
},
computed: {},
mounted() {
this.$i18n.locale = window.CUSTOM_LOCALE
let selfhosted_url = window.localStorage.getItem('MY_TANDOOR_URL')
if (selfhosted_url !== undefined) {
this.selfhosted_url = selfhosted_url
this.import_mode = 'selfhosted'
}
},
methods: {
importTandoor: function () {
location.href = 'https://app.tandoor.dev/data/import/url?url=' + location.href
},
importSelfHosted: function () {
this.selfhosted_url = this.selfhosted_url.replace('/search/', '')
let import_path = 'data/import/url?url='
if (!this.selfhosted_url.endsWith('/')) {
import_path = '/' + import_path
}
location.href = this.selfhosted_url + import_path + location.href
},
}
}
</script>

View File

@@ -0,0 +1,37 @@
<template>
<div>
<b-form-group v-bind:label="label" class="mb-3">
<b-form-input v-model="new_value" type="number" :placeholder="placeholder"></b-form-input>
<em v-if="help" class="small text-muted">{{ help }}</em>
<small v-if="subtitle" class="text-muted">{{ subtitle }}</small>
</b-form-group>
</div>
</template>
<script>
export default {
name: "TextInput",
props: {
field: { type: String, default: "You Forgot To Set Field Name" },
label: { type: String, default: "Text Field" },
value: { type: String, default: "" },
placeholder: { type: Number, default: 0 },
help: { type: String, default: undefined },
subtitle: { type: String, default: undefined },
},
data() {
return {
new_value: undefined,
}
},
mounted() {
this.new_value = this.value
},
watch: {
new_value: function () {
this.$root.$emit("change", this.field, this.new_value)
},
},
methods: {},
}
</script>

View File

@@ -10,12 +10,10 @@
</b-card-header>
<b-collapse id="accordion-0" class="p-2" visible accordion="my-accordion" role="tabpanel">
<div v-for="i in steps.flatMap(s => s.ingredients)" v-bind:key="i.id">
<div>
<table class="table table-sm mb-0">
<ingredient-component
:use_plural="true"
:key="i.id"
<ingredient-component v-for="i in steps.flatMap(s => s.ingredients)" v-bind:key="i.id"
:detailed="true"
:ingredient="i"
:ingredient_factor="ingredient_factor"
@@ -79,6 +77,7 @@ const {ApiApiFactory} = require("@/utils/openapi/api")
import {StandardToasts} from "@/utils/utils"
import IngredientComponent from "@/components/IngredientComponent"
import LoadingSpinner from "@/components/LoadingSpinner"
import {useMealPlanStore} from "@/stores/MealPlanStore";
// import CustomInputSpinButton from "@/components/CustomInputSpinButton"
export default {
@@ -89,7 +88,7 @@ export default {
recipe: {required: true, type: Object},
servings: {type: Number, default: undefined},
modal_id: {required: true, type: Number},
mealplan: {type: Number, default: undefined},
mealplan: {type: Object, default: undefined},
list_recipe: {type: Number, default: undefined},
},
data() {
@@ -128,7 +127,7 @@ export default {
if (!this.recipe_servings) {
this.recipe_servings = result.data?.servings
}
this.steps.forEach(s => s.ingredients.filter(i => i.food.food_onhand === false).forEach(i => this.$set(i, 'checked', true)))
this.steps.forEach(s => s.ingredients.filter(i => i.food?.food_onhand === false).forEach(i => this.$set(i, 'checked', true)))
this.loading = false
})
.then(() => {
@@ -170,6 +169,9 @@ export default {
.then((result) => {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_CREATE)
this.$emit("finish")
if (this.mealplan !== undefined && this.mealplan !== null){
useMealPlanStore().plans[this.mealplan.id].shopping = true
}
})
.catch((err) => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_CREATE, err)

View File

@@ -1,7 +1,7 @@
<template>
<div>
<template v-if="recipe && recipe.loading">
<b-card no-body v-hover>
<b-card no-body v-hover style="height: 100%">
<b-card-img-lazy style="height: 15vh; object-fit: cover" class="" :src="placeholder_image"
v-bind:alt="$t('Recipe_Image')" top></b-card-img-lazy>
@@ -19,54 +19,59 @@
</b-card>
</template>
<template v-else>
<b-card no-body v-hover v-if="recipe">
<b-card no-body v-hover v-if="recipe" style="height: 100%">
<a :href="this.recipe.id !== undefined ? resolveDjangoUrl('view_recipe', this.recipe.id) : null">
<b-card-img-lazy style="height: 15vh; object-fit: cover" class="" :src="recipe_image"
v-bind:alt="$t('Recipe_Image')" top></b-card-img-lazy>
<div
class="card-img-overlay h-100 d-flex flex-column justify-content-right float-right text-right pt-2 pr-1"
v-if="show_context_menu">
<a>
<recipe-context-menu :recipe="recipe" class="float-right" :disabled_options="context_disabled_options"
v-if="recipe !== null"></recipe-context-menu>
</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> {{ working_time }}
</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> {{ waiting_time }}
</b-badge>
<div class="content">
<div class="content-overlay" v-if="recipe.description !== null && recipe.description !== ''"></div>
<b-card-img-lazy style="height: 15vh; object-fit: cover" class="" :src="recipe_image"
v-bind:alt="$t('Recipe_Image')" top></b-card-img-lazy>
<div class="content-details" >
<p class="content-text">
{{ recipe.description }}
</p>
</div>
<div class="card-img-overlay d-flex flex-column justify-content-left float-left text-left pt-2" style="width:40%"
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> {{ working_time }}
</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> {{ waiting_time }}
</b-badge>
</div>
</div>
</a>
<b-card-body class="p-4">
<h6>
<a :href="this.recipe.id !== undefined ? resolveDjangoUrl('view_recipe', this.recipe.id) : null">
<b-card-body class="p-2 pl-3 pr-3">
<div class="d-flex flex-row">
<div class="flex-grow-1">
<a :href="this.recipe.id !== undefined ? resolveDjangoUrl('view_recipe', this.recipe.id) : null" class="text-body font-weight-bold two-row-text">
<template v-if="recipe !== null">{{ recipe.name }}</template>
<template v-else>{{ meal_plan.title }}</template>
</a>
</h6>
</div>
<div class="justify-content-end">
<recipe-context-menu :recipe="recipe" class="justify-content-end float-right align-items-end pr-0"
:disabled_options="context_disabled_options"
v-if="recipe !== null"></recipe-context-menu>
</div>
</div>
<b-card-text style="text-overflow: ellipsis">
<template v-if="recipe !== null">
<recipe-rating :recipe="recipe"></recipe-rating>
<template v-if="recipe.description !== null && recipe.description !== undefined">
<span v-if="recipe.description.length > text_length">
{{ recipe.description.substr(0, text_length) + "\u2026" }}
</span>
<span v-if="recipe.description.length <= text_length">
{{ recipe.description }}
</span>
</template>
<p class="mt-1">
<div v-if="show_detail">
{{ recipe.description }}
</div>
<p class="mt-1 mb-1">
<last-cooked :recipe="recipe"></last-cooked>
<keywords-component :recipe="recipe"
<keywords-component :recipe="recipe" :limit="3"
style="margin-top: 4px; position: relative; z-index: 3;"></keywords-component>
</p>
<transition name="fade" mode="in-out">
@@ -75,29 +80,46 @@
<h6 class="card-title"><i class="fas fa-pepper-hot"></i> {{ $t("Ingredients") }}
</h6>
<ingredients-card
:steps="recipe.steps"
:header="false"
:detailed="false"
:servings="recipe.servings"
:use_plural="use_plural" />
</div>
</div>
</transition>
<ingredients-card
:steps="recipe.steps"
:header="false"
:detailed="false"
:servings="recipe.servings"/>
</div>
</div>
</transition>
<b-badge pill variant="info" v-if="!recipe.internal">{{ $t("External") }}</b-badge>
</template>
<template v-else>{{ meal_plan.note }}</template>
</b-card-text>
</b-card-body>
<b-card-footer v-if="footer_text !== undefined"><i v-bind:class="footer_icon"></i> {{ footer_text }}
</b-card-footer>
</b-card>
</template>
</div>
<!--
<recipe-rating :recipe="recipe"></recipe-rating>
<template v-if="recipe.description !== null && recipe.description !== undefined">
<span v-if="recipe.description.length > text_length">
{{ recipe.description.substr(0, text_length) + "\u2026" }}
</span>
<span v-if="recipe.description.length <= text_length">
{{ recipe.description }}
</span>
</template>
<b-card-footer v-if="footer_text !== undefined"><i v-bind:class="footer_icon"></i> {{ footer_text }}
</b-card-footer>
<template v-else>{{ meal_plan.note }}</template>
-->
</template>
<script>
@@ -117,7 +139,6 @@ export default {
mixins: [ResolveUrlMixin],
components: {
LastCooked,
RecipeRating,
KeywordsComponent,
"recipe-context-menu": RecipeContextMenu,
IngredientsCard
@@ -125,7 +146,7 @@ export default {
props: {
recipe: Object,
meal_plan: Object,
use_plural: { type: Boolean, default: false},
use_plural: {type: Boolean, default: false},
footer_text: String,
footer_icon: String,
detailed: {type: Boolean, default: true},
@@ -144,13 +165,6 @@ export default {
show_detail: function () {
return this.recipe?.steps !== undefined && this.detailed
},
text_length: function () {
if (this.show_detail) {
return 200
} else {
return 120
}
},
recipe_image: function () {
if (this.recipe == null || this.recipe.image === null) {
return window.IMAGE_PLACEHOLDER
@@ -191,4 +205,79 @@ export default {
{
opacity: 0;
}
.two-row-text {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2; /* number of lines to show */
line-clamp: 2;
-webkit-box-orient: vertical;
}
.content {
position: relative;
margin: auto;
overflow: visible;
}
.content .content-overlay {
background: rgba(0, 0, 0, 0.7);
position: absolute;
height: 99%;
width: 100%;
left: 0;
top: 0;
bottom: 0;
right: 0;
opacity: 0;
-webkit-transition: all 0.4s ease-in-out 0s;
-moz-transition: all 0.4s ease-in-out 0s;
transition: all 0.4s ease-in-out 0s;
}
.content:hover .content-overlay {
opacity: 1;
}
.content-details {
position: absolute;
text-align: center;
padding-left: 1em;
padding-right: 1em;
width: 100%;
top: 50%;
left: 50%;
opacity: 0;
-webkit-transform: translate(-50%, -50%);
-moz-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
-webkit-transition: all 0.3s ease-in-out 0s;
-moz-transition: all 0.3s ease-in-out 0s;
transition: all 0.3s ease-in-out 0s;
}
.content:hover .content-details {
top: 50%;
left: 50%;
opacity: 1;
}
.content-details h3 {
color: #fff;
font-weight: 500;
letter-spacing: 0.15em;
margin-bottom: 0.5em;
text-transform: uppercase;
}
.content-details p {
color: #fff;
font-size: 0.8em;
}
.fadeIn-bottom {
top: 80%;
}
</style>

View File

@@ -1,12 +1,12 @@
<template>
<div>
<div class="dropdown d-print-none">
<a class="btn shadow-none" href="javascript:void(0);" role="button" id="dropdownMenuLink"
<a class="btn shadow-none pr-0 pl-0" href="javascript:void(0);" role="button" id="dropdownMenuLink"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-ellipsis-v fa-lg"></i>
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuLink">
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuLink" >
<a class="dropdown-item" :href="resolveDjangoUrl('edit_recipe', recipe.id)" v-if="!disabled_options.edit"><i
class="fas fa-pencil-alt fa-fw"></i> {{ $t("Edit") }}</a>
@@ -106,6 +106,7 @@ import ShoppingModal from "@/components/Modals/ShoppingModal"
import moment from "moment"
import Vue from "vue"
import {ApiApiFactory} from "@/utils/openapi/api"
import {useMealPlanStore} from "@/stores/MealPlanStore";
Vue.prototype.moment = moment
@@ -191,6 +192,7 @@ export default {
apiClient
.createMealPlan(entry)
.then((result) => {
useMealPlanStore().plans.push(result.data)
this.$bvModal.hide(`modal-meal-plan_${this.modal_id}`)
if (reviewshopping) {
this.mealplan = result.data.id

View File

@@ -24,7 +24,7 @@
<span v-if="user_preferences.shopping_auto_sync < 1">{{ $t('Disable') }}</span>
</div>
<br/>
<b-button class="btn btn-sm" @click="user_preferences.shopping_auto_sync = 0">{{ $t('Disabled') }}</b-button>
<b-button class="btn btn-sm" @click="user_preferences.shopping_auto_sync = 0; updateSettings(false)">{{ $t('Disabled') }}</b-button>
</b-form-group>
<b-form-group :description="$t('mealplan_autoadd_shopping_desc')">

View File

@@ -35,7 +35,7 @@
<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" :use_plural="use_plural"
<ingredients-card :steps="[step]" :ingredient_factor="ingredient_factor"
@checked-state-changed="$emit('checked-state-changed', $event)"/>
</table>
</div>