1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-08-04 21:15:22 +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:
Hayden 2021-11-04 18:15:23 -08:00 committed by GitHub
parent ec3b53cdc3
commit 9f8c61a75a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 323 additions and 163 deletions

View file

@ -17,7 +17,8 @@ const routes = {
recipesParseIngredients: `${prefix}/parser/ingredients`,
recipesRecipeSlug: (recipe_slug: string) => `${prefix}/recipes/${recipe_slug}`,
recipesRecipeSlugZip: (recipe_slug: string) => `${prefix}/recipes/${recipe_slug}/zip`,
recipesRecipeSlugExport: (recipe_slug: string) => `${prefix}/recipes/${recipe_slug}/exports`,
recipesRecipeSlugExportZip: (recipe_slug: string) => `${prefix}/recipes/${recipe_slug}/exports/zip`,
recipesRecipeSlugImage: (recipe_slug: string) => `${prefix}/recipes/${recipe_slug}/image`,
recipesRecipeSlugAssets: (recipe_slug: string) => `${prefix}/recipes/${recipe_slug}/assets`,
@ -72,6 +73,10 @@ export interface BulkCreatePayload {
imports: BulkCreateRecipe[];
}
export interface RecipeZipToken {
token: string;
}
export class RecipeAPI extends BaseCRUDAPI<Recipe, CreateRecipe> {
baseRoute: string = routes.recipesBase;
itemRoute = routes.recipesRecipeSlug;
@ -151,4 +156,12 @@ export class RecipeAPI extends BaseCRUDAPI<Recipe, CreateRecipe> {
parser = parser || "nlp";
return await this.requests.post<ParsedIngredient>(routes.recipesParseIngredient, { parser, ingredient });
}
async getZipToken(recipeSlug: string) {
return await this.requests.post<RecipeZipToken>(routes.recipesRecipeSlugExport(recipeSlug), {});
}
getZipRedirectUrl(recipeSlug: string, token: string) {
return `${routes.recipesRecipeSlugExportZip(recipeSlug)}?token=${token}`;
}
}

View file

@ -49,6 +49,7 @@
fab
color="info"
:card-menu="false"
@print="$emit('print')"
/>
</div>
<div v-if="value" class="custom-btn-group mb-">

View file

@ -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>

View file

@ -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>

View file

@ -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;
},

View file

@ -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);
},
},

View file

@ -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");
}
);
},
},

View file

@ -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),
};

View file

@ -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")];

View file

@ -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>

View file

@ -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>

View file

@ -1,5 +1,5 @@
<template>
<v-navigation-drawer class="d-flex flex-column" :value="value" clipped app width="240px">
<v-navigation-drawer class="d-flex flex-column d-print-none" :value="value" clipped app width="240px">
<!-- User Profile -->
<template v-if="$auth.user">
<v-list-item two-line to="/user/profile" exact>

View file

@ -1,6 +1,6 @@
<template>
<div class="text-center">
<v-snackbar v-model="toastAlert.open" top :color="toastAlert.color" timeout="1500" @input="toastAlert.open = false">
<v-snackbar v-model="toastAlert.open" top :color="toastAlert.color" timeout="2000" @input="toastAlert.open = false">
<v-icon dark left>
{{ icon }}
</v-icon>

View file

@ -14,6 +14,7 @@
:icon="icon"
:color="color"
retain-focus-on-click
:class="btnClass"
@click="
on.click;
textToClipboard();
@ -48,6 +49,10 @@ export default {
type: Boolean,
default: true,
},
btnClass: {
type: String,
default: "",
},
},
data() {
return {

View file

@ -1,11 +1,13 @@
const EMAIL_REGEX =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@(([[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@(([[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
const URL_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,256}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/gm;
const URL_REGEX = /[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
export const validators = {
required: (v: string) => !!v || "This Field is Required",
email: (v: string) => !v || EMAIL_REGEX.test(v) || "Email Must Be Valid",
whitespace: (v: string) => !v || v.split(" ").length <= 1 || "No Whitespace Allowed",
url: (v: string) => !v || URL_REGEX.test(v) || "Must Be A Valid URL",
}
minLength: (min: number) => (v: string) => !v || v.length >= min || `Must Be At Least ${min} Characters`,
maxLength: (max: number) => (v: string) => !v || v.length <= max || `Must Be At Most ${max} Characters`,
};

View file

@ -120,14 +120,6 @@ export default defineComponent({
restricted: true,
},
{ divider: true },
{
icon: this.$globals.icons.zip,
title: "Recipe from zip",
subtitle: "Restore from a exported recipe",
to: "/recipe/create?tab=zip",
restricted: true,
},
{ divider: true },
{
icon: this.$globals.icons.pages,
title: "Cookbook",

View file

@ -7,7 +7,7 @@
<v-card v-if="skeleton" :color="`white ${false ? 'darken-2' : 'lighten-4'}`" class="pa-3">
<v-skeleton-loader class="mx-auto" height="700px" type="card"></v-skeleton-loader>
</v-card>
<v-card v-else-if="recipe">
<v-card v-else-if="recipe" class="d-print-none">
<!-- Recipe Header -->
<div class="d-flex justify-end flex-wrap align-stretch">
<v-card v-if="!enableLandscape" width="50%" flat class="d-flex flex-column justify-center align-center">
@ -63,6 +63,7 @@
"
@save="updateRecipe(recipe.slug, recipe)"
@delete="deleteRecipe(recipe.slug)"
@print="printRecipe"
/>
<!-- Editors -->
@ -78,7 +79,7 @@
<RecipeSettingsMenu class="my-1 mx-1" :value="recipe.settings" @upload="uploadImage" />
</div>
<!-- Recipe Title Section -->
<template v-if="!form && !enableLandscape">
<template v-if="!form && enableLandscape">
<v-card-title class="pa-0 ma-0 headline">
{{ recipe.name }}
</v-card-title>
@ -161,7 +162,7 @@
<v-spacer></v-spacer>
<RecipeRating
v-if="!enableLandscape"
v-if="enableLandscape"
:key="recipe.slug"
:value="recipe.rating"
:name="recipe.name"
@ -263,6 +264,7 @@
</v-card-text>
</div>
</v-card>
<RecipePrintView v-if="recipe" :recipe="recipe" />
</v-container>
</template>
@ -298,29 +300,31 @@ import RecipeImageUploadBtn from "~/components/Domain/Recipe/RecipeImageUploadBt
import RecipeSettingsMenu from "~/components/Domain/Recipe/RecipeSettingsMenu.vue";
import RecipeIngredientEditor from "~/components/Domain/Recipe/RecipeIngredientEditor.vue";
import RecipeIngredientParserMenu from "~/components/Domain/Recipe/RecipeIngredientParserMenu.vue";
import RecipePrintView from "~/components/Domain/Recipe/RecipePrintView.vue";
import { Recipe } from "~/types/api-types/recipe";
import { useStaticRoutes } from "~/composables/api";
import { uuid4 } from "~/composables/use-uuid";
export default defineComponent({
components: {
draggable,
RecipeActionMenu,
RecipeDialogBulkAdd,
RecipeAssets,
RecipeCategoryTagSelector,
RecipeChips,
RecipeDialogBulkAdd,
RecipeImageUploadBtn,
RecipeIngredientEditor,
RecipeIngredientParserMenu,
RecipeIngredients,
RecipeInstructions,
RecipeNotes,
RecipeNutrition,
RecipePrintView,
RecipeRating,
RecipeSettingsMenu,
RecipeIngredientEditor,
RecipeTimeCard,
RecipeIngredientParserMenu,
VueMarkdown,
draggable,
},
setup() {
const route = useRoute();
@ -519,9 +523,25 @@ export default defineComponent({
// @ts-ignore
return this.$vuetify.breakpoint.xs ? "200" : "400";
},
// Won't work with Composition API in Vue 2. In Vue 3, this will happen in the setup function.
edit: {
set(val) {
// @ts-ignore
this.$router.replace({ query: { ...this.$route.query, val } });
},
get() {
// @ts-ignore
return this.$route.query.edit;
},
},
},
mounted() {
if (this.edit) {
this.form = true;
}
},
methods: {
printPage() {
printRecipe() {
window.print();
},
},

View file

@ -287,12 +287,20 @@
</v-col>
</v-row>
<v-card-actions class="justify-end">
<BaseButton delete @click="bulkUrls = []"> Clear </BaseButton>
<BaseButton
delete
@click="
bulkUrls = [];
lockBulkImport = false;
"
>
Clear
</BaseButton>
<v-spacer></v-spacer>
<BaseButton color="info" @click="bulkUrls.push({ url: '', categories: [], tags: [] })">
<template #icon> {{ $globals.icons.createAlt }} </template> New
</BaseButton>
<BaseButton :disabled="bulkUrls.length === 0" @click="bulkCreate">
<BaseButton :disabled="bulkUrls.length === 0 || lockBulkImport" @click="bulkCreate">
<template #icon> {{ $globals.icons.check }} </template> Submit
</BaseButton>
</v-card-actions>
@ -352,13 +360,13 @@ export default defineComponent({
const api = useApiSingleton();
const router = useRouter();
function handleResponse(response: any) {
function handleResponse(response: any, edit: Boolean = false) {
if (response?.status !== 201) {
state.error = true;
state.loading = false;
return;
}
router.push(`/recipe/${response.data}`);
router.push(`/recipe/${response.data}?edit=${edit}`);
}
// ===================================================
@ -385,12 +393,17 @@ export default defineComponent({
async function createByUrl(url: string) {
if (!domUrlForm.value.validate() || url === "") {
console.log("Invalid URL", url);
return;
}
state.loading = true;
const { response } = await api.recipes.createOneByUrl(url);
if (response?.status !== 201) {
state.error = true;
// @ts-ignore
if (!response?.error?.response?.data?.detail?.message) {
state.error = true;
}
state.loading = false;
return;
}
@ -408,7 +421,7 @@ export default defineComponent({
return;
}
const { response } = await api.recipes.createOne({ name });
handleResponse(response);
handleResponse(response, true);
}
// ===================================================
@ -432,6 +445,7 @@ export default defineComponent({
// Bulk Importer
const bulkUrls = ref([{ url: "", categories: [], tags: [] }]);
const lockBulkImport = ref(false);
async function bulkCreate() {
if (bulkUrls.value.length === 0) {
@ -442,6 +456,7 @@ export default defineComponent({
if (response?.status === 202) {
alert.success("Bulk Import process has started");
lockBulkImport.value = true;
} else {
alert.error("Bulk import process has failed");
}
@ -450,6 +465,7 @@ export default defineComponent({
return {
bulkCreate,
bulkUrls,
lockBulkImport,
debugTreeView,
tabs,
domCreateByName,

View file

@ -4,6 +4,7 @@
:icon="$globals.icons.primary"
:title="$t('page.all-recipes')"
:recipes="recipes"
@deleted="removeRecipe"
></RecipeCardSection>
<v-card v-intersect="infiniteScroll"></v-card>
<v-fade-transition>
@ -45,7 +46,18 @@ export default defineComponent({
loading.value = false;
}, 500);
return { recipes, infiniteScroll, loading };
function removeRecipe(slug: string) {
// @ts-ignore
for (let i = 0; i < recipes?.value?.length; i++) {
// @ts-ignore
if (recipes?.value[i].slug === slug) {
recipes?.value.splice(i, 1);
break;
}
}
}
return { recipes, infiniteScroll, loading, removeRecipe };
},
head() {
return {