mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-24 15:49:42 +02:00
fix(backend): 🐛 Fix recipe page issues (#778)
* fix(backend): 🐛 Fix favorite assignment on backend * fix(frontend): 🐛 fix printer button on recipe page * style(frontend): 🚸 add user feadback on copy of recipe link * fix(frontend): 🐛 Fix enableLandscape incorrect bindings to remove duplicate values * feat(frontend): ✨ add ingredient copy button for markdown list -[ ] format * feat(frontend): ✨ add remove prefix button to bulk entry * fix(frontend): 🐛 disable random button when no recipes are present * fix(frontend): ✨ fix .zip download error * fix(frontend): 🚸 close image dialog on upload/get * fix(frontend): 🐛 fix assignment on creation for categories and tags * feat(frontend): ✨ Open editor on creation / fix edit button on main screen * fix(frontend): 🐛 fix false negative regex match for urls on creationg page * feat(frontend): 🚸 provide better user feadback when recipe exists * feat(frontend): ✨ lock bulk importer on submit * remove zip from navigation * fix(frontend): ✨ rerender recipes on delete Co-authored-by: Hayden K <hay-kot@pm.me>
This commit is contained in:
parent
ec3b53cdc3
commit
9f8c61a75a
27 changed files with 323 additions and 163 deletions
|
@ -49,6 +49,7 @@
|
|||
fab
|
||||
color="info"
|
||||
:card-menu="false"
|
||||
@print="$emit('print')"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="value" class="custom-btn-group mb-">
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
<RecipeRating :value="rating" :name="name" :slug="slug" :small="true" />
|
||||
<v-spacer></v-spacer>
|
||||
<RecipeChips :truncate="true" :items="tags" :title="false" :limit="2" :small="true" :is-category="false" />
|
||||
<RecipeContextMenu :slug="slug" :name="name" />
|
||||
<RecipeContextMenu :slug="slug" :name="name" @deleted="$emit('deleted', slug)" />
|
||||
</v-card-actions>
|
||||
<slot></slot>
|
||||
</v-card>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
</v-icon>
|
||||
<v-toolbar-title class="headline"> {{ title }} </v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn :icon="$vuetify.breakpoint.xsOnly" text @click="navigateRandom">
|
||||
<v-btn :icon="$vuetify.breakpoint.xsOnly" text :disabled="recipes.length === 0" @click="navigateRandom">
|
||||
<v-icon :left="!$vuetify.breakpoint.xsOnly">
|
||||
{{ $globals.icons.diceMultiple }}
|
||||
</v-icon>
|
||||
|
@ -66,6 +66,7 @@
|
|||
:rating="recipe.rating"
|
||||
:image="recipe.image"
|
||||
:tags="recipe.tags"
|
||||
@deleted="$emit('deleted', $event)"
|
||||
/>
|
||||
</v-lazy>
|
||||
</v-col>
|
||||
|
|
|
@ -93,14 +93,16 @@ export default defineComponent({
|
|||
async select() {
|
||||
const newItem = await (async () => {
|
||||
if (this.tagDialog) {
|
||||
const newItem = await this.api.tags.createOne({ name: this.itemName });
|
||||
return newItem;
|
||||
const { data } = await this.api.tags.createOne({ name: this.itemName });
|
||||
return data;
|
||||
} else {
|
||||
const newItem = await this.api.categories.createOne({ name: this.itemName });
|
||||
return newItem;
|
||||
const { data } = await this.api.categories.createOne({ name: this.itemName });
|
||||
return data;
|
||||
}
|
||||
})();
|
||||
|
||||
console.log(newItem);
|
||||
|
||||
this.$emit(CREATED_ITEM_EVENT, newItem);
|
||||
this.dialog = false;
|
||||
},
|
||||
|
|
|
@ -98,7 +98,7 @@ export default {
|
|||
getAllCategories();
|
||||
getAllTags();
|
||||
|
||||
return { api, allTags, allCategories };
|
||||
return { api, allTags, allCategories, getAllCategories, getAllTags };
|
||||
},
|
||||
|
||||
data() {
|
||||
|
@ -152,6 +152,9 @@ export default {
|
|||
},
|
||||
pushToItem(createdItem) {
|
||||
createdItem = this.returnObject ? createdItem : createdItem.name;
|
||||
// TODO: Remove excessive get calls
|
||||
this.getAllCategories();
|
||||
this.getAllTags();
|
||||
this.selected.push(createdItem);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
<script>
|
||||
import { defineComponent, ref } from "@nuxtjs/composition-api";
|
||||
import { useApiSingleton } from "~/composables/use-api";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
export default defineComponent({
|
||||
props: {
|
||||
menuTop: {
|
||||
|
@ -156,7 +157,7 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
menuAction(action) {
|
||||
async menuAction(action) {
|
||||
this.loading = true;
|
||||
|
||||
switch (action) {
|
||||
|
@ -182,10 +183,13 @@ export default defineComponent({
|
|||
this.$router.push(`/recipe/${this.slug}` + "?edit=true");
|
||||
break;
|
||||
case "print":
|
||||
this.$router.push(`/recipe/${this.slug}` + "?print=true");
|
||||
this.$emit("print");
|
||||
break;
|
||||
case "download":
|
||||
window.open(`/api/recipes/${this.slug}/zip`);
|
||||
// TODO: Refacor this entire component to not suck so much
|
||||
// eslint-disable-next-line
|
||||
const { data } = await this.api.recipes.getZipToken(this.slug);
|
||||
window.open(this.api.recipes.getZipRedirectUrl(this.slug, data.token));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -194,16 +198,20 @@ export default defineComponent({
|
|||
this.loading = false;
|
||||
},
|
||||
async deleteRecipe() {
|
||||
console.log("Delete Called");
|
||||
await this.api.recipes.deleteOne(this.slug);
|
||||
this.$emit("deleted");
|
||||
},
|
||||
updateClipboard() {
|
||||
const copyText = this.recipeURL;
|
||||
navigator.clipboard.writeText(copyText).then(
|
||||
() => {
|
||||
console.log("Copied to Clipboard", copyText);
|
||||
alert.success("Recipe link copied to clipboard");
|
||||
},
|
||||
() => console.log("Copied Failed", copyText)
|
||||
() => {
|
||||
console.log("Copied Failed", copyText);
|
||||
alert.error("Copied Failed");
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -24,7 +24,23 @@
|
|||
:placeholder="$t('new-recipe.paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list')"
|
||||
>
|
||||
</v-textarea>
|
||||
<v-btn outlined color="info" small @click="trimAllLines"> Trim Whitespace </v-btn>
|
||||
<v-tooltip top>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn outlined color="info" small v-bind="attrs" @click="trimAllLines" v-on="on">
|
||||
Trim Whitespace
|
||||
</v-btn>
|
||||
</template>
|
||||
<span> Trim leading and trailing whitespace as well as blank lines </span>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip top>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn class="ml-1" outlined color="info" small v-bind="attrs" @click="removeFirstCharacter" v-on="on">
|
||||
Trim Prefix
|
||||
</v-btn>
|
||||
</template>
|
||||
<span> Trim first character from each line </span>
|
||||
</v-tooltip>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
@ -52,14 +68,20 @@ export default defineComponent({
|
|||
return state.inputText.split("\n").filter((line) => !(line === "\n" || !line));
|
||||
}
|
||||
|
||||
function trimAllLines() {
|
||||
const splitLintes = splitText();
|
||||
function removeFirstCharacter() {
|
||||
state.inputText = splitText()
|
||||
.map((line) => line.substr(1))
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
splitLintes.forEach((element: string, index: number) => {
|
||||
splitLintes[index] = element.trim();
|
||||
function trimAllLines() {
|
||||
const splitLines = splitText();
|
||||
|
||||
splitLines.forEach((element: string, index: number) => {
|
||||
splitLines[index] = element.trim();
|
||||
});
|
||||
|
||||
state.inputText = splitLintes.join("\n");
|
||||
state.inputText = splitLines.join("\n");
|
||||
}
|
||||
|
||||
function save() {
|
||||
|
@ -70,6 +92,7 @@ export default defineComponent({
|
|||
return {
|
||||
splitText,
|
||||
trimAllLines,
|
||||
removeFirstCharacter,
|
||||
save,
|
||||
...toRefs(state),
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<v-menu offset-y top nudge-top="6" :close-on-content-click="false">
|
||||
<v-menu v-model="menu" offset-y top nudge-top="6" :close-on-content-click="false">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn color="accent" dark v-bind="attrs" v-on="on">
|
||||
<v-icon left>
|
||||
|
@ -59,10 +59,12 @@ export default defineComponent({
|
|||
data: () => ({
|
||||
url: "",
|
||||
loading: false,
|
||||
menu: false,
|
||||
}),
|
||||
methods: {
|
||||
uploadImage(fileObject) {
|
||||
this.$emit(UPLOAD_EVENT, fileObject);
|
||||
this.menu = false;
|
||||
},
|
||||
async getImageFromURL() {
|
||||
this.loading = true;
|
||||
|
@ -70,6 +72,7 @@ export default defineComponent({
|
|||
this.$emit(REFRESH_EVENT);
|
||||
}
|
||||
this.loading = false;
|
||||
this.menu = false;
|
||||
},
|
||||
getMessages() {
|
||||
return this.slug ? [""] : [this.$i18n.t("recipe.save-recipe-before-use")];
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
<template>
|
||||
<div v-if="value && value.length > 0">
|
||||
<h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2>
|
||||
<div class="d-flex justify-start">
|
||||
<h2 class="mb-4 mt-1">{{ $t("recipe.ingredients") }}</h2>
|
||||
<AppButtonCopy btn-class="ml-auto" :copy-text="ingredientCopyText" />
|
||||
</div>
|
||||
<div>
|
||||
<div v-for="(ingredient, index) in value" :key="'ingredient' + index">
|
||||
<h3 v-if="showTitleEditor[index]" class="mt-2">{{ ingredient.title }}</h3>
|
||||
|
@ -18,9 +21,10 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, defineComponent } from "@nuxtjs/composition-api";
|
||||
import VueMarkdown from "@adapttive/vue-markdown";
|
||||
import { useFraction } from "@/composables/use-fraction";
|
||||
export default {
|
||||
export default defineComponent({
|
||||
components: {
|
||||
VueMarkdown,
|
||||
},
|
||||
|
@ -65,7 +69,16 @@ export default {
|
|||
return `${return_qty} ${unit?.name || " "} ${food?.name || " "} ${note}`;
|
||||
}
|
||||
|
||||
return { parseIngredientText };
|
||||
const ingredientCopyText = computed(() => {
|
||||
// Returns a string of all ingredients in markdown list format -[ ]
|
||||
return props.value
|
||||
.map((ingredient) => {
|
||||
return `- [ ] ${parseIngredientText(ingredient)}`;
|
||||
})
|
||||
.join("\n");
|
||||
});
|
||||
|
||||
return { parseIngredientText, ingredientCopyText };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -101,7 +114,7 @@ export default {
|
|||
this.$set(this.showTitleEditor, index, newVal);
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -1,80 +1,89 @@
|
|||
<template>
|
||||
<div class="container print">
|
||||
<div>
|
||||
<h1>
|
||||
<svg class="icon" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="#E58325"
|
||||
d="M8.1,13.34L3.91,9.16C2.35,7.59 2.35,5.06 3.91,3.5L10.93,10.5L8.1,13.34M13.41,13L20.29,19.88L18.88,21.29L12,14.41L5.12,21.29L3.71,19.88L13.36,10.22L13.16,10C12.38,9.23 12.38,7.97 13.16,7.19L17.5,2.82L18.43,3.74L15.19,7L16.15,7.94L19.39,4.69L20.31,5.61L17.06,8.85L18,9.81L21.26,6.56L22.18,7.5L17.81,11.84C17.03,12.62 15.77,12.62 15,11.84L14.78,11.64L13.41,13Z"
|
||||
/>
|
||||
</svg>
|
||||
{{ recipe.name }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="time-container">
|
||||
<RecipeTimeCard :prep-time="recipe.prepTime" :total-time="recipe.totalTime" :perform-time="recipe.performTime" />
|
||||
</div>
|
||||
<v-btn
|
||||
v-if="recipe.recipeYield"
|
||||
dense
|
||||
small
|
||||
:hover="false"
|
||||
type="label"
|
||||
:ripple="false"
|
||||
elevation="0"
|
||||
color="secondary darken-1"
|
||||
class="rounded-sm static"
|
||||
>
|
||||
{{ recipe.recipeYield }}
|
||||
</v-btn>
|
||||
<div>
|
||||
<VueMarkdown :source="recipe.description"> </VueMarkdown>
|
||||
<h2>{{ $t("recipe.ingredients") }}</h2>
|
||||
<ul>
|
||||
<li v-for="(ingredient, index) in recipe.recipeIngredient" :key="index">
|
||||
<v-icon>
|
||||
{{ $globals.icons.checkboxBlankOutline }}
|
||||
</v-icon>
|
||||
<p>{{ ingredient.note }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2>{{ $t("recipe.instructions") }}</h2>
|
||||
<div v-for="(step, index) in recipe.recipeInstructions" :key="index">
|
||||
<h2 v-if="step.title">{{ step.title }}</h2>
|
||||
<div class="ml-5">
|
||||
<h3>{{ $t("recipe.step-index", { step: index + 1 }) }}</h3>
|
||||
<VueMarkdown :source="step.text"> </VueMarkdown>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="recipe" class="container print">
|
||||
<div>
|
||||
<h1>
|
||||
<svg class="icon" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="#E58325"
|
||||
d="M8.1,13.34L3.91,9.16C2.35,7.59 2.35,5.06 3.91,3.5L10.93,10.5L8.1,13.34M13.41,13L20.29,19.88L18.88,21.29L12,14.41L5.12,21.29L3.71,19.88L13.36,10.22L13.16,10C12.38,9.23 12.38,7.97 13.16,7.19L17.5,2.82L18.43,3.74L15.19,7L16.15,7.94L19.39,4.69L20.31,5.61L17.06,8.85L18,9.81L21.26,6.56L22.18,7.5L17.81,11.84C17.03,12.62 15.77,12.62 15,11.84L14.78,11.64L13.41,13Z"
|
||||
/>
|
||||
</svg>
|
||||
{{ recipe.name }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="time-container">
|
||||
<RecipeTimeCard
|
||||
:prep-time="recipe.prepTime"
|
||||
:total-time="recipe.totalTime"
|
||||
:perform-time="recipe.performTime"
|
||||
/>
|
||||
</div>
|
||||
<v-btn
|
||||
v-if="recipe.recipeYield"
|
||||
dense
|
||||
small
|
||||
:hover="false"
|
||||
type="label"
|
||||
:ripple="false"
|
||||
elevation="0"
|
||||
color="secondary darken-1"
|
||||
class="rounded-sm static"
|
||||
>
|
||||
{{ recipe.recipeYield }}
|
||||
</v-btn>
|
||||
<div>
|
||||
<VueMarkdown :source="recipe.description"> </VueMarkdown>
|
||||
<h2>{{ $t("recipe.ingredients") }}</h2>
|
||||
<ul>
|
||||
<li v-for="(ingredient, index) in recipe.recipeIngredient" :key="index">
|
||||
<v-icon>
|
||||
{{ $globals.icons.checkboxBlankOutline }}
|
||||
</v-icon>
|
||||
<p>{{ ingredient.note }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2>{{ $t("recipe.instructions") }}</h2>
|
||||
<div v-for="(step, index) in recipe.recipeInstructions" :key="index">
|
||||
<h2 v-if="step.title">{{ step.title }}</h2>
|
||||
<div class="ml-5">
|
||||
<h3>{{ $t("recipe.step-index", { step: index + 1 }) }}</h3>
|
||||
<VueMarkdown :source="step.text"> </VueMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<v-divider v-if="recipe.notes.length > 0" class="mb-5 mt-0"></v-divider>
|
||||
<br />
|
||||
<v-divider v-if="recipe.notes.length > 0" class="mb-5 mt-0"></v-divider>
|
||||
|
||||
<div v-for="(note, index) in recipe.notes" :key="index + 'note'">
|
||||
<h3>{{ note.title }}</h3>
|
||||
<VueMarkdown :source="note.text"> </VueMarkdown>
|
||||
<div v-for="(note, index) in recipe.notes" :key="index + 'note'">
|
||||
<h3>{{ note.title }}</h3>
|
||||
<VueMarkdown :source="note.text"> </VueMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
// @ts-ignore
|
||||
import VueMarkdown from "@adapttive/vue-markdown";
|
||||
import RecipeTimeCard from "./RecipeTimeCard.vue";
|
||||
export default {
|
||||
import RecipeTimeCard from "~/components/Domain/Recipe/RecipeTimeCard.vue";
|
||||
import { Recipe } from "~/types/api-types/recipe";
|
||||
export default defineComponent({
|
||||
components: {
|
||||
RecipeTimeCard,
|
||||
VueMarkdown,
|
||||
},
|
||||
props: {
|
||||
recipe: {
|
||||
type: Object,
|
||||
type: Object as () => Recipe,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue