mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-08-02 20:15:24 +02:00
feat(backend): ✨ Minor linting, bulk URL import, and improve BG tasks (#760)
* Fixes #751 * Fixes not showing original URL * start slice at 0 instead of 1 * remove print statements * add linter for print statements and remove print * hide all buttons when edit disabled * add bulk import API * update attribute bindings * unify button styles * bulk add recipe feature * thanks linter! * uncomment code Co-authored-by: Hayden <hay-kot@pm.me>
This commit is contained in:
parent
1e5ef28f91
commit
2afaf70a03
24 changed files with 295 additions and 65 deletions
|
@ -1,4 +1,6 @@
|
|||
import { BaseCRUDAPI } from "../_base";
|
||||
import { Category } from "./categories";
|
||||
import { Tag } from "./tags";
|
||||
import { Recipe, CreateRecipe } from "~/types/api-types/recipe";
|
||||
|
||||
const prefix = "/api";
|
||||
|
@ -8,6 +10,7 @@ const routes = {
|
|||
recipesBase: `${prefix}/recipes`,
|
||||
recipesTestScrapeUrl: `${prefix}/recipes/test-scrape-url`,
|
||||
recipesCreateUrl: `${prefix}/recipes/create-url`,
|
||||
recipesCreateUrlBulk: `${prefix}/recipes/create-url/bulk`,
|
||||
recipesCreateFromZip: `${prefix}/recipes/create-from-zip`,
|
||||
recipesCategory: `${prefix}/recipes/category`,
|
||||
recipesParseIngredient: `${prefix}/parser/ingredient`,
|
||||
|
@ -59,6 +62,16 @@ export interface ParsedIngredient {
|
|||
ingredient: Ingredient;
|
||||
}
|
||||
|
||||
export interface BulkCreateRecipe {
|
||||
url: string;
|
||||
categories: Category[];
|
||||
tags: Tag[];
|
||||
}
|
||||
|
||||
export interface BulkCreatePayload {
|
||||
imports: BulkCreateRecipe[];
|
||||
}
|
||||
|
||||
export class RecipeAPI extends BaseCRUDAPI<Recipe, CreateRecipe> {
|
||||
baseRoute: string = routes.recipesBase;
|
||||
itemRoute = routes.recipesRecipeSlug;
|
||||
|
@ -90,6 +103,10 @@ export class RecipeAPI extends BaseCRUDAPI<Recipe, CreateRecipe> {
|
|||
return await this.requests.post(routes.recipesCreateUrl, { url });
|
||||
}
|
||||
|
||||
async createManyByUrl(payload: BulkCreatePayload) {
|
||||
return await this.requests.post(routes.recipesCreateUrlBulk, payload);
|
||||
}
|
||||
|
||||
// Recipe Comments
|
||||
|
||||
// Methods to Generate reference urls for assets/images *
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//TODO: Prevent fetching Categories/Tags multiple time when selector is on page multiple times
|
||||
|
||||
<template>
|
||||
<v-autocomplete
|
||||
v-model="selected"
|
||||
|
@ -14,12 +16,14 @@
|
|||
:solo="solo"
|
||||
:return-object="returnObject"
|
||||
:flat="flat"
|
||||
v-bind="$attrs"
|
||||
@input="emitChange"
|
||||
>
|
||||
<template #selection="data">
|
||||
<v-chip
|
||||
v-if="showSelected"
|
||||
:key="data.index"
|
||||
:small="dense"
|
||||
class="ma-1"
|
||||
:input-value="data.selected"
|
||||
close
|
||||
|
|
|
@ -26,9 +26,7 @@
|
|||
</v-card>
|
||||
|
||||
<div v-if="edit" class="d-flex justify-end">
|
||||
<v-btn class="mt-1" color="secondary" dark @click="addNote">
|
||||
<v-icon>{{ $globals.icons.create }}</v-icon>
|
||||
</v-btn>
|
||||
<BaseButton class="ml-auto my-2" @click="addNote"> {{ $t("general.new") }}</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -92,7 +92,7 @@
|
|||
@end="onMoveCallback"
|
||||
>
|
||||
<v-card v-for="mealplan in plan.meals" :key="mealplan.id" v-model="hover[mealplan.id]" class="my-1">
|
||||
<v-list-item>
|
||||
<v-list-item :to="edit ? null : `/recipe/${mealplan.recipe.slug}`">
|
||||
<v-list-item-avatar :rounded="false">
|
||||
<RecipeCardImage v-if="mealplan.recipe" tiny icon-size="25" :slug="mealplan.recipe.slug" />
|
||||
<v-icon v-else>
|
||||
|
@ -108,8 +108,8 @@
|
|||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<v-card-actions>
|
||||
<v-divider v-if="edit" class="mx-2"></v-divider>
|
||||
<v-card-actions v-if="edit">
|
||||
<v-btn color="error" icon @click="actions.deleteOne(mealplan.id)">
|
||||
<v-icon>{{ $globals.icons.delete }}</v-icon>
|
||||
</v-btn>
|
||||
|
|
|
@ -110,8 +110,8 @@
|
|||
/>
|
||||
</draggable>
|
||||
<div class="d-flex justify-end mt-2">
|
||||
<RecipeIngredientParserMenu class="mr-1" :slug="recipe.slug" :ingredients="recipe.recipeIngredient" />
|
||||
<RecipeDialogBulkAdd class="mr-1" @bulk-data="addIngredient" />
|
||||
<RecipeIngredientParserMenu :slug="recipe.slug" :ingredients="recipe.recipeIngredient" />
|
||||
<RecipeDialogBulkAdd class="ml-1 mr-1" @bulk-data="addIngredient" />
|
||||
<BaseButton @click="addIngredient"> {{ $t("general.new") }} </BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -228,6 +228,30 @@
|
|||
<RecipeNotes v-model="recipe.notes" :edit="form" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-card-actions class="justify-end">
|
||||
<v-text-field
|
||||
v-if="form"
|
||||
v-model="recipe.orgURL"
|
||||
class="mt-10"
|
||||
:label="$t('recipe.original-url')"
|
||||
></v-text-field>
|
||||
<v-btn
|
||||
v-else-if="recipe.orgURL"
|
||||
dense
|
||||
small
|
||||
:hover="false"
|
||||
type="label"
|
||||
:ripple="false"
|
||||
elevation="0"
|
||||
:href="recipe.orgURL"
|
||||
color="secondary darken-1"
|
||||
target="_blank"
|
||||
class="rounded-sm mr-4"
|
||||
>
|
||||
{{ $t("recipe.original-url") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</v-card>
|
||||
|
|
|
@ -142,7 +142,7 @@
|
|||
<v-tab-item value="debug" eager>
|
||||
<v-form ref="domUrlForm" @submit.prevent="debugUrl(recipeUrl)">
|
||||
<v-card flat>
|
||||
<v-card-title class="headline"> Recipe Debugger</v-card-title>
|
||||
<v-card-title class="headline"> Recipe Importer </v-card-title>
|
||||
<v-card-text>
|
||||
Grab the URL of the recipe you want to debug and paste it here. The URL will be scraped by the recipe
|
||||
scraper and the results will be displayed. If you don't see any data returned, the site you are trying
|
||||
|
@ -174,6 +174,17 @@
|
|||
</v-card>
|
||||
</v-form>
|
||||
</v-tab-item>
|
||||
|
||||
<v-tab-item value="bulk" eager>
|
||||
<v-card flat>
|
||||
<v-card-title class="headline"> Recipe Bulk Importer </v-card-title>
|
||||
<v-card-text>
|
||||
The Bulk recipe importer allows you to import multiple recipes at once by queing the sites on the
|
||||
backend and running the task in the background. This can be useful when initially migrating to Mealie,
|
||||
or when you want to import a large number of recipes.
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
</section>
|
||||
<v-divider class="mt-5"></v-divider>
|
||||
|
@ -195,6 +206,74 @@
|
|||
height="700px"
|
||||
/>
|
||||
</section>
|
||||
<!-- Debug Extras -->
|
||||
<section v-else-if="tab === 'bulk'" class="mt-2">
|
||||
<v-row v-for="(bulkUrl, idx) in bulkUrls" :key="'bulk-url' + idx" class="my-1" dense>
|
||||
<v-col cols="12" xs="12" sm="12" md="12">
|
||||
<v-text-field
|
||||
v-model="bulkUrls[idx].url"
|
||||
:label="$t('new-recipe.recipe-url')"
|
||||
dense
|
||||
single-line
|
||||
validate-on-blur
|
||||
autofocus
|
||||
filled
|
||||
hide-details
|
||||
clearable
|
||||
:prepend-inner-icon="$globals.icons.link"
|
||||
rounded
|
||||
class="rounded-lg"
|
||||
>
|
||||
<template #append>
|
||||
<v-btn color="error" icon x-small @click="bulkUrls.splice(idx, 1)">
|
||||
<v-icon>
|
||||
{{ $globals.icons.delete }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" xs="12" sm="6">
|
||||
<RecipeCategoryTagSelector
|
||||
v-model="bulkUrls[idx].categories"
|
||||
validate-on-blur
|
||||
autofocus
|
||||
single-line
|
||||
filled
|
||||
hide-details
|
||||
dense
|
||||
clearable
|
||||
rounded
|
||||
class="rounded-lg"
|
||||
></RecipeCategoryTagSelector>
|
||||
</v-col>
|
||||
<v-col cols="12" xs="12" sm="6">
|
||||
<RecipeCategoryTagSelector
|
||||
v-model="bulkUrls[idx].tags"
|
||||
validate-on-blur
|
||||
autofocus
|
||||
tag-selector
|
||||
hide-details
|
||||
filled
|
||||
dense
|
||||
single-line
|
||||
clearable
|
||||
rounded
|
||||
class="rounded-lg"
|
||||
></RecipeCategoryTagSelector>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-actions class="justify-end">
|
||||
<BaseButton delete @click="bulkUrls = []"> 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">
|
||||
<template #icon> {{ $globals.icons.check }} </template> Submit
|
||||
</BaseButton>
|
||||
</v-card-actions>
|
||||
</section>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -204,10 +283,12 @@ import { defineComponent, reactive, toRefs, ref, useRouter, useContext } from "@
|
|||
// @ts-ignore No Types for v-jsoneditor
|
||||
import VJsoneditor from "v-jsoneditor";
|
||||
import { useApiSingleton } from "~/composables/use-api";
|
||||
import RecipeCategoryTagSelector from "~/components/Domain/Recipe/RecipeCategoryTagSelector.vue";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import { Recipe } from "~/types/api-types/recipe";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
export default defineComponent({
|
||||
components: { VJsoneditor },
|
||||
components: { VJsoneditor, RecipeCategoryTagSelector },
|
||||
setup() {
|
||||
const state = reactive({
|
||||
error: false,
|
||||
|
@ -233,6 +314,11 @@ export default defineComponent({
|
|||
text: "Import with .zip",
|
||||
value: "zip",
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.link,
|
||||
text: "Bulk URL Import",
|
||||
value: "bulk",
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.robot,
|
||||
text: "Debug Scraper",
|
||||
|
@ -249,7 +335,6 @@ export default defineComponent({
|
|||
state.loading = false;
|
||||
return;
|
||||
}
|
||||
console.log(response);
|
||||
router.push(`/recipe/${response.data}`);
|
||||
}
|
||||
|
||||
|
@ -300,7 +385,6 @@ export default defineComponent({
|
|||
return;
|
||||
}
|
||||
const { response } = await api.recipes.createOne({ name });
|
||||
console.log("Create By Name Func", response);
|
||||
handleResponse(response);
|
||||
}
|
||||
|
||||
|
@ -318,11 +402,31 @@ export default defineComponent({
|
|||
formData.append(newRecipeZipFileName, newRecipeZip.value);
|
||||
|
||||
const { response } = await api.upload.file("/api/recipes/create-from-zip", formData);
|
||||
console.log(response);
|
||||
handleResponse(response);
|
||||
}
|
||||
|
||||
// ===================================================
|
||||
// Bulk Importer
|
||||
|
||||
const bulkUrls = ref([{ url: "", categories: [], tags: [] }]);
|
||||
|
||||
async function bulkCreate() {
|
||||
if (bulkUrls.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { response } = await api.recipes.createManyByUrl({ imports: bulkUrls.value });
|
||||
|
||||
if (response?.status === 202) {
|
||||
alert.success("Bulk Import process has started");
|
||||
} else {
|
||||
alert.error("Bulk import process has failed");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
bulkCreate,
|
||||
bulkUrls,
|
||||
debugTreeView,
|
||||
tabs,
|
||||
domCreateByName,
|
||||
|
|
|
@ -21,7 +21,7 @@ import { useLazyRecipes } from "~/composables/use-recipes";
|
|||
export default defineComponent({
|
||||
components: { RecipeCardSection },
|
||||
setup() {
|
||||
const start = ref(1);
|
||||
const start = ref(0);
|
||||
const limit = ref(30);
|
||||
const increment = ref(30);
|
||||
const ready = ref(false);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue