Feature/ingredient sections (#624)

* add ingredient sections to UI

* update changelog

* move recipe favorite to action bar

* fix button position on meal-planner

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden 2021-07-09 14:33:23 -08:00 committed by GitHub
parent 9b5cf36981
commit 458ba2964f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 77 additions and 34 deletions

View file

@ -13,14 +13,16 @@
### General ### General
- Recipe Instructions now collapse when checked - Recipe Instructions now collapse when checked
- Default recipe settings can be set through ENV variables - Default recipe settings can be set through ENV variables
- Recipe Ingredient lists can now container ingredient sections.
### Localization ### Localization
#### Huge thanks to [@sephrat](https://github.com/sephrat) for all his work on the project. He's very consistent in his contributions to the project and nearly every release has had some of his code in it! Here's some highlights from this release Huge thanks to [@sephrat](https://github.com/sephrat) for all his work on the project. He's very consistent in his contributions to the project and nearly every release has had some of his code in it! Here's some highlights from this release
- Lazy Load Translations (Huge Performance Increase!) - Lazy Load Translations (Huge Performance Increase!)
- Tons of localization additions all around the site. - Tons of localization additions all around the site.
- All of the work that goes into managing and making Crowdin a great feature the application
#### Here a list of contributors on Crowding who make Mealie possible in different locals #### Here a list of contributors on Crowding who make Mealie possible in different locals

View file

@ -18,14 +18,16 @@
<v-hover v-slot="{ hover }" :open-delay="50"> <v-hover v-slot="{ hover }" :open-delay="50">
<v-card :class="{ 'on-hover': hover }" :elevation="hover ? 12 : 2"> <v-card :class="{ 'on-hover': hover }" :elevation="hover ? 12 : 2">
<CardImage large :slug="planDay.meals[0].slug" icon-size="200" @click="openSearch(index, modes.primary)"> <CardImage large :slug="planDay.meals[0].slug" icon-size="200" @click="openSearch(index, modes.primary)">
<v-fade-transition> <div>
<v-btn v-if="hover" small color="info" class="ma-1" @click.stop="addCustomItem(index, modes.primary)"> <v-fade-transition>
<v-icon left> <v-btn v-if="hover" small color="info" class="ma-1" @click.stop="addCustomItem(index, modes.primary)">
{{ $globals.icons.edit }} <v-icon left>
</v-icon> {{ $globals.icons.edit }}
{{ $t('reicpe.no-recipe') }} </v-icon>
</v-btn> {{ $t("reicpe.no-recipe") }}
</v-fade-transition> </v-btn>
</v-fade-transition>
</div>
</CardImage> </CardImage>
<v-card-title class="my-n3 mb-n6"> <v-card-title class="my-n3 mb-n6">
@ -40,14 +42,14 @@
<v-icon left> <v-icon left>
{{ $globals.icons.edit }} {{ $globals.icons.edit }}
</v-icon> </v-icon>
{{ $t('reicpe.no-recipe') }} {{ $t("reicpe.no-recipe") }}
</v-btn> </v-btn>
</v-fade-transition> </v-fade-transition>
<v-btn color="info" outlined small @click="openSearch(index, modes.sides)"> <v-btn color="info" outlined small @click="openSearch(index, modes.sides)">
<v-icon small class="mr-1"> <v-icon small class="mr-1">
{{ $globals.icons.create }} {{ $globals.icons.create }}
</v-icon> </v-icon>
{{ $t('meal-plan.side') }} {{ $t("meal-plan.side") }}
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
</v-hover> </v-hover>

View file

@ -13,7 +13,7 @@
<v-icon color="primary" class="icon-position" :size="iconSize"> <v-icon color="primary" class="icon-position" :size="iconSize">
{{ $globals.icons.primary }} {{ $globals.icons.primary }}
</v-icon> </v-icon>
<slot> </slot> <slot> </slot>
</div> </div>
</template> </template>
@ -85,6 +85,7 @@ export default {
} }
.icon-slot > div { .icon-slot > div {
top: 0;
position: absolute; position: absolute;
z-index: 1; z-index: 1;
} }

View file

@ -1,17 +1,17 @@
<template> <template>
<v-tooltip bottom nudge-right="50" :color="buttonStyle ? 'primary' : 'secondary'"> <v-tooltip bottom nudge-right="50" :color="buttonStyle ? 'info' : 'secondary'">
<template v-slot:activator="{ on, attrs }"> <template v-slot:activator="{ on, attrs }">
<v-btn <v-btn
small small
@click.prevent="toggleFavorite" @click.prevent="toggleFavorite"
v-if="isFavorite || showAlways" v-if="isFavorite || showAlways"
:color="buttonStyle ? 'primary' : 'secondary'" :color="buttonStyle ? 'info' : 'secondary'"
:icon="!buttonStyle" :icon="!buttonStyle"
:fab="buttonStyle" :fab="buttonStyle"
v-bind="attrs" v-bind="attrs"
v-on="on" v-on="on"
> >
<v-icon :small="!buttonStyle" color="secondary"> <v-icon :small="!buttonStyle" :color="buttonStyle ? 'white' : 'secondary'">
{{ isFavorite ? $globals.icons.heart : $globals.icons.heartOutline }} {{ isFavorite ? $globals.icons.heart : $globals.icons.heartOutline }}
</v-icon> </v-icon>
</v-btn> </v-btn>

View file

@ -1,11 +1,20 @@
<template> <template>
<div v-if="edit || ( value && value.length > 0 )"> <div v-if="edit || (value && value.length > 0)">
<h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2> <h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2>
<div v-if="edit"> <div v-if="edit">
<draggable :value="value" @input="updateIndex" @start="drag = true" @end="drag = false" handle=".handle"> <draggable :value="value" @input="updateIndex" @start="drag = true" @end="drag = false" handle=".handle">
<transition-group type="transition" :name="!drag ? 'flip-list' : null"> <transition-group type="transition" :name="!drag ? 'flip-list' : null">
<div v-for="(ingredient, index) in value" :key="generateKey('ingredient', index)"> <div v-for="(ingredient, index) in value" :key="generateKey('ingredient', index)">
<v-row align="center"> <v-row align="center">
<v-text-field
v-if="edit && showTitleEditor[index]"
class="mx-3 mt-3"
v-model="value[index].title"
dense
:label="$t('recipe.section-title')"
>
</v-text-field>
<v-textarea <v-textarea
class="mr-2" class="mr-2"
:label="$t('recipe.ingredient')" :label="$t('recipe.ingredient')"
@ -15,6 +24,18 @@
dense dense
rows="1" rows="1"
> >
<template slot="append">
<v-tooltip right nudge-right="10">
<template v-slot:activator="{ on, attrs }">
<v-btn icon small class="mt-n1" v-bind="attrs" v-on="on" @click="toggleShowTitle(index)">
<v-icon>{{ showTitleEditor[index] ? $globals.icons.minus : $globals.icons.createAlt }}</v-icon>
</v-btn>
</template>
<span>{{
showTitleEditor[index] ? $t("recipe.remove-section") : $t("recipe.insert-section")
}}</span>
</v-tooltip>
</template>
<template slot="append-outer"> <template slot="append-outer">
<v-icon class="handle">{{ $globals.icons.arrowUpDown }}</v-icon> <v-icon class="handle">{{ $globals.icons.arrowUpDown }}</v-icon>
</template> </template>
@ -35,18 +56,16 @@
</div> </div>
</div> </div>
<div v-else> <div v-else>
<v-list-item <div v-for="(ingredient, index) in value" :key="generateKey('ingredient', index)">
dense <h3 class="mt-2" v-if="showTitleEditor[index]">{{ ingredient.title }}</h3>
v-for="(ingredient, index) in value" <v-divider v-if="showTitleEditor[index]"></v-divider>
:key="generateKey('ingredient', index)" <v-list-item dense @click="toggleChecked(index)">
@click="toggleChecked(index)" <v-checkbox hide-details :value="checked[index]" class="pt-0 my-auto py-auto" color="secondary"> </v-checkbox>
> <v-list-item-content>
<v-checkbox hide-details :value="checked[index]" class="pt-0 my-auto py-auto" color="secondary"> </v-checkbox> <vue-markdown class="ma-0 pa-0 text-subtitle-1 dense-markdown" :source="ingredient.note"> </vue-markdown>
</v-list-item-content>
<v-list-item-content> </v-list-item>
<vue-markdown class="ma-0 pa-0 text-subtitle-1 dense-markdown" :source="ingredient.note"> </vue-markdown> </div>
</v-list-item-content>
</v-list-item>
</div> </div>
</div> </div>
</template> </template>
@ -76,10 +95,19 @@ export default {
return { return {
drag: false, drag: false,
checked: [], checked: [],
showTitleEditor: [],
}; };
}, },
mounted() { mounted() {
this.checked = this.value.map(() => false); this.checked = this.value.map(() => false);
this.showTitleEditor = this.value.map(x => this.validateTitle(x.title));
},
watch: {
value: {
handler() {
this.showTitleEditor = this.value.map(x => this.validateTitle(x.title));
},
},
}, },
methods: { methods: {
addIngredient(ingredients = null) { addIngredient(ingredients = null) {
@ -118,6 +146,16 @@ export default {
removeByIndex(list, index) { removeByIndex(list, index) {
list.splice(index, 1); list.splice(index, 1);
}, },
validateTitle(title) {
return !(title === null || title === "");
},
toggleShowTitle(index) {
const newVal = !this.showTitleEditor[index];
if (!newVal) {
this.value[index].title = "";
}
this.$set(this.showTitleEditor, index, newVal);
},
}, },
}; };
</script> </script>

View file

@ -18,6 +18,7 @@
/> />
<v-spacer></v-spacer> <v-spacer></v-spacer>
<div v-if="!value" class="custom-btn-group ma-1"> <div v-if="!value" class="custom-btn-group ma-1">
<FavoriteBadge class="mx-1" color="info" button-style v-if="loggedIn" :slug="slug" show-always />
<v-tooltip bottom color="info"> <v-tooltip bottom color="info">
<template v-slot:activator="{ on, attrs }"> <template v-slot:activator="{ on, attrs }">
<v-btn <v-btn
@ -66,13 +67,15 @@
<script> <script>
import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog.vue"; import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog.vue";
import ContextMenu from "@/components/Recipe/ContextMenu.vue"; import ContextMenu from "@/components/Recipe/ContextMenu.vue";
import FavoriteBadge from "@/components/Recipe/FavoriteBadge.vue";
const SAVE_EVENT = "save"; const SAVE_EVENT = "save";
const DELETE_EVENT = "delete"; const DELETE_EVENT = "delete";
const CLOSE_EVENT = "close"; const CLOSE_EVENT = "close";
const JSON_EVENT = "json"; const JSON_EVENT = "json";
export default { export default {
components: { ConfirmationDialog, ContextMenu }, components: { ConfirmationDialog, ContextMenu, FavoriteBadge },
props: { props: {
slug: { slug: {
type: String, type: String,

View file

@ -12,7 +12,6 @@
class="d-print-none" class="d-print-none"
:key="imageKey" :key="imageKey"
> >
<FavoriteBadge class="ma-1" button-style v-if="loggedIn" :slug="recipeDetails.slug" show-always />
<RecipeTimeCard <RecipeTimeCard
:class="isMobile ? undefined : 'force-bottom'" :class="isMobile ? undefined : 'force-bottom'"
:prepTime="recipeDetails.prepTime" :prepTime="recipeDetails.prepTime"
@ -69,7 +68,6 @@
<script> <script>
import RecipePageActionMenu from "@/components/Recipe/RecipePageActionMenu.vue"; import RecipePageActionMenu from "@/components/Recipe/RecipePageActionMenu.vue";
import { api } from "@/api"; import { api } from "@/api";
import FavoriteBadge from "@/components/Recipe/FavoriteBadge";
import RecipeViewer from "@/components/Recipe/RecipeViewer"; import RecipeViewer from "@/components/Recipe/RecipeViewer";
import PrintView from "@/components/Recipe/PrintView"; import PrintView from "@/components/Recipe/PrintView";
import RecipeEditor from "@/components/Recipe/RecipeEditor"; import RecipeEditor from "@/components/Recipe/RecipeEditor";
@ -88,7 +86,6 @@ export default {
RecipePageActionMenu, RecipePageActionMenu,
PrintView, PrintView,
NoRecipe, NoRecipe,
FavoriteBadge,
CommentsSection, CommentsSection,
}, },
mixins: [user], mixins: [user],
@ -233,7 +230,7 @@ export default {
async saveRecipe() { async saveRecipe() {
if (this.validateRecipe()) { if (this.validateRecipe()) {
let slug = await api.recipes.update(this.recipeDetails); let slug = await api.recipes.update(this.recipeDetails);
if(!slug) return; if (!slug) return;
if (this.fileObject) { if (this.fileObject) {
this.saveImage(true); this.saveImage(true);