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

@@ -18,7 +18,8 @@
</div>
</div>
</div>
<div class="mb-3" v-for="book in filteredBooks" :key="book.id">
<div style="padding-bottom: 55px">
<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)">
@@ -53,7 +54,21 @@
@reload="openBook(current_book, true)"
></cookbook-slider>
</transition>
</div>
</div>
<bottom-navigation-bar>
<template #custom_create_functions>
<div class="dropdown-divider" ></div>
<h6 class="dropdown-header">{{ $t('Books')}}</h6>
<a class="dropdown-item" @click="createNew()"><i
class="fa fa-book"></i> {{$t("Create")}}</a>
</template>
</bottom-navigation-bar>
</div>
</template>
@@ -66,13 +81,14 @@ import { ApiApiFactory } from "@/utils/openapi/api"
import CookbookSlider from "@/components/CookbookSlider"
import LoadingSpinner from "@/components/LoadingSpinner"
import { StandardToasts, ApiMixin } from "@/utils/utils"
import BottomNavigationBar from "@/components/BottomNavigationBar.vue";
Vue.use(BootstrapVue)
export default {
name: "CookbookView",
mixins: [ApiMixin],
components: { LoadingSpinner, CookbookSlider },
components: { LoadingSpinner, CookbookSlider, BottomNavigationBar },
data() {
return {
cookbooks: [],

View File

@@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './CookbookView.vue'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './ExportResponseView.vue'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './ExportView.vue'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './ImportResponseView.vue'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@@ -24,8 +24,11 @@
<div class="row justify-content-center">
<div class="col-12 justify-content-cente">
<b-checkbox v-model="import_multiple" switch><span
v-if="import_multiple"><i class="far fa-copy fa-fw"></i> {{ $t('Multiple') }}</span><span
v-if="!import_multiple"><i class="far fa-file fa-fw"></i> {{ $t('Single') }}</span></b-checkbox>
v-if="import_multiple"><i
class="far fa-copy fa-fw"></i> {{ $t('Multiple') }}</span><span
v-if="!import_multiple"><i
class="far fa-file fa-fw"></i> {{ $t('Single') }}</span>
</b-checkbox>
</div>
</div>
<b-input-group class="mt-2" :class="{ bounce: empty_input }"
@@ -52,23 +55,23 @@
</b-button>
<!-- recent imports, nice for testing/development -->
<!-- <div class="row mt-2"> -->
<!-- <div class="col col-md-12">-->
<!-- <div v-if="!import_multiple">-->
<!-- <a href="#" @click="clearRecentImports()">Clear recent-->
<!-- imports</a>-->
<!-- <ul>-->
<!-- <li v-for="x in recent_urls" v-bind:key="x">-->
<!-- <a href="#"-->
<!-- @click="loadRecipe(x, false, undefined)">{{-->
<!-- x-->
<!-- }}</a>-->
<!-- </li>-->
<!-- </ul>-->
<!-- <div class="row mt-2"> -->
<!-- <div class="col col-md-12">-->
<!-- <div v-if="!import_multiple">-->
<!-- <a href="#" @click="clearRecentImports()">Clear recent-->
<!-- imports</a>-->
<!-- <ul>-->
<!-- <li v-for="x in recent_urls" v-bind:key="x">-->
<!-- <a href="#"-->
<!-- @click="loadRecipe(x, false, undefined)">{{-->
<!-- x-->
<!-- }}</a>-->
<!-- </li>-->
<!-- </ul>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
</div>
</div>
@@ -204,7 +207,7 @@
v-if="!import_multiple">
<recipe-card :recipe="recipe_json" :detailed="false"
:show_context_menu="false" :use_plural="use_plural"
:show_context_menu="false"
></recipe-card>
</b-col>
<b-col>
@@ -238,17 +241,22 @@
</b-row>
</b-card-body>
<b-card-footer class="text-center">
<div class="d-flex justify-content-center mb-3" v-if="import_loading">
<b-spinner variant="primary"></b-spinner>
</div>
<b-button-group>
<b-button @click="importRecipe('view')" v-if="!import_multiple">Import &
<b-button @click="importRecipe('view')" v-if="!import_multiple"
:disabled="import_loading">Import &
View
</b-button> <!-- TODO localize -->
<b-button @click="importRecipe('edit')" variant="success"
v-if="!import_multiple">Import & Edit
v-if="!import_multiple" :disabled="import_loading">Import & Edit
</b-button>
<b-button @click="importRecipe('import')" v-if="!import_multiple">Import &
<b-button @click="importRecipe('import')" v-if="!import_multiple"
:disabled="import_loading">Import &
Restart
</b-button>
<b-button @click="location.reload()">Restart
<b-button @click="location.reload()" :disabled="import_loading">Restart
</b-button>
</b-button-group>
</b-card-footer>
@@ -462,6 +470,7 @@ export default {
source_data: '',
recipe_json: undefined,
use_plural: false,
import_loading: false,
// recipe_html: undefined,
// recipe_tree: undefined,
recipe_images: [],
@@ -495,6 +504,13 @@ export default {
apiClient.retrieveSpace(window.ACTIVE_SPACE_ID).then(r => {
this.use_plural = r.data.use_plural
})
let urlParams = new URLSearchParams(window.location.search)
if (urlParams.has("url")) {
this.website_url = urlParams.get('url')
this.loadRecipe(this.website_url)
}
},
methods: {
/**
@@ -504,6 +520,7 @@ export default {
* @param silent do not show any messages for imports
*/
importRecipe: function (action, data, silent) {
this.import_loading = true
if (this.recipe_json !== undefined) {
this.$set(this.recipe_json, 'keywords', this.recipe_json.keywords.filter(k => k.show))
}
@@ -528,12 +545,14 @@ export default {
if (recipe_json.source_url !== '') {
this.failed_imports.push(recipe_json.source_url)
}
this.import_loading = false
if (!silent) {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_CREATE)
}
})
} else {
console.log('cant import recipe without data')
this.import_loading = false
if (!silent) {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_CREATE)
}
@@ -563,6 +582,7 @@ export default {
this.imported_recipes.push(recipe)
break;
case 'nothing':
this.import_loading = false
break;
}
},
@@ -614,6 +634,11 @@ export default {
}
return axios.post(resolveDjangoUrl('api_recipe_from_source'), payload,).then((response) => {
if (response.status === 201 && 'link' in response.data) {
window.location = response.data.link
return
}
this.loading = false
this.recipe_json = response.data['recipe_json'];

View File

@@ -1,63 +1,101 @@
<template>
<div v-if="recipe_json !== undefined" class="mt-2 mt-md-0">
<h5>Steps</h5>
<div class="row">
<div class="col col-md-12 text-center">
<b-button @click="autoSortIngredients()" variant="secondary" v-b-tooltip.hover v-if="recipe_json.steps.length > 1"
:title="$t('Auto_Sort_Help')"><i class="fas fa-random"></i> {{ $t('Auto_Sort') }}
</b-button>
<b-button @click="splitAllSteps('\n')" variant="secondary" class="ml-1" v-b-tooltip.hover
:title="$t('Split_All_Steps')"><i
class="fas fa-expand-arrows-alt"></i> {{ $t('All') }}
</b-button>
<b-button @click="mergeAllSteps()" variant="primary" class="ml-1" v-b-tooltip.hover
:title="$t('Combine_All_Steps')"><i
class="fas fa-compress-arrows-alt"></i> {{ $t('All') }}
</b-button>
</div>
</div>
<div class="row mt-2" v-for="(s, index) in recipe_json.steps"
v-bind:key="index">
<div class="col col-md-4 d-none d-md-block">
<draggable :list="s.ingredients" group="ingredients"
:empty-insert-threshold="10">
<b-list-group-item v-for="i in s.ingredients"
v-bind:key="i.original_text"><i
class="fas fa-arrows-alt"></i> {{ i.original_text }}
</b-list-group-item>
</draggable>
</div>
<div class="col col-md-8 col-12">
<b-input-group>
<b-textarea
style="white-space: pre-wrap" v-model="s.instruction"
max-rows="10"></b-textarea>
<b-input-group-append>
<b-button variant="secondary" @click="splitStep(s,'\n')"><i
class="fas fa-expand-arrows-alt"></i></b-button>
<b-button variant="danger"
@click="recipe_json.steps.splice(recipe_json.steps.findIndex(x => x === s),1)">
<i class="fas fa-trash-alt"></i>
</b-button>
</b-input-group-append>
</b-input-group>
<div class="text-center mt-1">
<b-button @click="mergeStep(s)" variant="primary"
v-if="index + 1 < recipe_json.steps.length"><i
class="fas fa-compress-arrows-alt"></i>
</b-button>
<b-button variant="success"
@click="recipe_json.steps.splice(recipe_json.steps.findIndex(x => x === s) +1,0,{ingredients:[], instruction: ''})">
<i class="fas fa-plus"></i>
</b-button>
<div v-if="recipe_json !== undefined" class="mt-2 mt-md-0">
<h5>Steps</h5>
<div class="row">
<div class="col col-md-12 text-center">
<b-button @click="autoSortIngredients()" variant="secondary" v-b-tooltip.hover v-if="recipe_json.steps.length > 1"
:title="$t('Auto_Sort_Help')"><i class="fas fa-random"></i> {{ $t('Auto_Sort') }}
</b-button>
<b-button @click="splitAllSteps('\n')" variant="secondary" class="ml-1" v-b-tooltip.hover
:title="$t('Split_All_Steps')"><i
class="fas fa-expand-arrows-alt"></i> {{ $t('All') }}
</b-button>
<b-button @click="mergeAllSteps()" variant="primary" class="ml-1" v-b-tooltip.hover
:title="$t('Combine_All_Steps')"><i
class="fas fa-compress-arrows-alt"></i> {{ $t('All') }}
</b-button>
</div>
</div>
<div class="row mt-2" v-for="(s, index) in recipe_json.steps"
v-bind:key="index">
<div class="col col-md-4 d-none d-md-block">
<draggable :list="s.ingredients" group="ingredients"
:empty-insert-threshold="10">
<b-list-group-item v-for="i in s.ingredients"
v-bind:key="i.original_text"><i
class="fas fa-arrows-alt mr-2"></i>
<b-badge variant="light">{{ i.amount.toFixed(2) }}</b-badge>
<b-badge variant="secondary" v-if="i.unit">{{ i.unit.name }}</b-badge>
<b-badge variant="info" v-if="i.food">{{ i.food.name }}</b-badge>
<i>{{ i.original_text }}</i>
<b-button @click="prepareIngredientEditModal(s,i)" v-b-modal.ingredient_edit_modal class="float-right btn-sm"><i class="fas fa-pencil-alt"></i></b-button>
</b-list-group-item>
</draggable>
</div>
<div class="col col-md-8 col-12">
<b-input-group>
<b-textarea
style="white-space: pre-wrap" v-model="s.instruction"
max-rows="10"></b-textarea>
<b-input-group-append>
<b-button variant="secondary" @click="splitStep(s,'\n')"><i
class="fas fa-expand-arrows-alt"></i></b-button>
<b-button variant="danger"
@click="recipe_json.steps.splice(recipe_json.steps.findIndex(x => x === s),1)">
<i class="fas fa-trash-alt"></i>
</b-button>
</b-input-group-append>
</b-input-group>
<div class="text-center mt-1">
<b-button @click="mergeStep(s)" variant="primary"
v-if="index + 1 < recipe_json.steps.length"><i
class="fas fa-compress-arrows-alt"></i>
</b-button>
<b-button variant="success"
@click="recipe_json.steps.splice(recipe_json.steps.findIndex(x => x === s) +1,0,{ingredients:[], instruction: ''})">
<i class="fas fa-plus"></i>
</b-button>
</div>
</div>
<b-modal id="ingredient_edit_modal" :title="$t('Edit')">
<div v-if="current_edit_ingredient !== null">
<b-form-group v-bind:label="$t('Original_Text')" class="mb-3">
<b-form-input v-model="current_edit_ingredient.original_text" type="text" disabled></b-form-input>
</b-form-group>
<b-form-group v-bind:label="$t('Amount')" class="mb-3">
<b-form-input v-model.number="current_edit_ingredient.amount" type="number"></b-form-input>
</b-form-group>
<b-form-group v-bind:label="$t('Unit')" class="mb-3" v-if="current_edit_ingredient.unit !== null">
<b-form-input v-model="current_edit_ingredient.unit.name" type="text"></b-form-input>
</b-form-group>
<b-form-group v-bind:label="$t('Food')" class="mb-3">
<b-form-input v-model="current_edit_ingredient.food.name" type="text"></b-form-input>
</b-form-group>
<b-form-group v-bind:label="$t('Note')" class="mb-3">
<b-form-input v-model="current_edit_ingredient.note" type="text"></b-form-input>
</b-form-group>
</div>
<template v-slot:modal-footer>
<div class="row w-100">
<div class="col-auto justify-content-end">
<b-button class="mx-1" @click="destroyIngredientEditModal()">{{ $t('Ok') }}</b-button>
<b-button class="mx-1" @click="removeIngredient(current_edit_step,current_edit_ingredient);destroyIngredientEditModal()" variant="danger">{{ $t('Delete') }}</b-button>
</div>
</div>
</template>
</b-modal>
</div>
</div>
</div>
</div>
</template>
<script>
@@ -67,116 +105,161 @@ import draggable from "vuedraggable";
import stringSimilarity from "string-similarity"
export default {
name: "ImportViewStepEditor",
components: {
draggable
},
props: {
recipe: undefined
},
data() {
return {
recipe_json: undefined
}
},
watch: {
recipe_json: function () {
this.$emit('change', this.recipe_json)
name: "ImportViewStepEditor",
components: {
draggable
},
},
mounted() {
this.recipe_json = this.recipe
},
methods: {
/**
* utility function used by splitAllSteps and splitStep to split a single step object into multiple step objects
* @param step: single step
* @param split_character: character to split steps at
* @return array of step objects
*/
splitStepObject: function (step, split_character) {
let steps = []
step.instruction.split(split_character).forEach(part => {
if (part.trim() !== '') {
steps.push({'instruction': part, 'ingredients': []})
props: {
recipe: undefined
},
data() {
return {
recipe_json: undefined,
current_edit_ingredient: null,
current_edit_step: null,
}
})
steps[0].ingredients = step.ingredients // put all ingredients from the original step in the ingredients of the first step of the split step list
return steps
},
/**
* Splits all steps of a given recipe_json at the split character (e.g. \n or \n\n)
* @param split_character: character to split steps at
*/
splitAllSteps: function (split_character) {
let steps = []
this.recipe_json.steps.forEach(step => {
steps = steps.concat(this.splitStepObject(step, split_character))
})
this.recipe_json.steps = steps
watch: {
recipe_json: function () {
this.$emit('change', this.recipe_json)
},
},
/**
* Splits the given step at the split character (e.g. \n or \n\n)
* @param step: step ingredients to split
* @param split_character: character to split steps at
*/
splitStep: function (step, split_character) {
let old_index = this.recipe_json.steps.findIndex(x => x === step)
let new_steps = this.splitStepObject(step, split_character)
this.recipe_json.steps.splice(old_index, 1, ...new_steps)
mounted() {
this.recipe_json = this.recipe
},
/**
* Merge all steps of a given recipe_json into one
*/
mergeAllSteps: function () {
let step = {'instruction': '', 'ingredients': []}
this.recipe_json.steps.forEach(s => {
step.instruction += s.instruction + '\n'
step.ingredients = step.ingredients.concat(s.ingredients)
})
this.recipe_json.steps = [step]
},
/**
* Merge two steps (the given and next one)
*/
mergeStep: function (step) {
let step_index = this.recipe_json.steps.findIndex(x => x === step)
let removed_steps = this.recipe_json.steps.splice(step_index, 2)
methods: {
/**
* utility function used by splitAllSteps and splitStep to split a single step object into multiple step objects
* @param step: single step
* @param split_character: character to split steps at
* @return array of step objects
*/
splitStepObject: function (step, split_character) {
let steps = []
step.instruction.split(split_character).forEach(part => {
if (part.trim() !== '') {
steps.push({'instruction': part, 'ingredients': []})
}
})
steps[0].ingredients = step.ingredients // put all ingredients from the original step in the ingredients of the first step of the split step list
return steps
},
/**
* Splits all steps of a given recipe_json at the split character (e.g. \n or \n\n)
* @param split_character: character to split steps at
*/
splitAllSteps: function (split_character) {
let steps = []
this.recipe_json.steps.forEach(step => {
steps = steps.concat(this.splitStepObject(step, split_character))
})
this.recipe_json.steps = steps
},
/**
* Splits the given step at the split character (e.g. \n or \n\n)
* @param step: step ingredients to split
* @param split_character: character to split steps at
*/
splitStep: function (step, split_character) {
let old_index = this.recipe_json.steps.findIndex(x => x === step)
let new_steps = this.splitStepObject(step, split_character)
this.recipe_json.steps.splice(old_index, 1, ...new_steps)
},
/**
* Merge all steps of a given recipe_json into one
*/
mergeAllSteps: function () {
let step = {'instruction': '', 'ingredients': []}
this.recipe_json.steps.forEach(s => {
step.instruction += s.instruction + '\n'
step.ingredients = step.ingredients.concat(s.ingredients)
})
this.recipe_json.steps = [step]
},
/**
* Merge two steps (the given and next one)
*/
mergeStep: function (step) {
let step_index = this.recipe_json.steps.findIndex(x => x === step)
let removed_steps = this.recipe_json.steps.splice(step_index, 2)
this.recipe_json.steps.splice(step_index, 0, {
'instruction': removed_steps.flatMap(x => x.instruction).join('\n'),
'ingredients': removed_steps.flatMap(x => x.ingredients)
})
},
/**
* automatically assign ingredients to steps based on text matching
*/
autoSortIngredients: function () {
let ingredients = this.recipe_json.steps.flatMap(s => s.ingredients)
this.recipe_json.steps.forEach(s => s.ingredients = [])
this.recipe_json.steps.splice(step_index, 0, {
'instruction': removed_steps.flatMap(x => x.instruction).join('\n'),
'ingredients': removed_steps.flatMap(x => x.ingredients)
})
},
/**
* automatically assign ingredients to steps based on text matching
*/
autoSortIngredients: function () {
let ingredients = this.recipe_json.steps.flatMap(s => s.ingredients)
this.recipe_json.steps.forEach(s => s.ingredients = [])
ingredients.forEach(i => {
let found = false
this.recipe_json.steps.forEach(s => {
if (s.instruction.includes(i.food.name.trim()) && !found) {
found = true
s.ingredients.push(i)
}
})
if (!found) {
let best_match = {rating: 0, step: this.recipe_json.steps[0]}
this.recipe_json.steps.forEach(s => {
let match = stringSimilarity.findBestMatch(i.food.name.trim(), s.instruction.split(' '))
if (match.bestMatch.rating > best_match.rating) {
best_match = {rating: match.bestMatch.rating, step: s}
ingredients.forEach(i => {
let found = false
this.recipe_json.steps.forEach(s => {
if (s.instruction.includes(i.food.name.trim()) && !found) {
found = true
s.ingredients.push(i)
}
})
if (!found) {
let best_match = {rating: 0, step: this.recipe_json.steps[0]}
this.recipe_json.steps.forEach(s => {
let match = stringSimilarity.findBestMatch(i.food.name.trim(), s.instruction.split(' '))
if (match.bestMatch.rating > best_match.rating) {
best_match = {rating: match.bestMatch.rating, step: s}
}
})
best_match.step.ingredients.push(i)
found = true
}
})
},
/**
* Prepare variable that holds currently edited ingredient for modal to manipulate it
* add default placeholder for food/unit in case it is not present, so it can be edited as well
* @param ingredient
*/
prepareIngredientEditModal: function (step, ingredient) {
if (ingredient.unit === null) {
ingredient.unit = {
"name": ""
}
}
})
best_match.step.ingredients.push(i)
found = true
if (ingredient.food === null) {
ingredient.food = {
"name": ""
}
}
this.current_edit_ingredient = ingredient
this.current_edit_step = step
},
/**
* can be called to remove an ingredient from the given step
* @param step step to remove ingredient from
* @param ingredient ingredient to remove
*/
removeIngredient: function (step, ingredient) {
step.ingredients = step.ingredients.filter((i) => i !== ingredient)
},
/**
* cleanup method called to close modal
* closes modal UI and cleanups variables
*/
destroyIngredientEditModal: function () {
this.$bvModal.hide('ingredient_edit_modal')
if (this.current_edit_ingredient.unit.name === ''){
this.current_edit_ingredient.unit = null
}
if (this.current_edit_ingredient.food.name === ''){
this.current_edit_ingredient.food = null
}
this.current_edit_ingredient = null
this.current_edit_step = null
}
})
}
}
}
</script>

View File

@@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './ImportView.vue'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './IngredientEditorView.vue'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@@ -2,7 +2,7 @@
<div>
<b-tabs content-class="mt-3" v-model="current_tab">
<b-tab :title="$t('Planner')" active>
<div class="row calender-row">
<div class="row calender-row d-none d-lg-block">
<div class="col-12 calender-parent">
<calendar-view
:show-date="showDate"
@@ -48,6 +48,79 @@
</calendar-view>
</div>
</div>
<div class="row d-block d-lg-none">
<div>
<div class="col-12">
<div class="col-12 d-flex justify-content-center mt-2">
<b-button-toolbar key-nav aria-label="Toolbar with button groups">
<b-button-group class="mx-1">
<b-button v-html="'<<'" class="p-2 pr-3 pl-3"
@click="setShowDate($refs.header.headerProps.previousPeriod)"></b-button>
</b-button-group>
<b-button-group class="mx-1">
<b-button @click="setShowDate($refs.header.headerProps.currentPeriod)"><i
class="fas fa-home"></i></b-button>
<b-form-datepicker right button-only button-variant="secondary" @context="datePickerChanged"></b-form-datepicker>
</b-button-group>
<b-button-group class="mx-1">
<b-button v-html="'>>'" class="p-2 pr-3 pl-3"
@click="setShowDate($refs.header.headerProps.nextPeriod)"></b-button>
</b-button-group>
</b-button-toolbar>
</div>
</div>
<div class="col-12 mt-2" style="padding-bottom: 60px">
<div v-for="day in mobileSimpleGrid" v-bind:key="day.day">
<b-list-group>
<b-list-group-item>
<div class="d-flex flex-row align-middle">
<h6 class="mb-0 mt-1 align-middle">{{ day.date_label }}</h6>
<div class="flex-grow-1 text-right">
<b-button class="btn-sm btn-outline-primary" @click="showMealPlanEditModal(null, day.create_default_date)"><i
class="fa fa-plus"></i></b-button>
</div>
</div>
</b-list-group-item>
<b-list-group-item v-for="plan in day.plan_entries" v-bind:key="plan.entry.id" >
<div class="d-flex flex-row align-items-center">
<div>
<b-img style="height: 50px; width: 50px; object-fit: cover"
:src="plan.entry.recipe.image" rounded="circle" v-if="plan.entry.recipe?.image"></b-img>
<b-img style="height: 50px; width: 50px; object-fit: cover"
:src="image_placeholder" rounded="circle" v-else></b-img>
</div>
<div class="flex-grow-1 ml-2"
style="text-overflow: ellipsis; overflow-wrap: anywhere;">
<span class="two-row-text">
<a :href="resolveDjangoUrl('view_recipe', plan.entry.recipe.id)" v-if="plan.entry.recipe">{{ plan.entry.recipe.name }}</a>
<span v-else>{{ plan.entry.title }}</span> <br/>
</span>
<span v-if="plan.entry.note" class="two-row-text">
<small>{{ plan.entry.note }}</small> <br/>
</span>
<small class="text-muted">
<span v-if="plan.entry.shopping" class="font-light"><i class="fas fa-shopping-cart fa-xs "/></span>
{{ plan.entry.meal_type_name }}
<span v-if="plan.entry.recipe">
- <i class="fa fa-clock"></i> {{ plan.entry.recipe.working_time + plan.entry.recipe.waiting_time }} {{ $t('min') }}
</span>
</small>
</div>
<div class="hover-button">
<a class="pr-2" @click.stop="openContextMenu($event, {originalItem: plan})"><i class="fas fa-ellipsis-v"></i></a>
</div>
</div>
</b-list-group-item>
</b-list-group>
</div>
</div>
</div>
</div>
</b-tab>
<b-tab :title="$t('Settings')">
<div class="row mt-3">
@@ -166,7 +239,7 @@
<ContextMenuItem
@click="
$refs.menu.close()
moveEntryLeft(contextData)
moveEntryLeft(contextData.originalItem)
"
>
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-arrow-left"></i>
@@ -175,7 +248,7 @@
<ContextMenuItem
@click="
$refs.menu.close()
moveEntryRight(contextData)
moveEntryRight(contextData.originalItem)
"
>
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-arrow-right"></i>
@@ -192,7 +265,7 @@
<ContextMenuItem
@click="
$refs.menu.close()
deleteEntry(contextData)
deleteEntry(contextData.originalItem)
"
>
<a class="dropdown-item p-2 text-danger" href="javascript:void(0)"><i class="fas fa-trash"></i>
@@ -203,9 +276,7 @@
<meal-plan-edit-modal
:entry="entryEditing"
:modal_title="modal_title"
:edit_modal_show="edit_modal_show"
@save-entry="editEntry"
@delete-entry="deleteEntry"
:create_date="mealplan_default_date"
@reload-meal-types="refreshMealTypes"
></meal-plan-edit-modal>
<auto-meal-plan-modal
@@ -214,46 +285,29 @@
@create-plan="doAutoPlan"
></auto-meal-plan-modal>
<transition name="slide-fade">
<div class="row fixed-bottom p-2 b-1 border-top text-center" style="background: rgba(255, 255, 255, 0.6)"
v-if="current_tab === 0">
<div class="col-md-3 col-6 mb-1 mb-md-0">
<button class="btn btn-block btn-success shadow-none" @click="createEntryClick(new Date())"><i
class="fas fa-calendar-plus"></i> {{ $t("Create") }}
</button>
</div>
<div class="col-md-3 col-6 mb-1 mb-md-0">
<a class="btn btn-block btn-primary shadow-none" :href="iCalUrl"
><i class="fas fa-download"></i>
{{ $t("Export_To_ICal") }}
</a>
</div>
<div class="col-md-3 col-6 mb-1 mb-md-0">
<div class="row d-none d-lg-block">
<div class="col-12 float-right">
<button class="btn btn-success shadow-none" @click="createEntryClick(new Date())"><i
class="fas fa-calendar-plus"></i> {{ $t("Create") }}
</button>
<a class="btn btn-primary shadow-none" :href="iCalUrl"><i class="fas fa-download"></i>
{{ $t("Export_To_ICal") }}
</a>
</div>
</div>
<bottom-navigation-bar :create_links="[{label:$t('Export_To_ICal'), url: iCalUrl, icon:'fas fa-download'}]">
<template #custom_create_functions>
<h6 class="dropdown-header">{{ $t('Meal_Plan')}}</h6>
<a class="dropdown-item" @click="createEntryClick(new Date())"><i
class="fas fa-calendar-plus fa-fw"></i> {{ $t("Create") }}</a>
</template>
<div class="col-md-3 col-6 mb-1 mb-md-0">
<button class="btn btn-block btn-primary shadow-none" @click="createAutoPlan(new Date())">
{{ $t("Auto_Planner") }}
</button>
</div>
<div class="col-12 d-flex justify-content-center mt-2 d-block d-md-none">
<b-button-toolbar key-nav aria-label="Toolbar with button groups">
<b-button-group class="mx-1">
<b-button v-html="'<<'" class="p-2 pr-3 pl-3"
@click="setShowDate($refs.header.headerProps.previousPeriod)"></b-button>
<b-button v-html="'<'" @click="setStartingDay(-1)" class="p-2 pr-3 pl-3"></b-button>
</b-button-group>
<b-button-group class="mx-1">
<b-button @click="setShowDate($refs.header.headerProps.currentPeriod)"><i
class="fas fa-home"></i></b-button>
<b-form-datepicker button-only button-variant="secondary"></b-form-datepicker>
</b-button-group>
<b-button-group class="mx-1">
<b-button v-html="'>'" @click="setStartingDay(1)" class="p-2 pr-3 pl-3"></b-button>
<b-button v-html="'>>'" class="p-2 pr-3 pl-3"
@click="setShowDate($refs.header.headerProps.nextPeriod)"></b-button>
</b-button-group>
</b-button-toolbar>
</div>
</div>
</transition>
</bottom-navigation-bar>
</div>
</template>
@@ -276,6 +330,8 @@ import VueCookies from "vue-cookies"
import {ApiMixin, StandardToasts, ResolveUrlMixin} from "@/utils/utils"
import {CalendarView, CalendarMathMixin} from "vue-simple-calendar/src/components/bundle"
import {ApiApiFactory} from "@/utils/openapi/api"
import BottomNavigationBar from "@/components/BottomNavigationBar.vue";
import {useMealPlanStore} from "@/stores/MealPlanStore";
import axios from "axios";
import AutoMealPlanModal from "@/components/AutoMealPlanModal";
@@ -299,6 +355,7 @@ export default {
MealPlanCalenderHeader,
EmojiInput,
draggable,
BottomNavigationBar,
},
mixins: [CalendarMathMixin, ApiMixin, ResolveUrlMixin],
data: function () {
@@ -334,29 +391,18 @@ export default {
{text: this.$t("Year"), value: "year"},
],
displayPeriodCount: [1, 2, 3],
entryEditing: {
date: null,
id: -1,
meal_type: null,
note: "",
note_markdown: "",
recipe: null,
servings: 1,
shared: [],
title: "",
title_placeholder: this.$t("Title"),
},
},
shopping_list: [],
current_period: null,
entryEditing: {},
edit_modal_show: false,
entryEditing: null,
mealplan_default_date: null,
ical_url: window.ICAL_URL,
image_placeholder: window.IMAGE_PLACEHOLDER,
}
},
computed: {
modal_title: function () {
if (this.entryEditing.id === -1) {
if (this.entryEditing === null || this.entryEditing?.id === -1) {
return this.$t("Create_Meal_Plan_Entry")
} else {
return this.$t("Edit_Meal_Plan_Entry")
@@ -364,7 +410,7 @@ export default {
},
plan_items: function () {
let items = []
this.plan_entries.forEach((entry) => {
useMealPlanStore().plan_list.forEach((entry) => {
items.push(this.buildItem(entry))
})
return items
@@ -398,6 +444,22 @@ export default {
return ""
}
},
mobileSimpleGrid() {
let grid = []
if (this.current_period !== null) {
for (const x of Array(7).keys()) {
let moment_date = moment(this.current_period.periodStart).add(x, "d")
grid.push({
date: moment_date,
create_default_date: moment_date.format("YYYY-MM-DD"), // improve meal plan edit modal to do formatting itself and accept dates
date_label: moment_date.format('ddd DD.MM'),
plan_entries: this.plan_items.filter((m) => moment(m.startDate).isSame(moment_date, 'day'))
})
}
}
return grid
}
},
mounted() {
this.$nextTick(function () {
@@ -407,6 +469,7 @@ export default {
})
this.$root.$on("change", this.updateEmoji)
this.$i18n.locale = window.CUSTOM_LOCALE
moment.locale(window.CUSTOM_LOCALE)
},
watch: {
settings: {
@@ -504,33 +567,26 @@ export default {
}
})
},
editEntry(edit_entry) {
if (edit_entry.id !== -1) {
this.plan_entries.forEach((entry, index) => {
if (entry.id === edit_entry.id) {
this.$set(this.plan_entries, index, edit_entry)
this.saveEntry(this.plan_entries[index])
}
})
} else {
this.createEntry(edit_entry)
}
datePickerChanged(ctx) {
this.setShowDate(ctx.selectedDate)
},
setShowDate(d) {
this.showDate = d
},
createEntryClick(data) {
this.entryEditing = this.options.entryEditing
this.entryEditing.date = moment(data).format("YYYY-MM-DD")
this.$bvModal.show(`edit-modal`)
this.mealplan_default_date = moment(data).format("YYYY-MM-DD")
this.entryEditing = null
this.$nextTick(function () {
this.$bvModal.show(`id_meal_plan_edit_modal`)
})
},
findEntry(id) {
return this.plan_entries.filter((entry) => {
return useMealPlanStore().plan_list.filter((entry) => {
return entry.id === id
})[0]
},
moveEntry(null_object, target_date, drag_event) {
this.plan_entries.forEach((entry) => {
useMealPlanStore().plan_list.forEach((entry) => {
if (entry.id === this.dragged_item.id) {
if (drag_event.ctrlKey) {
let new_entry = Object.assign({}, entry)
@@ -544,7 +600,7 @@ export default {
})
},
moveEntryLeft(data) {
this.plan_entries.forEach((entry) => {
useMealPlanStore().plan_list.forEach((entry) => {
if (entry.id === data.id) {
entry.date = moment(entry.date).subtract(1, "d")
this.saveEntry(entry)
@@ -552,7 +608,7 @@ export default {
})
},
moveEntryRight(data) {
this.plan_entries.forEach((entry) => {
useMealPlanStore().plan_list.forEach((entry) => {
if (entry.id === data.id) {
entry.date = moment(entry.date).add(1, "d")
this.saveEntry(entry)
@@ -560,20 +616,7 @@ export default {
})
},
deleteEntry(data) {
this.plan_entries.forEach((entry, index, list) => {
if (entry.id === data.id) {
let apiClient = new ApiApiFactory()
apiClient
.destroyMealPlan(entry.id)
.then((e) => {
list.splice(index, 1)
})
.catch((err) => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
}
})
useMealPlanStore().deleteObject(data)
},
entryClick(data) {
let entry = this.findEntry(data.id)
@@ -583,7 +626,7 @@ export default {
this.$refs.menu.open($event, value)
},
openEntryEdit(entry) {
this.$bvModal.show(`edit-modal`)
this.$bvModal.show(`id_meal_plan_edit_modal`)
this.entryEditing = entry
this.entryEditing.date = moment(entry.date).format("YYYY-MM-DD")
if (this.entryEditing.recipe != null) {
@@ -592,18 +635,9 @@ export default {
},
periodChangedCallback(date) {
this.current_period = date
let apiClient = new ApiApiFactory()
apiClient
.listMealPlans({
query: {
from_date: moment(date.periodStart).format("YYYY-MM-DD"),
to_date: moment(date.periodEnd).format("YYYY-MM-DD"),
},
})
.then((result) => {
this.plan_entries = result.data
})
useMealPlanStore().refreshFromAPI(moment(date.periodStart).format("YYYY-MM-DD"), moment(date.periodEnd).format("YYYY-MM-DD"))
this.refreshMealTypes()
},
refreshMealTypes() {
@@ -619,25 +653,11 @@ export default {
saveEntry(entry) {
entry.date = moment(entry.date).format("YYYY-MM-DD")
let apiClient = new ApiApiFactory()
apiClient.updateMealPlan(entry.id, entry).catch((err) => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
useMealPlanStore().updateObject(entry)
},
createEntry(entry) {
entry.date = moment(entry.date).format("YYYY-MM-DD")
let apiClient = new ApiApiFactory()
apiClient
.createMealPlan(entry)
.catch((err) => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
.then((entry_result) => {
this.plan_entries.push(entry_result.data)
})
useMealPlanStore().createObject(entry)
},
buildItem(plan_entry) {
//dirty hack to order items within a day
@@ -649,6 +669,15 @@ export default {
entry: plan_entry,
}
},
showMealPlanEditModal: function (entry, date) {
this.mealplan_default_date = date
this.entryEditing = entry
this.$nextTick(function () {
this.$bvModal.show(`id_meal_plan_edit_modal`)
})
}
createAutoPlan() {
this.$bvModal.show(`autoplan-modal`)
},
@@ -713,6 +742,10 @@ export default {
</script>
<style>
#id_base_container {
margin-top: 12px
}
.slide-fade-enter-active {
transition: all 0.3s ease;
}

View File

@@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './MealPlanView.vue'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@@ -11,7 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@@ -41,7 +41,6 @@
<!-- model isn't paginated and loads in one API call -->
<div v-if="!paginated">
<generic-horizontal-card v-for="i in items_left" v-bind:key="i.id" :item="i"
:use_plural="use_plural"
:model="this_model" @item-action="startAction($event, 'left')"
@finish-action="finishAction"/>
</div>
@@ -51,7 +50,6 @@
<template v-slot:cards>
<generic-horizontal-card v-for="i in items_left" v-bind:key="i.id" :item="i"
:model="this_model"
:use_plural="use_plural"
@item-action="startAction($event, 'left')"
@finish-action="finishAction"/>
</template>
@@ -63,7 +61,6 @@
<template v-slot:cards>
<generic-horizontal-card v-for="i in items_right" v-bind:key="i.id" :item="i"
:model="this_model"
:use_plural="use_plural"
@item-action="startAction($event, 'right')"
@finish-action="finishAction"/>
</template>

View File

@@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './ModelListView'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './OfflineView.vue'
import i18n from "@/i18n";
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './ProfileView.vue'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@@ -572,7 +572,7 @@
{{ $t("Enable_Amount") }}
</button>
<template v-if="use_plural">
<button type="button" class="dropdown-item"
v-if="!ingredient.always_use_plural_unit"
@click="ingredient.always_use_plural_unit = true">
@@ -600,7 +600,7 @@
<i class="fas fa-filter fa-fw"></i>
{{ $t("Use_Plural_Food_Simple") }}
</button>
</template>
<button type="button" class="dropdown-item"
@click="copyTemplateReference(index, ingredient)">

View File

@@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './RecipeEditView'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@@ -1,5 +1,5 @@
<template>
<div id="app" style="margin-bottom: 4vh">
<div id="app" style="padding-bottom: 60px">
<RecipeSwitcher ref="ref_recipe_switcher"/>
<div class="row">
<div class="col-12 col-xl-10 col-lg-10 offset-xl-1 offset-lg-1">
@@ -90,7 +90,7 @@
<b-form-group v-if="ui.show_meal_plan"
v-bind:label="$t('Meal_Plan_Days')"
label-for="popover-input-5" label-cols="8" class="mb-1">
<b-form-input type="number" v-model="ui.meal_plan_days"
<b-form-input type="number" v-model.number="ui.meal_plan_days"
id="popover-input-5" size="sm"
class="mt-1"></b-form-input>
</b-form-group>
@@ -797,8 +797,9 @@
<div class="col-12 col-xl-10 col-lg-10 offset-xl-1 offset-lg-1">
<div style="overflow-x:visible; overflow-y: hidden;white-space: nowrap;">
<b-dropdown id="sortby" :text="sortByLabel" variant="outline-primary" size="sm" style="overflow-y: visible; overflow-x: visible; position: static"
class="shadow-none" toggle-class="text-decoration-none" >
<b-dropdown id="sortby" :text="sortByLabel" variant="outline-primary" size="sm"
style="overflow-y: visible; overflow-x: visible; position: static"
class="shadow-none" toggle-class="text-decoration-none">
<div v-for="o in sortOptions" :key="o.id">
<b-dropdown-item
v-on:click="
@@ -812,7 +813,7 @@
</b-dropdown>
<b-button variant="outline-primary" size="sm" class="shadow-none ml-1"
@click="resetSearch()"><i class="fas fa-file-alt"></i> {{
@click="resetSearch()" v-if="searchFiltered()"><i class="fas fa-file-alt"></i> {{
search.pagination_page
}}/{{ Math.ceil(pagination_count / ui.page_size) }} {{ $t("Reset") }} <i
class="fas fa-times-circle"></i>
@@ -828,34 +829,92 @@
</div>
</div>
<template v-if="!searchFiltered() && ui.show_meal_plan && meal_plan_grid.length > 0">
<hr/>
<div class="row">
<div class="col col-md-12">
<div
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); column-gap: 0.5rem;row-gap: 0.5rem; grid-auto-rows: max-content; ">
<div v-for="day in meal_plan_grid" v-bind:key="day.day" :class="{'d-none d-sm-block': day.plan_entries.length === 0}">
<b-list-group >
<b-list-group-item class="hover-div pb-0">
<div class="d-flex flex-row align-items-center">
<div>
<h6>{{ day.date_label }}</h6>
</div>
<div class="flex-grow-1 text-right">
<b-button class="hover-button btn-outline-primary btn-sm" @click="showMealPlanEditModal(null, day.create_default_date)"><i
class="fa fa-plus"></i></b-button>
</div>
</div>
</b-list-group-item>
<b-list-group-item v-for="plan in day.plan_entries" v-bind:key="plan.id" class="hover-div">
<div class="d-flex flex-row align-items-center">
<div>
<b-img style="height: 50px; width: 50px; object-fit: cover"
:src="plan.recipe.image" rounded="circle" v-if="plan.recipe?.image"></b-img>
<b-img style="height: 50px; width: 50px; object-fit: cover"
:src="image_placeholder" rounded="circle" v-else></b-img>
</div>
<div class="flex-grow-1 ml-2"
style="text-overflow: ellipsis; overflow-wrap: anywhere;">
<span class="two-row-text">
<a :href="resolveDjangoUrl('view_recipe', plan.recipe.id)" v-if="plan.recipe">{{ plan.recipe.name }}</a>
<span v-else>{{ plan.title }}</span>
</span>
</div>
<div class="hover-button">
<b-button @click="showMealPlanEditModal(plan,null)" class="btn-outline-primary btn-sm"><i class="fas fa-pencil-alt"></i></b-button>
</div>
</div>
</b-list-group-item>
</b-list-group>
</div>
</div>
</div>
</div>
<hr/>
</template>
<div v-if="recipes.length > 0" class="mt-4">
<div class="row">
<div class="col col-md-12">
<div
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-gap: 0.4rem">
<template v-if="!searchFiltered()">
<recipe-card
v-bind:key="`mp_${m.id}`"
v-for="m in meal_plans"
:recipe="m.recipe"
:meal_plan="m"
:use_plural="use_plural"
:footer_text="m.meal_type_name"
footer_icon="far fa-calendar-alt"
></recipe-card>
</template>
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); column-gap: 0.5rem;row-gap: 1rem; grid-auto-rows: max-content; ">
<!-- TODO remove once new meal plan view has proven to be good -->
<!-- <template v-if="!searchFiltered()">-->
<!-- <recipe-card-->
<!-- v-bind:key="`mp_${m.id}`"-->
<!-- v-for="m in meal_plans"-->
<!-- :recipe="m.recipe"-->
<!-- :meal_plan="m"-->
<!-- :use_plural="use_plural"-->
<!-- :footer_text="m.meal_type_name"-->
<!-- footer_icon="far fa-calendar-alt"-->
<!-- ></recipe-card>-->
<!-- </template>-->
<recipe-card v-for="r in recipes" v-bind:key="r.id" :recipe="r"
:footer_text="isRecentOrNew(r)[0]"
:footer_icon="isRecentOrNew(r)[1]"
:use_plural="use_plural">
</recipe-card>
</recipe-card>
</div>
</div>
</div>
<div class="row" style="margin-top: 2vh" v-if="!random_search">
<div class="col col-md-12">
<b-pagination pills v-model="search.pagination_page" :total-rows="pagination_count"
<b-pagination v-model="search.pagination_page" :total-rows="pagination_count" first-number
last-number size="lg"
:per-page="ui.page_size" @change="pageChange" align="center"></b-pagination>
</div>
</div>
@@ -894,6 +953,15 @@
</div>
</div>
</div>
<meal-plan-edit-modal
:entry="mealplan_entry_edit"
:create_date="mealplan_default_date"
></meal-plan-edit-modal>
<bottom-navigation-bar>
</bottom-navigation-bar>
</div>
</div>
</div>
@@ -916,7 +984,10 @@ import LoadingSpinner from "@/components/LoadingSpinner" // TODO: is this deprec
import RecipeCard from "@/components/RecipeCard"
import GenericMultiselect from "@/components/GenericMultiselect"
import RecipeSwitcher from "@/components/Buttons/RecipeSwitcher"
import { ApiApiFactory } from "@/utils/openapi/api"
import {ApiApiFactory} from "@/utils/openapi/api"
import {useMealPlanStore} from "@/stores/MealPlanStore";
import BottomNavigationBar from "@/components/BottomNavigationBar.vue";
import MealPlanEditModal from "@/components/MealPlanEditModal.vue";
Vue.use(VueCookies)
Vue.use(BootstrapVue)
@@ -927,7 +998,7 @@ let UI_COOKIE_NAME = "ui_search_settings"
export default {
name: "RecipeSearchView",
mixins: [ResolveUrlMixin, ApiMixin, ToastMixin],
components: {GenericMultiselect, RecipeCard, Treeselect, RecipeSwitcher, Multiselect},
components: {GenericMultiselect, RecipeCard, Treeselect, RecipeSwitcher, Multiselect, BottomNavigationBar, MealPlanEditModal},
data() {
return {
// this.Models and this.Actions inherited from ApiMixin
@@ -935,6 +1006,7 @@ export default {
recipes_loading: true,
facets: {Books: [], Foods: [], Keywords: []},
meal_plans: [],
meal_plan_store: null,
last_viewed_recipes: [],
sortMenu: false,
use_plural: false,
@@ -1015,9 +1087,27 @@ export default {
pagination_count: 0,
random_search: false,
debug: false,
mealplan_default_date: null,
mealplan_entry_edit: null,
image_placeholder: window.IMAGE_PLACEHOLDER,
}
},
computed: {
meal_plan_grid: function () {
let grid = []
if (this.meal_plan_store !== null && this.meal_plan_store.plan_list.length > 0) {
for (const x of Array(this.ui.meal_plan_days).keys()) {
let moment_date = moment().add(x, "d")
grid.push({
date: moment_date,
create_default_date: moment_date.format("YYYY-MM-DD"), // improve meal plan edit modal to do formatting itself and accept dates
date_label: moment_date.format('ddd DD.MM'),
plan_entries: this.meal_plan_store.plan_list.filter((m) => moment(m.date).isSame(moment_date, 'day'))
})
}
}
return grid
},
locale: function () {
return window.CUSTOM_LOCALE
},
@@ -1120,6 +1210,12 @@ export default {
})
return sort_order
},
isMobile: function () { //TODO move to central helper
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
},
isTouch: function () {
return window.matchMedia("(pointer: coarse)").matches
}
},
mounted() {
@@ -1169,6 +1265,7 @@ export default {
this.use_plural = r.data.use_plural
})
this.$i18n.locale = window.CUSTOM_LOCALE
moment.locale(window.CUSTOM_LOCALE)
this.debug = localStorage.getItem("DEBUG") == "True" || false
},
watch: {
@@ -1257,21 +1354,26 @@ export default {
return [...new Map(data.map((item) => [key(item), item])).values()]
},
loadMealPlan: function () {
if (this.ui.show_meal_plan) {
let params = {
options: {
query: {
from_date: moment().format("YYYY-MM-DD"),
to_date: moment().add(this.ui.meal_plan_days, "days").format("YYYY-MM-DD"),
},
},
}
this.genericAPI(this.Models.MEAL_PLAN, this.Actions.LIST, params).then((result) => {
this.meal_plans = result.data
})
} else {
this.meal_plans = []
}
console.log('loadMealpLan')
this.meal_plan_store = useMealPlanStore()
this.meal_plan_store.refreshFromAPI(moment().format("YYYY-MM-DD"), moment().add(this.ui.meal_plan_days, "days").format("YYYY-MM-DD"))
// if (this.ui.show_meal_plan) {
// let params = {
// options: {
// query: {
// from_date: moment().format("YYYY-MM-DD"),
// to_date: moment().add(this.ui.meal_plan_days, "days").format("YYYY-MM-DD"),
// },
// },
// }
// this.genericAPI(this.Models.MEAL_PLAN, this.Actions.LIST, params).then((result) => {
// this.meal_plans = result.data
// })
// } else {
// this.meal_plans = []
// }
},
genericSelectChanged: function (obj) {
if (obj.var.includes("::")) {
@@ -1544,6 +1646,15 @@ export default {
type.filter((x) => x.operator === false && x.not === false).length > 1
)
},
showMealPlanEditModal: function (entry, date) {
this.mealplan_default_date = date
this.mealplan_entry_edit = entry
this.$nextTick(function () {
this.$bvModal.show(`id_meal_plan_edit_modal`)
})
}
},
}
</script>
@@ -1579,4 +1690,12 @@ export default {
width: 30px;
}
.hover-button {
display: none;
}
.hover-div:hover .hover-button {
display: inline-block;
}
</style>

View File

@@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './RecipeSearchView'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@@ -4,7 +4,7 @@
<loading-spinner></loading-spinner>
</template>
<div v-if="!loading">
<div v-if="!loading" style="padding-bottom: 60px">
<RecipeSwitcher ref="ref_recipe_switcher" @switch="quickSwitch($event)"/>
<div class="row">
<div class="col-12" style="text-align: center">
@@ -90,7 +90,6 @@
:ingredient_factor="ingredient_factor"
:servings="servings"
:header="true"
:use_plural="use_plural"
id="ingredient_container"
@checked-state-changed="updateIngredientCheckedState"
@change-servings="servings = $event"
@@ -124,7 +123,6 @@
:step="s"
:ingredient_factor="ingredient_factor"
:index="index"
:use_plural="use_plural"
:start_time="start_time"
@update-start-time="updateStartTime"
@checked-state-changed="updateIngredientCheckedState"
@@ -149,11 +147,14 @@
<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'">
v-if="share_uid !== 'None' && !loading">
<div class="col col-md-12">
<a :href="resolveDjangoUrl('view_report_share_abuse', share_uid)">{{ $t("Report Abuse") }}</a>
<import-tandoor></import-tandoor> <br/>
<a :href="resolveDjangoUrl('view_report_share_abuse', share_uid)" class="mt-3">{{ $t("Report Abuse") }}</a>
</div>
</div>
<bottom-navigation-bar></bottom-navigation-bar>
</div>
</template>
@@ -182,6 +183,8 @@ import NutritionComponent from "@/components/NutritionComponent"
import RecipeSwitcher from "@/components/Buttons/RecipeSwitcher"
import CustomInputSpinButton from "@/components/CustomInputSpinButton"
import {ApiApiFactory} from "@/utils/openapi/api";
import ImportTandoor from "@/components/Modals/ImportTandoor.vue";
import BottomNavigationBar from "@/components/BottomNavigationBar.vue";
Vue.prototype.moment = moment
@@ -191,6 +194,7 @@ export default {
name: "RecipeView",
mixins: [ResolveUrlMixin, ToastMixin],
components: {
ImportTandoor,
LastCooked,
RecipeRating,
PdfViewer,
@@ -204,6 +208,7 @@ export default {
AddRecipeToBook,
RecipeSwitcher,
CustomInputSpinButton,
BottomNavigationBar,
},
computed: {
ingredient_factor: function () {
@@ -221,7 +226,6 @@ export default {
},
data() {
return {
use_plural: false,
loading: true,
recipe: undefined,
rootrecipe: undefined,
@@ -244,10 +248,6 @@ export default {
this.requestWakeLock()
window.addEventListener('resize', this.handleResize);
let apiClient = new ApiApiFactory()
apiClient.retrieveSpace(window.ACTIVE_SPACE_ID).then(r => {
this.use_plural = r.data.use_plural
})
},
beforeUnmount() {
this.destroyWakeLock()

View File

@@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './RecipeView.vue'
import i18n from "@/i18n";
import {createPinia, PiniaVuePlugin} from 'pinia'
Vue.config.productionTip = false
@@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './SettingsView.vue'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@@ -11,7 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@@ -1,6 +1,7 @@
<template>
<div id="app" style="margin-bottom: 4vh">
<div id="app">
<b-alert :show="!online" dismissible class="small float-up" variant="warning">{{ $t("OfflineAlert") }}</b-alert>
<div class="row float-top w-100">
<div class="col-auto no-gutter ml-auto">
<b-button variant="link" class="px-1 pt-0 pb-1 d-none d-md-inline-block">
@@ -469,30 +470,6 @@
</b-tab>
</b-tabs>
<transition name="slided-fade">
<div class="row fixed-bottom p-2 b-1 border-top text-center d-flex d-md-none"
style="background: rgba(255, 255, 255, 0.6);width: 105%;" v-if="current_tab === 0">
<div class="col-6">
<a class="btn btn-block btn-success shadow-none" @click="entrymode = !entrymode; "
><i class="fas fa-cart-plus"></i>
{{ $t("New_Entry") }}
</a>
</div>
<div class="col-6">
<b-dropdown id="dropdown-dropup" block dropup variant="primary" class="shadow-none">
<template #button-content><i class="fas fa-download"></i> {{ $t("Export") }}</template>
<DownloadPDF dom="#shoppinglist" name="shopping.pdf" :label="$t('download_pdf')"
icon="far fa-file-pdf"/>
<DownloadCSV :items="csvData" :delim="settings.csv_delim" name="shopping.csv"
:label="$t('download_csv')" icon="fas fa-file-csv"/>
<CopyToClipboard :items="csvData" :settings="settings" :label="$t('copy_to_clipboard')"
icon="fas fa-clipboard-list"/>
<CopyToClipboard :items="csvData" :settings="settings" format="table"
:label="$t('copy_markdown_table')" icon="fab fa-markdown"/>
</b-dropdown>
</div>
</div>
</transition>
<b-popover target="id_filters_button" triggers="click" placement="bottomleft" :title="$t('Filters')">
<div>
<b-form-group v-bind:label="$t('GroupBy')" label-for="popover-input-1" label-cols="6" class="mb-1">
@@ -588,6 +565,29 @@
</ContextMenu>
<shopping-modal v-if="new_recipe.id" :recipe="new_recipe" :servings="parseInt(add_recipe_servings)"
:modal_id="new_recipe.id" @finish="finishShopping" :list_recipe="new_recipe.list_recipe"/>
<bottom-navigation-bar>
<template #custom_create_functions>
<div class="dropdown-divider"></div>
<h6 class="dropdown-header">{{ $t('Shopping_list')}}</h6>
<a class="dropdown-item" @click="entrymode = !entrymode; " ><i class="fas fa-cart-plus"></i>
{{ $t("New_Entry") }}
</a>
<DownloadPDF dom="#shoppinglist" name="shopping.pdf" :label="$t('download_pdf')"
icon="far fa-file-pdf fa-fw"/>
<DownloadCSV :items="csvData" :delim="settings.csv_delim" name="shopping.csv"
:label="$t('download_csv')" icon="fas fa-file-csv fa-fw"/>
<CopyToClipboard :items="csvData" :settings="settings" :label="$t('copy_to_clipboard')"
icon="fas fa-clipboard-list fa-fw"/>
<CopyToClipboard :items="csvData" :settings="settings" format="table"
:label="$t('copy_markdown_table')" icon="fab fa-markdown fa-fw"/>
</template>
</bottom-navigation-bar>
</div>
</template>
@@ -615,6 +615,8 @@ import ShoppingSettingsComponent from "@/components/Settings/ShoppingSettingsCom
Vue.use(BootstrapVue)
Vue.use(VueCookies)
let SETTINGS_COOKIE_NAME = "shopping_settings"
import {Workbox} from 'workbox-window';
import BottomNavigationBar from "@/components/BottomNavigationBar.vue";
export default {
name: "ShoppingListView",
@@ -630,7 +632,8 @@ export default {
CopyToClipboard,
ShoppingModal,
draggable,
ShoppingSettingsComponent
ShoppingSettingsComponent,
BottomNavigationBar,
},
data() {
@@ -903,9 +906,30 @@ export default {
}
})
this.$i18n.locale = window.CUSTOM_LOCALE
console.log(window.CUSTOM_LOCALE)
},
methods: {
/**
* failed requests to sync entry check events are automatically re-queued by the service worker for sync
* this command allows to manually force replaying those events before re-enabling automatic sync
*/
replaySyncQueue: function () {
const wb = new Workbox('/service-worker.js');
wb.register();
wb.messageSW({type: 'BGSYNC_REPLAY_REQUESTS'}).then((r) => {
console.log('Background sync queue replayed!', r);
})
},
/**
* get the number of entries left in the sync queue for entry check events
* @returns {Promise<Number>} promise resolving to the number of entries left
*/
getSyncQueueLength: function () {
const wb = new Workbox('/service-worker.js');
wb.register();
return wb.messageSW({type: 'BGSYNC_COUNT_QUEUE'}).then((r) => {
return r
})
},
setFocus() {
if (this.ui.entry_mode_simple) {
this.$refs['amount_input_simple'].focus()
@@ -1043,21 +1067,27 @@ export default {
} else {
this.loading = true
}
this.genericAPI(this.Models.SHOPPING_LIST, this.Actions.LIST, params)
.then((results) => {
if (!autosync) {
if (results.data?.length) {
this.items = results.data
} else {
console.log("no data returned")
}
this.loading = false
this.genericAPI(this.Models.SHOPPING_LIST, this.Actions.LIST, params).then((results) => {
if (!autosync) {
if (results.data?.length) {
this.items = results.data
} else {
if (!this.auto_sync_blocked) {
this.mergeShoppingList(results.data)
}
console.log("no data returned")
}
})
this.loading = false
} else {
if (!this.auto_sync_blocked) {
this.getSyncQueueLength().then((r) => {
if (r === 0) {
this.mergeShoppingList(results.data)
} else {
this.auto_sync_running = false
this.replaySyncQueue()
}
})
}
}
})
.catch((err) => {
if (!autosync) {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
@@ -1205,7 +1235,7 @@ export default {
let api = new ApiApiFactory()
if (field) {
// assume if field is changing it should no longer be inherited
food.inherit_fields = food.inherit_fields.filter((x) => x.field !== field)
food.inherit_fields = food.inherit_fields?.filter((x) => x.field !== field)
}
return api

View File

@@ -1,6 +1,7 @@
import i18n from "@/i18n"
import Vue from "vue"
import App from "./ShoppingListView"
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@@ -11,7 +12,11 @@ if (process.env.NODE_ENV === "development") {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: (h) => h(App),
}).$mount("#app")

View File

@@ -151,9 +151,6 @@
<b-form-checkbox v-model="space.show_facet_count"> Facet Count</b-form-checkbox>
<span class="text-muted small">{{ $t('facet_count_info') }}</span><br/>
<b-form-checkbox v-model="space.use_plural">Use Plural form</b-form-checkbox>
<span class="text-muted small">{{ $t('plural_usage_info') }}</span><br/>
<label>{{ $t('FoodInherit') }}</label>
<generic-multiselect :initial_selection="space.food_inherit"
:model="Models.FOOD_INHERIT_FIELDS"

View File

@@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './SpaceManageView.vue'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './SupermarketView.vue'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@@ -0,0 +1,155 @@
<template>
<div id="app">
<div class="row" v-if="food">
<div class="col-12">
<h2>{{ food.name }}</h2>
</div>
</div>
<div class="row">
<div class="col-12">
<b-form v-if="food">
<b-form-group :label="$t('Name')" description="">
<b-form-input v-model="food.name"></b-form-input>
</b-form-group>
<b-form-group :label="$t('Plural')" description="">
<b-form-input v-model="food.plural_name"></b-form-input>
</b-form-group>
<b-form-group :label="$t('Recipe')" :description="$t('food_recipe_help')">
<generic-multiselect
@change="food.recipe = $event.val;"
:model="Models.RECIPE"
:initial_selection="food.recipe"
label="name"
:multiple="false"
:placeholder="$t('Recipe')"
></generic-multiselect>
</b-form-group>
<b-form-group :description="$t('OnHand_help')">
<b-form-checkbox v-model="food.food_onhand">{{ $t('OnHand') }}</b-form-checkbox>
</b-form-group>
<b-form-group :description="$t('ignore_shopping_help')">
<b-form-checkbox v-model="food.ignore_shopping">{{ $t('Ignore_Shopping') }}</b-form-checkbox>
</b-form-group>
<b-form-group :label="$t('Shopping_Category')" :description="$t('shopping_category_help')">
<generic-multiselect
@change="food.supermarket_category = $event.val;"
:model="Models.SHOPPING_CATEGORY"
:initial_selection="food.supermarket_category"
label="name"
:multiple="false"
:placeholder="$t('Shopping_Category')"
></generic-multiselect>
</b-form-group>
<hr/>
<!-- todo add conditions if false disable dont hide -->
<b-form-group :label="$t('Substitutes')" :description="$t('substitute_help')">
<generic-multiselect
@change="food.substitute = $event.val;"
:model="Models.FOOD"
:initial_selection="food.substitute"
label="name"
:multiple="false"
:placeholder="$t('Substitutes')"
></generic-multiselect>
</b-form-group>
<b-form-group :description="$t('substitute_siblings_help')">
<b-form-checkbox v-model="food.substitute_siblings">{{ $t('substitute_siblings') }}</b-form-checkbox>
</b-form-group>
<b-form-group :label="$t('InheritFields')" :description="$t('InheritFields_help')">
<generic-multiselect
@change="food.inherit_fields = $event.val;"
:model="Models.FOOD_INHERIT_FIELDS"
:initial_selection="food.inherit_fields"
label="name"
:multiple="false"
:placeholder="$t('InheritFields')"
></generic-multiselect>
</b-form-group>
<b-form-group :label="$t('ChildInheritFields')" :description="$t('ChildInheritFields_help')">
<generic-multiselect
@change="food.child_inherit_fields = $event.val;"
:model="Models.FOOD_INHERIT_FIELDS"
:initial_selection="food.child_inherit_fields"
label="name"
:multiple="false"
:placeholder="$t('ChildInheritFields')"
></generic-multiselect>
</b-form-group>
<!-- TODO change to a button -->
<b-form-group :description="$t('reset_children_help')">
<b-form-checkbox v-model="food.reset_inherit">{{ $t('reset_children') }}</b-form-checkbox>
</b-form-group>
<b-button variant="primary" @click="updateFood">{{ $t('Save') }}</b-button>
</b-form>
</div>
</div>
</div>
</template>
<script>
import Vue from "vue"
import {BootstrapVue} from "bootstrap-vue"
import "bootstrap-vue/dist/bootstrap-vue.css"
import {ApiApiFactory} from "@/utils/openapi/api";
import RecipeCard from "@/components/RecipeCard.vue";
import GenericMultiselect from "@/components/GenericMultiselect.vue";
import {ApiMixin, StandardToasts} from "@/utils/utils";
Vue.use(BootstrapVue)
export default {
name: "TestView",
mixins: [ApiMixin],
components: {
GenericMultiselect
},
data() {
return {
food: undefined,
}
},
mounted() {
this.$i18n.locale = window.CUSTOM_LOCALE
let apiClient = new ApiApiFactory()
apiClient.retrieveFood('1').then((r) => {
this.food = r.data
})
},
methods: {
updateFood: function () {
let apiClient = new ApiApiFactory()
apiClient.updateFood(this.food.id, this.food).then((r) => {
this.food = r.data
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
}
},
}
</script>
<style>
</style>

View File

@@ -0,0 +1,22 @@
import Vue from 'vue'
import App from './TestView.vue'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
// TODO move this and other default stuff to centralized JS file (verify nothing breaks)
let publicPath = localStorage.STATIC_URL + 'vue/'
if (process.env.NODE_ENV === 'development') {
publicPath = 'http://localhost:8080/'
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

0
vue/src/apps/base_app.js Normal file
View File