mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-02 04:39:54 -05:00
Fix after rebase
This commit is contained in:
122
vue/src/components/Modals/AddRecipeToBook.vue
Normal file
122
vue/src/components/Modals/AddRecipeToBook.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
|
||||
<div>
|
||||
<b-modal class="modal" :id="`id_modal_add_book_${modal_id}`" :title="$t('Manage_Books')" :ok-title="$t('Add')"
|
||||
:cancel-title="$t('Close')" @ok="addToBook()" @shown="loadBookEntries">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center" v-for="be in this.recipe_book_list" v-bind:key="be.id">
|
||||
{{ be.book_content.name }} <span class="btn btn-sm btn-danger" @click="removeFromBook(be)"><i class="fa fa-trash-alt"></i></span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<multiselect
|
||||
style="margin-top: 1vh"
|
||||
v-model="selected_book"
|
||||
:options="books_filtered"
|
||||
:taggable="true"
|
||||
@tag="createBook"
|
||||
v-bind:tag-placeholder="$t('Create')"
|
||||
:placeholder="$t('Select_Book')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
id="id_books"
|
||||
:multiple="false"
|
||||
:loading="books_loading"
|
||||
@search-change="loadBooks">
|
||||
</multiselect>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Multiselect from 'vue-multiselect'
|
||||
|
||||
import moment from 'moment'
|
||||
|
||||
Vue.prototype.moment = moment
|
||||
|
||||
import Vue from "vue";
|
||||
import {BootstrapVue} from "bootstrap-vue";
|
||||
import {ApiApiFactory} from "@/utils/openapi/api";
|
||||
import {makeStandardToast, StandardToasts} from "@/utils/utils";
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
export default {
|
||||
name: 'AddRecipeToBook',
|
||||
components: {
|
||||
Multiselect
|
||||
},
|
||||
props: {
|
||||
recipe: Object,
|
||||
modal_id: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
books: [],
|
||||
books_loading: false,
|
||||
recipe_book_list: [],
|
||||
selected_book: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
books_filtered: function () {
|
||||
let books_filtered = []
|
||||
|
||||
this.books.forEach(b => {
|
||||
if (this.recipe_book_list.filter(e => e.book === b.id).length === 0) {
|
||||
books_filtered.push(b)
|
||||
}
|
||||
})
|
||||
|
||||
return books_filtered
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
methods: {
|
||||
loadBooks: function (query) {
|
||||
this.books_loading = true
|
||||
let apiFactory = new ApiApiFactory()
|
||||
apiFactory.listRecipeBooks({query: {query: query}}).then(results => {
|
||||
this.books = results.data.filter(e => this.recipe_book_list.indexOf(e) === -1)
|
||||
this.books_loading = false
|
||||
})
|
||||
},
|
||||
createBook: function (name) {
|
||||
let apiFactory = new ApiApiFactory()
|
||||
apiFactory.createRecipeBook({name: name}).then(r => {
|
||||
this.books.push(r.data)
|
||||
this.selected_book = r.data
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||
})
|
||||
},
|
||||
addToBook: function () {
|
||||
let apiFactory = new ApiApiFactory()
|
||||
apiFactory.createRecipeBookEntry({book: this.selected_book.id, recipe: this.recipe.id}).then(r => {
|
||||
this.recipe_book_list.push(r.data)
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||
})
|
||||
},
|
||||
removeFromBook: function (book_entry) {
|
||||
let apiFactory = new ApiApiFactory()
|
||||
apiFactory.destroyRecipeBookEntry(book_entry.id).then(r => {
|
||||
this.recipe_book_list = this.recipe_book_list.filter(e => e.id !== book_entry.id)
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_DELETE)
|
||||
})
|
||||
},
|
||||
loadBookEntries: function () {
|
||||
|
||||
let apiFactory = new ApiApiFactory()
|
||||
apiFactory.listRecipeBookEntrys({query: {recipe: this.recipe.id}}).then(r => {
|
||||
this.recipe_book_list = r.data
|
||||
this.loadBooks('')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
|
||||
@@ -28,7 +28,7 @@
|
||||
<script>
|
||||
import Vue from "vue"
|
||||
import { BootstrapVue } from "bootstrap-vue"
|
||||
import { getForm } from "@/utils/utils"
|
||||
import { getForm, formFunctions } from "@/utils/utils"
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
@@ -84,6 +84,10 @@ export default {
|
||||
show: function () {
|
||||
if (this.show) {
|
||||
this.form = getForm(this.model, this.action, this.item1, this.item2)
|
||||
// TODO: I don't know how to generalize this, but Food needs default values to drive inheritance
|
||||
if (this.form?.form_function) {
|
||||
this.form = formFunctions[this.form.form_function](this.form)
|
||||
}
|
||||
this.dirty = true
|
||||
this.$bvModal.show("modal_" + this.id)
|
||||
} else {
|
||||
|
||||
219
vue/src/components/Modals/MealPlanEditModal.vue
Normal file
219
vue/src/components/Modals/MealPlanEditModal.vue
Normal file
@@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<b-modal :id="modal_id" size="lg" :title="modal_title" hide-footer aria-label="">
|
||||
<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="row mt-3">
|
||||
<div class="col-12 col-lg-6 col-xl-6">
|
||||
<b-form-group>
|
||||
<generic-multiselect
|
||||
@change="selectRecipe"
|
||||
:initial_selection="entryEditing_initial_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_selection="entryEditing_initial_meal_type"
|
||||
:allow_create="true"
|
||||
:create_placeholder="$t('Create_New_Meal_Type')"
|
||||
@new="createMealType"
|
||||
></generic-multiselect>
|
||||
<span class="text-danger" v-if="missing_meal_type">{{ $t("Meal_Type_Required") }}</span>
|
||||
<small tabindex="-1" class="form-text text-muted" v-if="!missing_meal_type">{{
|
||||
$t("Meal_Type")
|
||||
}}</small>
|
||||
</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>
|
||||
<!-- TODO: hide this checkbox if autoadding menuplans, but allow editing on-hand -->
|
||||
<b-input-group v-if="!autoMealPlan">
|
||||
<b-form-checkbox id="AddToShopping" v-model="entryEditing.addshopping" />
|
||||
<small tabindex="-1" class="form-text text-muted">{{ $t("AddToShopping") }}</small>
|
||||
</b-input-group>
|
||||
</div>
|
||||
<div class="col-lg-6 d-none d-lg-block d-xl-block">
|
||||
<recipe-card :recipe="entryEditing.recipe" v-if="entryEditing.recipe != null"></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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue"
|
||||
import { BootstrapVue } from "bootstrap-vue"
|
||||
import GenericMultiselect from "@/components/GenericMultiselect"
|
||||
import { ApiMixin, getUserPreference } from "@/utils/utils"
|
||||
|
||||
const { ApiApiFactory } = require("@/utils/openapi/api")
|
||||
|
||||
const { StandardToasts } = require("@/utils/utils")
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
export default {
|
||||
name: "MealPlanEditModal",
|
||||
props: {
|
||||
entry: Object,
|
||||
entryEditing_initial_recipe: Array,
|
||||
entryEditing_initial_meal_type: Array,
|
||||
modal_title: String,
|
||||
modal_id: {
|
||||
type: String,
|
||||
default: "edit-modal",
|
||||
},
|
||||
allow_delete: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
mixins: [ApiMixin],
|
||||
components: {
|
||||
GenericMultiselect,
|
||||
RecipeCard: () => import("@/components/RecipeCard.vue"),
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
entryEditing: {},
|
||||
missing_recipe: false,
|
||||
missing_meal_type: false,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
entry: {
|
||||
handler() {
|
||||
this.entryEditing = Object.assign({}, this.entry)
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
mounted: function() {},
|
||||
computed: {
|
||||
autoMealPlan: function() {
|
||||
return getUserPreference("mealplan_autoadd_shopping")
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
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
|
||||
}
|
||||
if (this.entryEditing.recipe == null && this.entryEditing.title === "") {
|
||||
this.missing_recipe = true
|
||||
cancel = true
|
||||
}
|
||||
if (!cancel) {
|
||||
this.$bvModal.hide(`edit-modal`)
|
||||
this.$emit("save-entry", this.entryEditing)
|
||||
}
|
||||
},
|
||||
deleteEntry() {
|
||||
this.$bvModal.hide(`edit-modal`)
|
||||
this.$emit("delete-entry", this.entryEditing)
|
||||
},
|
||||
selectMealType(event) {
|
||||
this.missing_meal_type = false
|
||||
if (event.val != null) {
|
||||
this.entryEditing.meal_type = event.val
|
||||
} else {
|
||||
this.entryEditing.meal_type = null
|
||||
}
|
||||
},
|
||||
createMealType(event) {
|
||||
if (event != "") {
|
||||
let apiClient = new ApiApiFactory()
|
||||
|
||||
apiClient
|
||||
.createMealType({ name: event })
|
||||
.then((e) => {
|
||||
this.$emit("reload-meal-types")
|
||||
})
|
||||
.catch((error) => {
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
|
||||
})
|
||||
}
|
||||
},
|
||||
selectRecipe(event) {
|
||||
console.log(event, this.entryEditing)
|
||||
this.missing_recipe = false
|
||||
if (event.val != null) {
|
||||
this.entryEditing.recipe = event.val
|
||||
this.entryEditing.title_placeholder = this.entryEditing.recipe.name
|
||||
this.entryEditing.servings = this.entryEditing.recipe.servings
|
||||
} else {
|
||||
this.entryEditing.recipe = null
|
||||
this.entryEditing.title_placeholder = ""
|
||||
this.entryEditing.servings = 1
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
158
vue/src/components/Modals/ShoppingModal.vue
Normal file
158
vue/src/components/Modals/ShoppingModal.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-modal :id="`shopping_${this.modal_id}`" hide-footer @show="loadRecipe">
|
||||
<template v-slot:modal-title><h4>{{$t('Add_Servings_to_Shopping',{'servings': servings})}}</h4></template>
|
||||
<loading-spinner v-if="loading"></loading-spinner>
|
||||
<div class="accordion" role="tablist" v-if="!loading">
|
||||
<b-card no-body class="mb-1">
|
||||
<b-card-header header-tag="header" class="p-1" role="tab">
|
||||
<b-button block v-b-toggle.accordion-0 class="text-left" variant="outline-info">{{recipe.name}}</b-button>
|
||||
</b-card-header>
|
||||
<b-collapse id="accordion-0" visible accordion="my-accordion" role="tabpanel">
|
||||
<ingredients-card
|
||||
:steps="steps"
|
||||
:recipe="recipe.id"
|
||||
:ingredient_factor="ingredient_factor"
|
||||
:servings="servings"
|
||||
:show_shopping="true"
|
||||
:add_shopping_mode="true"
|
||||
:header="false"
|
||||
@add-to-shopping="addShopping($event)"
|
||||
/>
|
||||
</b-collapse>
|
||||
<!-- eslint-disable vue/no-v-for-template-key-on-child -->
|
||||
<template v-for="r in related_recipes">
|
||||
<b-card no-body class="mb-1" :key="r.recipe.id">
|
||||
<b-card-header header-tag="header" class="p-1" role="tab">
|
||||
<b-button btn-sm block v-b-toggle="'accordion-' +r.recipe.id" class="text-left" variant="outline-primary">{{r.recipe.name}}</b-button>
|
||||
</b-card-header>
|
||||
<b-collapse :id="'accordion-'+r.recipe.id" accordion="my-accordion" role="tabpanel">
|
||||
<ingredients-card
|
||||
:steps="r.steps"
|
||||
:recipe="r.recipe.id"
|
||||
:ingredient_factor="ingredient_factor"
|
||||
:servings="servings"
|
||||
:show_shopping="true"
|
||||
:add_shopping_mode="true"
|
||||
:header="false"
|
||||
@add-to-shopping="addShopping($event)"
|
||||
/>
|
||||
</b-collapse>
|
||||
</b-card>
|
||||
</template>
|
||||
<!-- eslint-disable vue/no-v-for-template-key-on-child -->
|
||||
</b-card>
|
||||
|
||||
</div>
|
||||
<div class="row mt-3 mb-3">
|
||||
<div class="col-12 text-right">
|
||||
<b-button class="mx-2" variant="secondary" @click="$bvModal.hide(`shopping_${modal_id}`)">{{ $t('Cancel') }}
|
||||
</b-button>
|
||||
<b-button class="mx-2" variant="success" @click="saveShopping">{{ $t('Save') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
import {BootstrapVue} from 'bootstrap-vue'
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
const {ApiApiFactory} = require("@/utils/openapi/api");
|
||||
import {StandardToasts} from "@/utils/utils";
|
||||
import IngredientsCard from "@/components/IngredientsCard";
|
||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
|
||||
|
||||
export default {
|
||||
name: 'ShoppingModal',
|
||||
components: {IngredientsCard, LoadingSpinner},
|
||||
mixins: [],
|
||||
props: {
|
||||
recipe: {required: true, type: Object},
|
||||
servings: {type: Number},
|
||||
modal_id: {required: true, type: Number},
|
||||
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
steps: [],
|
||||
recipe_servings: 0,
|
||||
add_shopping: [],
|
||||
related_recipes: []
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
},
|
||||
computed: {
|
||||
ingredient_factor: function () {
|
||||
return this.servings / this.recipe.servings || this.recipe_servings
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
||||
},
|
||||
methods: {
|
||||
loadRecipe: function() {
|
||||
this.add_shopping = []
|
||||
this.related_recipes = []
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.retrieveRecipe(this.recipe.id).then((result) => {
|
||||
this.steps = result.data.steps
|
||||
// ALERT: this will all break if ingredients are re-used between recipes
|
||||
// ALERT: this also doesn't quite work right if the same recipe appears multiple time in the related recipes
|
||||
this.add_shopping = [...this.add_shopping, ...this.steps.map(x => x.ingredients).flat().map(x => x.id)]
|
||||
this.recipe_servings = result.data?.servings
|
||||
this.loading = false
|
||||
})
|
||||
|
||||
// get a list of related recipes
|
||||
apiClient.relatedRecipe(this.recipe.id).then((result) => {
|
||||
return result.data
|
||||
}).then((related_recipes) => {
|
||||
let promises = []
|
||||
related_recipes.forEach(x => {
|
||||
promises.push(apiClient.listSteps(x.id).then((recipe_steps) => {
|
||||
this.related_recipes.push({
|
||||
'recipe': x,
|
||||
'steps': recipe_steps.data.results.filter(x => x.ingredients.length > 0)
|
||||
})
|
||||
}))
|
||||
})
|
||||
return Promise.all(promises)
|
||||
}).then(() => {
|
||||
this.add_shopping = [
|
||||
...this.add_shopping,
|
||||
...this.related_recipes.map(x => x.steps).flat().map(x => x.ingredients).flat().map(x => x.id)
|
||||
]
|
||||
})
|
||||
},
|
||||
addShopping: function(e) {
|
||||
if (e.add) {
|
||||
this.add_shopping.push(e.item.id)
|
||||
} else {
|
||||
this.add_shopping = this.add_shopping.filter(x => x !== e.item.id)
|
||||
}
|
||||
},
|
||||
saveShopping: function() {
|
||||
// another choice would be to create ShoppingListRecipes for each recipe - this bundles all related recipe under the parent recipe
|
||||
let shopping_recipe = {
|
||||
'id': this.recipe.id,
|
||||
'ingredients': this.add_shopping,
|
||||
'servings': this.servings
|
||||
}
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.shoppingRecipe(this.recipe.id, shopping_recipe).then((result) => {
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||
}).catch((err) => {
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
||||
})
|
||||
this.$bvModal.hide(`shopping_${this.modal_id}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user