mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 02:03:35 -04:00 
			
		
		
		
	refactor: split up recipe create page (#1283)
* refactor: split up recipe create page * add flat card Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
This commit is contained in:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							30d19c6503
						
					
				
				
					commit
					8f7c7c39bb
				
			| @@ -8,7 +8,7 @@ | ||||
|  | ||||
| Mealie offers two main ways to create recipes. You can use the integrated recipe-scraper to create recipes from hundreds of websites, or you can create recipes manually using the recipe editor. | ||||
|  | ||||
| [Demo](https://beta.mealie.io/recipe/create?tab=url){ .md-button .md-button--primary .align-right } | ||||
| [Demo](https://beta.mealie.io/recipe/create/url){ .md-button .md-button--primary .align-right } | ||||
|  | ||||
| ### Importing Recipes | ||||
|  | ||||
|   | ||||
| @@ -116,7 +116,7 @@ export default defineComponent({ | ||||
|           icon: this.$globals.icons.link, | ||||
|           title: "Import", | ||||
|           subtitle: "Import a recipe by URL", | ||||
|           to: "/recipe/create?tab=url", | ||||
|           to: "/recipe/create/url", | ||||
|           restricted: true, | ||||
|         }, | ||||
|         { divider: true }, | ||||
| @@ -124,7 +124,7 @@ export default defineComponent({ | ||||
|           icon: this.$globals.icons.edit, | ||||
|           title: "Create", | ||||
|           subtitle: "Create a recipe manually", | ||||
|           to: "/recipe/create?tab=new", | ||||
|           to: "/recipe/create/new", | ||||
|           restricted: true, | ||||
|         }, | ||||
|         { divider: true }, | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <v-container class="narrow-container flex-column pa-0"> | ||||
|     <v-container class="flex-column"> | ||||
|       <BasePageTitle divider> | ||||
|         <template #header> | ||||
|           <v-img max-height="175" max-width="175" :src="require('~/static/svgs/recipes-create.svg')"></v-img> | ||||
| @@ -9,311 +9,17 @@ | ||||
|         Select one of the various ways to create a recipe | ||||
|         <template #content> | ||||
|           <div class="ml-auto"> | ||||
|             <BaseOverflowButton v-model="tab" rounded :items="tabs"> </BaseOverflowButton> | ||||
|             <BaseOverflowButton v-model="subpage" rounded :items="subpages"> </BaseOverflowButton> | ||||
|           </div> | ||||
|         </template> | ||||
|       </BasePageTitle> | ||||
|  | ||||
|       <section> | ||||
|         <v-tabs-items v-model="tab" class="mt-2"> | ||||
|           <!-- Create From URL --> | ||||
|           <v-tab-item value="url" eager> | ||||
|             <v-form ref="domUrlForm" @submit.prevent="createByUrl(recipeUrl, importKeywordsAsTags)"> | ||||
|               <v-card flat> | ||||
|                 <v-card-title class="headline"> Scrape Recipe </v-card-title> | ||||
|                 <v-card-text> | ||||
|                   Scrape a recipe by url. Provide the url for the site you want to scrape, and Mealie will attempt to | ||||
|                   scrape the recipe from that site and add it to your collection. | ||||
|                   <v-text-field | ||||
|                     v-model="recipeUrl" | ||||
|                     :label="$t('new-recipe.recipe-url')" | ||||
|                     :prepend-inner-icon="$globals.icons.link" | ||||
|                     validate-on-blur | ||||
|                     autofocus | ||||
|                     filled | ||||
|                     clearable | ||||
|                     class="rounded-lg mt-2" | ||||
|                     rounded | ||||
|                     :rules="[validators.url]" | ||||
|                     :hint="$t('new-recipe.url-form-hint')" | ||||
|                     persistent-hint | ||||
|                   ></v-text-field> | ||||
|                   <v-checkbox v-model="importKeywordsAsTags" label="Import original keywords as tags"> | ||||
|                   </v-checkbox> | ||||
|                 </v-card-text> | ||||
|                 <v-card-actions class="justify-center"> | ||||
|                   <div style="width: 250px"> | ||||
|                     <BaseButton :disabled="recipeUrl === null" rounded block type="submit" :loading="loading" /> | ||||
|                   </div> | ||||
|                 </v-card-actions> | ||||
|               </v-card> | ||||
|             </v-form> | ||||
|             <v-expand-transition> | ||||
|               <v-alert v-show="error" color="error" class="mt-6 white--text"> | ||||
|                 <v-card-title class="ma-0 pa-0"> | ||||
|                   <v-icon left color="white" x-large> {{ $globals.icons.robot }} </v-icon> | ||||
|                   {{ $t("new-recipe.error-title") }} | ||||
|                 </v-card-title> | ||||
|                 <v-divider class="my-3 mx-2"></v-divider> | ||||
|  | ||||
|                 <p> | ||||
|                   {{ $t("new-recipe.error-details") }} | ||||
|                 </p> | ||||
|                 <div class="d-flex row justify-space-around my-3 force-white"> | ||||
|                   <a | ||||
|                     class="dark" | ||||
|                     href="https://developers.google.com/search/docs/data-types/recipe" | ||||
|                     target="_blank" | ||||
|                     rel="noreferrer nofollow" | ||||
|                   > | ||||
|                     {{ $t("new-recipe.google-ld-json-info") }} | ||||
|                   </a> | ||||
|                   <a href="https://github.com/hay-kot/mealie/issues" target="_blank" rel="noreferrer nofollow"> | ||||
|                     {{ $t("new-recipe.github-issues") }} | ||||
|                   </a> | ||||
|                   <a href="https://schema.org/Recipe" target="_blank" rel="noreferrer nofollow"> | ||||
|                     {{ $t("new-recipe.recipe-markup-specification") }} | ||||
|                   </a> | ||||
|                 </div> | ||||
|               </v-alert> | ||||
|             </v-expand-transition> | ||||
|           </v-tab-item> | ||||
|  | ||||
|           <!-- Create By Name --> | ||||
|           <v-tab-item value="new" eager> | ||||
|             <v-card flat> | ||||
|               <v-card-title class="headline"> Create Recipe </v-card-title> | ||||
|               <v-card-text> | ||||
|                 Create a recipe by providing the name. All recipes must have unique names. | ||||
|                 <v-form ref="domCreateByName"> | ||||
|                   <v-text-field | ||||
|                     v-model="newRecipeName" | ||||
|                     :label="$t('recipe.recipe-name')" | ||||
|                     :prepend-inner-icon="$globals.icons.primary" | ||||
|                     validate-on-blur | ||||
|                     autofocus | ||||
|                     filled | ||||
|                     clearable | ||||
|                     class="rounded-lg mt-2" | ||||
|                     rounded | ||||
|                     :rules="[validators.required]" | ||||
|                     hint="New recipe names must be unique" | ||||
|                     persistent-hint | ||||
|                   ></v-text-field> | ||||
|                 </v-form> | ||||
|               </v-card-text> | ||||
|               <v-card-actions class="justify-center"> | ||||
|                 <div style="width: 250px"> | ||||
|                   <BaseButton | ||||
|                     :disabled="newRecipeName === ''" | ||||
|                     rounded | ||||
|                     block | ||||
|                     :loading="loading" | ||||
|                     @click="createByName(newRecipeName)" | ||||
|                   /> | ||||
|                 </div> | ||||
|               </v-card-actions> | ||||
|             </v-card> | ||||
|           </v-tab-item> | ||||
|  | ||||
|           <!-- Create By Zip --> | ||||
|           <v-tab-item value="zip" eager> | ||||
|             <v-form> | ||||
|               <v-card> | ||||
|                 <v-card-title class="headline"> Import from Zip </v-card-title> | ||||
|                 <v-card-text> | ||||
|                   Import a single recipe that was exported from another Mealie instance. | ||||
|                   <v-file-input | ||||
|                     v-model="newRecipeZip" | ||||
|                     accept=".zip" | ||||
|                     label=".zip" | ||||
|                     filled | ||||
|                     clearable | ||||
|                     class="rounded-lg mt-2" | ||||
|                     rounded | ||||
|                     truncate-length="100" | ||||
|                     hint=".zip files must have been exported from Mealie" | ||||
|                     persistent-hint | ||||
|                     prepend-icon="" | ||||
|                     :prepend-inner-icon="$globals.icons.zip" | ||||
|                   > | ||||
|                   </v-file-input> | ||||
|                 </v-card-text> | ||||
|                 <v-card-actions class="justify-center"> | ||||
|                   <div style="width: 250px"> | ||||
|                     <BaseButton | ||||
|                       :disabled="newRecipeZip === null" | ||||
|                       large | ||||
|                       rounded | ||||
|                       block | ||||
|                       :loading="loading" | ||||
|                       @click="createByZip" | ||||
|                     /> | ||||
|                   </div> | ||||
|                 </v-card-actions> | ||||
|               </v-card> | ||||
|             </v-form> | ||||
|           </v-tab-item> | ||||
|  | ||||
|           <!-- Create By Zip --> | ||||
|           <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-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 | ||||
|                   to scrape is not supported by Mealie or it's scraper library. | ||||
|                   <v-text-field | ||||
|                     v-model="recipeUrl" | ||||
|                     :label="$t('new-recipe.recipe-url')" | ||||
|                     validate-on-blur | ||||
|                     :prepend-inner-icon="$globals.icons.link" | ||||
|                     autofocus | ||||
|                     filled | ||||
|                     clearable | ||||
|                     rounded | ||||
|                     class="rounded-lg mt-2" | ||||
|                     :rules="[validators.url]" | ||||
|                     :hint="$t('new-recipe.url-form-hint')" | ||||
|                     persistent-hint | ||||
|                   ></v-text-field> | ||||
|                 </v-card-text> | ||||
|                 <v-card-actions class="justify-center"> | ||||
|                   <div style="width: 250px"> | ||||
|                     <BaseButton | ||||
|                       :disabled="recipeUrl === null" | ||||
|                       rounded | ||||
|                       block | ||||
|                       type="submit" | ||||
|                       color="info" | ||||
|                       :loading="loading" | ||||
|                     > | ||||
|                       <template #icon> | ||||
|                         {{ $globals.icons.robot }} | ||||
|                       </template> | ||||
|                       Debug | ||||
|                     </BaseButton> | ||||
|                   </div> | ||||
|                 </v-card-actions> | ||||
|               </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> | ||||
|     </v-container> | ||||
|  | ||||
|     <v-container tag="section"> | ||||
|       <!--  Debug Extras --> | ||||
|       <section v-if="debugData && tab === 'debug'"> | ||||
|         <v-checkbox v-model="debugTreeView" label="Tree View"></v-checkbox> | ||||
|         <LazyRecipeJsonEditor | ||||
|           v-model="debugData" | ||||
|           class="primary" | ||||
|           :options="{ | ||||
|             mode: debugTreeView ? 'tree' : 'code', | ||||
|             search: false, | ||||
|             indentation: 4, | ||||
|             mainMenuBar: false, | ||||
|           }" | ||||
|           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"> | ||||
|             <RecipeOrganizerSelector | ||||
|               v-model="bulkUrls[idx].categories" | ||||
|               :items="allCategories || []" | ||||
|               selector-type="category" | ||||
|               :input-attrs="{ | ||||
|                 filled: true, | ||||
|                 singleLine: true, | ||||
|                 dense: true, | ||||
|                 rounded: true, | ||||
|                 class: 'rounded-lg', | ||||
|                 hideDetails: true, | ||||
|                 clearable: true, | ||||
|               }" | ||||
|             /> | ||||
|           </v-col> | ||||
|           <v-col cols="12" xs="12" sm="6"> | ||||
|             <RecipeOrganizerSelector | ||||
|               v-model="bulkUrls[idx].tags" | ||||
|               :items="allTags || []" | ||||
|               selector-type="tag" | ||||
|               :input-attrs="{ | ||||
|                 filled: true, | ||||
|                 singleLine: true, | ||||
|                 dense: true, | ||||
|                 rounded: true, | ||||
|                 class: 'rounded-lg', | ||||
|                 hideDetails: true, | ||||
|                 clearable: true, | ||||
|               }" | ||||
|             /> | ||||
|           </v-col> | ||||
|         </v-row> | ||||
|         <v-card-actions class="justify-end"> | ||||
|           <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 || lockBulkImport" @click="bulkCreate"> | ||||
|             <template #icon> {{ $globals.icons.check }} </template> Submit | ||||
|           </BaseButton> | ||||
|         </v-card-actions> | ||||
|         <NuxtChild /> | ||||
|       </section> | ||||
|     </v-container> | ||||
|  | ||||
|     <AdvancedOnly> | ||||
|       <v-container class="narrow-container d-flex justify-end"> | ||||
|       <v-container class="d-flex justify-end"> | ||||
|         <v-btn outlined rounded to="/group/migrations"> Looking For Migrations? </v-btn> | ||||
|       </v-container> | ||||
|     </AdvancedOnly> | ||||
| @@ -321,39 +27,16 @@ | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import { | ||||
|   defineComponent, | ||||
|   reactive, | ||||
|   toRefs, | ||||
|   ref, | ||||
|   useRouter, | ||||
|   useContext, | ||||
|   computed, | ||||
|   useRoute, | ||||
| } from "@nuxtjs/composition-api"; | ||||
| import { AxiosResponse } from "axios"; | ||||
| import { onMounted } from "vue-demi"; | ||||
| import { useUserApi } from "~/composables/api"; | ||||
| import { validators } from "~/composables/use-validators"; | ||||
| import { Recipe } from "~/types/api-types/recipe"; | ||||
| import { alert } from "~/composables/use-toast"; | ||||
| import { VForm } from "~/types/vuetify"; | ||||
| import { defineComponent, useRouter, useContext, computed, useRoute } from "@nuxtjs/composition-api"; | ||||
| import { MenuItem } from "~/components/global/BaseOverflowButton.vue"; | ||||
| import AdvancedOnly from "~/components/global/AdvancedOnly.vue"; | ||||
| import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue"; | ||||
| import { useCategories, useTags } from "~/composables/recipes"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   components: { AdvancedOnly, RecipeOrganizerSelector }, | ||||
|   components: { AdvancedOnly }, | ||||
|   setup() { | ||||
|     const state = reactive({ | ||||
|       error: false, | ||||
|       loading: false, | ||||
|     }); | ||||
|  | ||||
|     const { $globals } = useContext(); | ||||
|  | ||||
|     const tabs: MenuItem[] = [ | ||||
|     const subpages: MenuItem[] = [ | ||||
|       { | ||||
|         icon: $globals.icons.link, | ||||
|         text: "Import with URL", | ||||
| @@ -381,185 +64,21 @@ export default defineComponent({ | ||||
|       }, | ||||
|     ]; | ||||
|  | ||||
|     const api = useUserApi(); | ||||
|     const route = useRoute(); | ||||
|     const router = useRouter(); | ||||
|  | ||||
|     function handleResponse(response: AxiosResponse<string> | null, edit = false) { | ||||
|       if (response?.status !== 201) { | ||||
|         state.error = true; | ||||
|         state.loading = false; | ||||
|         return; | ||||
|       } | ||||
|       router.push(`/recipe/${response.data}?edit=${edit.toString()}`); | ||||
|     } | ||||
|  | ||||
|     const tab = computed({ | ||||
|       set(tab: string) { | ||||
|         router.replace({ query: { ...route.value.query, tab } }); | ||||
|     const subpage = computed({ | ||||
|       set(subpage: string) { | ||||
|         router.push({ path: `/recipe/create/${subpage}`, query: route.value.query }); | ||||
|       }, | ||||
|       get() { | ||||
|         return route.value.query.tab as string; | ||||
|         return route.value.path.split("/").pop() ?? "url"; | ||||
|       }, | ||||
|     }); | ||||
|  | ||||
|     const recipeUrl = computed({ | ||||
|       set(recipe_import_url: string | null) { | ||||
|         if (recipe_import_url !== null) { | ||||
|           recipe_import_url = recipe_import_url.trim(); | ||||
|           router.replace({ query: { ...route.value.query, recipe_import_url } }); | ||||
|         } | ||||
|       }, | ||||
|       get() { | ||||
|         return route.value.query.recipe_import_url as string | null; | ||||
|       }, | ||||
|     }); | ||||
|  | ||||
|     const importKeywordsAsTags = computed({ | ||||
|       get() { | ||||
|         return route.value.query.import_keywords_as_tags === "1"; | ||||
|       }, | ||||
|       set(keywordsAsTags: boolean) { | ||||
|         let import_keywords_as_tags = "0" | ||||
|         if (keywordsAsTags) { | ||||
|           import_keywords_as_tags = "1" | ||||
|         } | ||||
|         router.replace({query: {...route.value.query, import_keywords_as_tags}}) | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     onMounted(() => { | ||||
|       if (!recipeUrl.value) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       if (recipeUrl.value.includes("https")) { | ||||
|         createByUrl(recipeUrl.value, importKeywordsAsTags.value); | ||||
|  | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|  | ||||
|  | ||||
|     // =================================================== | ||||
|     // Recipe Debug URL Scraper | ||||
|  | ||||
|     const debugTreeView = ref(false); | ||||
|  | ||||
|     const debugData = ref<Recipe | null>(null); | ||||
|  | ||||
|     async function debugUrl(url: string | null) { | ||||
|       if (url === null) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       state.loading = true; | ||||
|  | ||||
|       const { data } = await api.recipes.testCreateOneUrl(url); | ||||
|  | ||||
|       state.loading = false; | ||||
|       debugData.value = data; | ||||
|     } | ||||
|  | ||||
|     // =================================================== | ||||
|     // Recipe URL Import | ||||
|     const domUrlForm = ref<VForm | null>(null); | ||||
|  | ||||
|     async function createByUrl(url: string, importKeywordsAsTags: boolean) { | ||||
|  | ||||
|       if (url === null) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       if (!domUrlForm.value?.validate() || url === "") { | ||||
|         console.log("Invalid URL", url); | ||||
|         return; | ||||
|       } | ||||
|       state.loading = true; | ||||
|       const { response } = await api.recipes.createOneByUrl(url, importKeywordsAsTags); | ||||
|       handleResponse(response); | ||||
|     } | ||||
|  | ||||
|     // =================================================== | ||||
|     // Recipe Create By Name | ||||
|     const newRecipeName = ref(""); | ||||
|     const domCreateByName = ref<VForm | null>(null); | ||||
|  | ||||
|     async function createByName(name: string) { | ||||
|       if (!domCreateByName.value?.validate() || name === "") { | ||||
|         return; | ||||
|       } | ||||
|       const { response } = await api.recipes.createOne({ name }); | ||||
|       // TODO createOne claims to return a Recipe, but actually the API only returns a string | ||||
|       // @ts-ignore See above | ||||
|       handleResponse(response, true); | ||||
|     } | ||||
|  | ||||
|     // =================================================== | ||||
|     // Recipe Import From Zip File | ||||
|     const newRecipeZip = ref<File | null>(null); | ||||
|     const newRecipeZipFileName = "archive"; | ||||
|  | ||||
|     async function createByZip() { | ||||
|       if (!newRecipeZip.value) { | ||||
|         return; | ||||
|       } | ||||
|       const formData = new FormData(); | ||||
|       formData.append(newRecipeZipFileName, newRecipeZip.value); | ||||
|  | ||||
|       const { response } = await api.upload.file("/api/recipes/create-from-zip", formData); | ||||
|       handleResponse(response); | ||||
|     } | ||||
|  | ||||
|     // =================================================== | ||||
|     // Bulk Importer | ||||
|  | ||||
|     const bulkUrls = ref([{ url: "", categories: [], tags: [] }]); | ||||
|     const lockBulkImport = ref(false); | ||||
|  | ||||
|     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"); | ||||
|         lockBulkImport.value = true; | ||||
|       } else { | ||||
|         alert.error("Bulk import process has failed"); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     const { allTags, useAsyncGetAll: getAllTags } = useTags(); | ||||
|     const { allCategories, useAsyncGetAll: getAllCategories } = useCategories(); | ||||
|  | ||||
|     getAllTags(); | ||||
|     getAllCategories(); | ||||
|  | ||||
|     return { | ||||
|       allTags, | ||||
|       allCategories, | ||||
|       tab, | ||||
|       recipeUrl, | ||||
|       importKeywordsAsTags, | ||||
|       bulkCreate, | ||||
|       bulkUrls, | ||||
|       lockBulkImport, | ||||
|       debugTreeView, | ||||
|       tabs, | ||||
|       domCreateByName, | ||||
|       domUrlForm, | ||||
|       newRecipeName, | ||||
|       newRecipeZip, | ||||
|       debugUrl, | ||||
|       debugData, | ||||
|       createByName, | ||||
|       createByUrl, | ||||
|       createByZip, | ||||
|       ...toRefs(state), | ||||
|       validators, | ||||
|       subpages, | ||||
|       subpage, | ||||
|     }; | ||||
|   }, | ||||
|   head() { | ||||
| @@ -569,9 +88,3 @@ export default defineComponent({ | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
| .force-white > a { | ||||
|   color: white !important; | ||||
| } | ||||
| </style> | ||||
|   | ||||
							
								
								
									
										143
									
								
								frontend/pages/recipe/create/bulk.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								frontend/pages/recipe/create/bulk.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,143 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <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> | ||||
|     <section 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"> | ||||
|           <RecipeOrganizerSelector | ||||
|             v-model="bulkUrls[idx].categories" | ||||
|             :items="allCategories || []" | ||||
|             selector-type="category" | ||||
|             :input-attrs="{ | ||||
|               filled: true, | ||||
|               singleLine: true, | ||||
|               dense: true, | ||||
|               rounded: true, | ||||
|               class: 'rounded-lg', | ||||
|               hideDetails: true, | ||||
|               clearable: true, | ||||
|             }" | ||||
|           /> | ||||
|         </v-col> | ||||
|         <v-col cols="12" xs="12" sm="6"> | ||||
|           <RecipeOrganizerSelector | ||||
|             v-model="bulkUrls[idx].tags" | ||||
|             :items="allTags || []" | ||||
|             selector-type="tag" | ||||
|             :input-attrs="{ | ||||
|               filled: true, | ||||
|               singleLine: true, | ||||
|               dense: true, | ||||
|               rounded: true, | ||||
|               class: 'rounded-lg', | ||||
|               hideDetails: true, | ||||
|               clearable: true, | ||||
|             }" | ||||
|           /> | ||||
|         </v-col> | ||||
|       </v-row> | ||||
|       <v-card-actions class="justify-end"> | ||||
|         <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 || lockBulkImport" @click="bulkCreate"> | ||||
|           <template #icon> {{ $globals.icons.check }} </template> Submit | ||||
|         </BaseButton> | ||||
|       </v-card-actions> | ||||
|     </section> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import { defineComponent, reactive, toRefs, ref } from "@nuxtjs/composition-api"; | ||||
| import { useUserApi } from "~/composables/api"; | ||||
| import { alert } from "~/composables/use-toast"; | ||||
| import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue"; | ||||
| import { useCategories, useTags } from "~/composables/recipes"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   components: { RecipeOrganizerSelector }, | ||||
|   setup() { | ||||
|     const state = reactive({ | ||||
|       error: false, | ||||
|       loading: false, | ||||
|     }); | ||||
|  | ||||
|     const api = useUserApi(); | ||||
|  | ||||
|     const bulkUrls = ref([{ url: "", categories: [], tags: [] }]); | ||||
|     const lockBulkImport = ref(false); | ||||
|  | ||||
|     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"); | ||||
|         lockBulkImport.value = true; | ||||
|       } else { | ||||
|         alert.error("Bulk import process has failed"); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     const { allTags, useAsyncGetAll: getAllTags } = useTags(); | ||||
|     const { allCategories, useAsyncGetAll: getAllCategories } = useCategories(); | ||||
|  | ||||
|     getAllTags(); | ||||
|     getAllCategories(); | ||||
|  | ||||
|     return { | ||||
|       allTags, | ||||
|       allCategories, | ||||
|       bulkCreate, | ||||
|       bulkUrls, | ||||
|       lockBulkImport, | ||||
|       ...toRefs(state), | ||||
|     }; | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
							
								
								
									
										110
									
								
								frontend/pages/recipe/create/debug.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								frontend/pages/recipe/create/debug.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <v-form ref="domUrlForm" @submit.prevent="debugUrl(recipeUrl)"> | ||||
|       <v-card flat> | ||||
|         <v-card-title class="headline"> Recipe Debugger </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 to scrape is | ||||
|           not supported by Mealie or it's scraper library. | ||||
|           <v-text-field | ||||
|             v-model="recipeUrl" | ||||
|             :label="$t('new-recipe.recipe-url')" | ||||
|             validate-on-blur | ||||
|             :prepend-inner-icon="$globals.icons.link" | ||||
|             autofocus | ||||
|             filled | ||||
|             clearable | ||||
|             rounded | ||||
|             class="rounded-lg mt-2" | ||||
|             :rules="[validators.url]" | ||||
|             :hint="$t('new-recipe.url-form-hint')" | ||||
|             persistent-hint | ||||
|           ></v-text-field> | ||||
|         </v-card-text> | ||||
|         <v-card-actions class="justify-center"> | ||||
|           <div style="width: 250px"> | ||||
|             <BaseButton :disabled="recipeUrl === null" rounded block type="submit" color="info" :loading="loading"> | ||||
|               <template #icon> | ||||
|                 {{ $globals.icons.robot }} | ||||
|               </template> | ||||
|               Debug | ||||
|             </BaseButton> | ||||
|           </div> | ||||
|         </v-card-actions> | ||||
|       </v-card> | ||||
|     </v-form> | ||||
|     <section v-if="debugData"> | ||||
|       <v-checkbox v-model="debugTreeView" label="Tree View"></v-checkbox> | ||||
|       <LazyRecipeJsonEditor | ||||
|         v-model="debugData" | ||||
|         class="primary" | ||||
|         :options="{ | ||||
|           mode: debugTreeView ? 'tree' : 'code', | ||||
|           search: false, | ||||
|           indentation: 4, | ||||
|           mainMenuBar: false, | ||||
|         }" | ||||
|         height="700px" | ||||
|       /> | ||||
|     </section> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import { defineComponent, reactive, toRefs, ref, useRouter, computed, useRoute } from "@nuxtjs/composition-api"; | ||||
| import { useUserApi } from "~/composables/api"; | ||||
| import { validators } from "~/composables/use-validators"; | ||||
| import { Recipe } from "~/types/api-types/recipe"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   setup() { | ||||
|     const state = reactive({ | ||||
|       error: false, | ||||
|       loading: false, | ||||
|     }); | ||||
|  | ||||
|     const api = useUserApi(); | ||||
|     const route = useRoute(); | ||||
|     const router = useRouter(); | ||||
|  | ||||
|     const recipeUrl = computed({ | ||||
|       set(recipe_import_url: string | null) { | ||||
|         if (recipe_import_url !== null) { | ||||
|           recipe_import_url = recipe_import_url.trim(); | ||||
|           router.replace({ query: { ...route.value.query, recipe_import_url } }); | ||||
|         } | ||||
|       }, | ||||
|       get() { | ||||
|         return route.value.query.recipe_import_url as string | null; | ||||
|       }, | ||||
|     }); | ||||
|  | ||||
|     const debugTreeView = ref(false); | ||||
|  | ||||
|     const debugData = ref<Recipe | null>(null); | ||||
|  | ||||
|     async function debugUrl(url: string | null) { | ||||
|       if (url === null) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       state.loading = true; | ||||
|  | ||||
|       const { data } = await api.recipes.testCreateOneUrl(url); | ||||
|  | ||||
|       state.loading = false; | ||||
|       debugData.value = data; | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|       recipeUrl, | ||||
|       debugTreeView, | ||||
|       debugUrl, | ||||
|       debugData, | ||||
|       ...toRefs(state), | ||||
|       validators, | ||||
|     }; | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
							
								
								
									
										18
									
								
								frontend/pages/recipe/create/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								frontend/pages/recipe/create/index.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| <template> | ||||
|   <div></div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import { defineComponent, onMounted, useRouter } from "@nuxtjs/composition-api"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   setup() { | ||||
|     const router = useRouter(); | ||||
|     onMounted(() => { | ||||
|       // Force redirect to first valid page | ||||
|       router.replace("/recipe/create/url"); | ||||
|     }); | ||||
|     return {}; | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
							
								
								
									
										82
									
								
								frontend/pages/recipe/create/new.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								frontend/pages/recipe/create/new.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| <template> | ||||
|   <v-card flat> | ||||
|     <v-card-title class="headline"> Create Recipe </v-card-title> | ||||
|     <v-card-text> | ||||
|       Create a recipe by providing the name. All recipes must have unique names. | ||||
|       <v-form ref="domCreateByName"> | ||||
|         <v-text-field | ||||
|           v-model="newRecipeName" | ||||
|           :label="$t('recipe.recipe-name')" | ||||
|           :prepend-inner-icon="$globals.icons.primary" | ||||
|           validate-on-blur | ||||
|           autofocus | ||||
|           filled | ||||
|           clearable | ||||
|           class="rounded-lg mt-2" | ||||
|           rounded | ||||
|           :rules="[validators.required]" | ||||
|           hint="New recipe names must be unique" | ||||
|           persistent-hint | ||||
|         ></v-text-field> | ||||
|       </v-form> | ||||
|     </v-card-text> | ||||
|     <v-card-actions class="justify-center"> | ||||
|       <div style="width: 250px"> | ||||
|         <BaseButton | ||||
|           :disabled="newRecipeName === ''" | ||||
|           rounded | ||||
|           block | ||||
|           :loading="loading" | ||||
|           @click="createByName(newRecipeName)" | ||||
|         /> | ||||
|       </div> | ||||
|     </v-card-actions> | ||||
|   </v-card> | ||||
| </template> | ||||
| <script lang="ts"> | ||||
| import { defineComponent, reactive, toRefs, ref, useRouter } from "@nuxtjs/composition-api"; | ||||
| import { AxiosResponse } from "axios"; | ||||
| import { useUserApi } from "~/composables/api"; | ||||
| import { validators } from "~/composables/use-validators"; | ||||
| import { VForm } from "~/types/vuetify"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   setup() { | ||||
|     const state = reactive({ | ||||
|       error: false, | ||||
|       loading: false, | ||||
|     }); | ||||
|     const api = useUserApi(); | ||||
|     const router = useRouter(); | ||||
|  | ||||
|     function handleResponse(response: AxiosResponse<string> | null, edit = false) { | ||||
|       if (response?.status !== 201) { | ||||
|         state.error = true; | ||||
|         state.loading = false; | ||||
|         return; | ||||
|       } | ||||
|       router.push(`/recipe/${response.data}?edit=${edit.toString()}`); | ||||
|     } | ||||
|  | ||||
|     const newRecipeName = ref(""); | ||||
|     const domCreateByName = ref<VForm | null>(null); | ||||
|  | ||||
|     async function createByName(name: string) { | ||||
|       if (!domCreateByName.value?.validate() || name === "") { | ||||
|         return; | ||||
|       } | ||||
|       const { response } = await api.recipes.createOne({ name }); | ||||
|       // TODO createOne claims to return a Recipe, but actually the API only returns a string | ||||
|       // @ts-ignore See above | ||||
|       handleResponse(response, true); | ||||
|     } | ||||
|     return { | ||||
|       domCreateByName, | ||||
|       newRecipeName, | ||||
|       createByName, | ||||
|       ...toRefs(state), | ||||
|       validators, | ||||
|     }; | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
							
								
								
									
										159
									
								
								frontend/pages/recipe/create/url.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								frontend/pages/recipe/create/url.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,159 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <v-form ref="domUrlForm" @submit.prevent="createByUrl(recipeUrl, importKeywordsAsTags)"> | ||||
|       <v-card flat> | ||||
|         <v-card-title class="headline"> Scrape Recipe </v-card-title> | ||||
|         <v-card-text> | ||||
|           Scrape a recipe by url. Provide the url for the site you want to scrape, and Mealie will attempt to scrape the | ||||
|           recipe from that site and add it to your collection. | ||||
|           <v-text-field | ||||
|             v-model="recipeUrl" | ||||
|             :label="$t('new-recipe.recipe-url')" | ||||
|             :prepend-inner-icon="$globals.icons.link" | ||||
|             validate-on-blur | ||||
|             autofocus | ||||
|             filled | ||||
|             clearable | ||||
|             class="rounded-lg mt-2" | ||||
|             rounded | ||||
|             :rules="[validators.url]" | ||||
|             :hint="$t('new-recipe.url-form-hint')" | ||||
|             persistent-hint | ||||
|           ></v-text-field> | ||||
|           <v-checkbox v-model="importKeywordsAsTags" label="Import original keywords as tags"> </v-checkbox> | ||||
|         </v-card-text> | ||||
|         <v-card-actions class="justify-center"> | ||||
|           <div style="width: 250px"> | ||||
|             <BaseButton :disabled="recipeUrl === null" rounded block type="submit" :loading="loading" /> | ||||
|           </div> | ||||
|         </v-card-actions> | ||||
|       </v-card> | ||||
|     </v-form> | ||||
|     <v-expand-transition> | ||||
|       <v-alert v-show="error" color="error" class="mt-6 white--text"> | ||||
|         <v-card-title class="ma-0 pa-0"> | ||||
|           <v-icon left color="white" x-large> {{ $globals.icons.robot }} </v-icon> | ||||
|           {{ $t("new-recipe.error-title") }} | ||||
|         </v-card-title> | ||||
|         <v-divider class="my-3 mx-2"></v-divider> | ||||
|  | ||||
|         <p> | ||||
|           {{ $t("new-recipe.error-details") }} | ||||
|         </p> | ||||
|         <div class="d-flex row justify-space-around my-3 force-white"> | ||||
|           <a | ||||
|             class="dark" | ||||
|             href="https://developers.google.com/search/docs/data-types/recipe" | ||||
|             target="_blank" | ||||
|             rel="noreferrer nofollow" | ||||
|           > | ||||
|             {{ $t("new-recipe.google-ld-json-info") }} | ||||
|           </a> | ||||
|           <a href="https://github.com/hay-kot/mealie/issues" target="_blank" rel="noreferrer nofollow"> | ||||
|             {{ $t("new-recipe.github-issues") }} | ||||
|           </a> | ||||
|           <a href="https://schema.org/Recipe" target="_blank" rel="noreferrer nofollow"> | ||||
|             {{ $t("new-recipe.recipe-markup-specification") }} | ||||
|           </a> | ||||
|         </div> | ||||
|       </v-alert> | ||||
|     </v-expand-transition> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import { defineComponent, reactive, toRefs, ref, useRouter, computed, useRoute } from "@nuxtjs/composition-api"; | ||||
| import { AxiosResponse } from "axios"; | ||||
| import { onMounted } from "vue-demi"; | ||||
| import { useUserApi } from "~/composables/api"; | ||||
| import { validators } from "~/composables/use-validators"; | ||||
| import { VForm } from "~/types/vuetify"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   setup() { | ||||
|     const state = reactive({ | ||||
|       error: false, | ||||
|       loading: false, | ||||
|     }); | ||||
|  | ||||
|     const api = useUserApi(); | ||||
|     const route = useRoute(); | ||||
|     const router = useRouter(); | ||||
|  | ||||
|     function handleResponse(response: AxiosResponse<string> | null, edit = false) { | ||||
|       if (response?.status !== 201) { | ||||
|         state.error = true; | ||||
|         state.loading = false; | ||||
|         return; | ||||
|       } | ||||
|       router.push(`/recipe/${response.data}?edit=${edit.toString()}`); | ||||
|     } | ||||
|  | ||||
|     const recipeUrl = computed({ | ||||
|       set(recipe_import_url: string | null) { | ||||
|         if (recipe_import_url !== null) { | ||||
|           recipe_import_url = recipe_import_url.trim(); | ||||
|           router.replace({ query: { ...route.value.query, recipe_import_url } }); | ||||
|         } | ||||
|       }, | ||||
|       get() { | ||||
|         return route.value.query.recipe_import_url as string | null; | ||||
|       }, | ||||
|     }); | ||||
|  | ||||
|     const importKeywordsAsTags = computed({ | ||||
|       get() { | ||||
|         return route.value.query.import_keywords_as_tags === "1"; | ||||
|       }, | ||||
|       set(keywordsAsTags: boolean) { | ||||
|         let import_keywords_as_tags = "0"; | ||||
|         if (keywordsAsTags) { | ||||
|           import_keywords_as_tags = "1"; | ||||
|         } | ||||
|         router.replace({ query: { ...route.value.query, import_keywords_as_tags } }); | ||||
|       }, | ||||
|     }); | ||||
|  | ||||
|     onMounted(() => { | ||||
|       if (!recipeUrl.value) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       if (recipeUrl.value.includes("https")) { | ||||
|         createByUrl(recipeUrl.value, importKeywordsAsTags.value); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     const domUrlForm = ref<VForm | null>(null); | ||||
|  | ||||
|     async function createByUrl(url: string, importKeywordsAsTags: boolean) { | ||||
|       if (url === null) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       if (!domUrlForm.value?.validate() || url === "") { | ||||
|         console.log("Invalid URL", url); | ||||
|         return; | ||||
|       } | ||||
|       state.loading = true; | ||||
|       const { response } = await api.recipes.createOneByUrl(url, importKeywordsAsTags); | ||||
|       handleResponse(response); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|       recipeUrl, | ||||
|       importKeywordsAsTags, | ||||
|       domUrlForm, | ||||
|       createByUrl, | ||||
|       ...toRefs(state), | ||||
|       validators, | ||||
|     }; | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
| .force-white > a { | ||||
|   color: white !important; | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										78
									
								
								frontend/pages/recipe/create/zip.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								frontend/pages/recipe/create/zip.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | ||||
| <template> | ||||
|   <v-form> | ||||
|     <v-card flat> | ||||
|       <v-card-title class="headline"> Import from Zip </v-card-title> | ||||
|       <v-card-text> | ||||
|         Import a single recipe that was exported from another Mealie instance. | ||||
|         <v-file-input | ||||
|           v-model="newRecipeZip" | ||||
|           accept=".zip" | ||||
|           label=".zip" | ||||
|           filled | ||||
|           clearable | ||||
|           class="rounded-lg mt-2" | ||||
|           rounded | ||||
|           truncate-length="100" | ||||
|           hint=".zip files must have been exported from Mealie" | ||||
|           persistent-hint | ||||
|           prepend-icon="" | ||||
|           :prepend-inner-icon="$globals.icons.zip" | ||||
|         > | ||||
|         </v-file-input> | ||||
|       </v-card-text> | ||||
|       <v-card-actions class="justify-center"> | ||||
|         <div style="width: 250px"> | ||||
|           <BaseButton :disabled="newRecipeZip === null" large rounded block :loading="loading" @click="createByZip" /> | ||||
|         </div> | ||||
|       </v-card-actions> | ||||
|     </v-card> | ||||
|   </v-form> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import { defineComponent, reactive, toRefs, ref, useRouter } from "@nuxtjs/composition-api"; | ||||
| import { AxiosResponse } from "axios"; | ||||
| import { useUserApi } from "~/composables/api"; | ||||
| import { validators } from "~/composables/use-validators"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   setup() { | ||||
|     const state = reactive({ | ||||
|       error: false, | ||||
|       loading: false, | ||||
|     }); | ||||
|     const api = useUserApi(); | ||||
|     const router = useRouter(); | ||||
|  | ||||
|     function handleResponse(response: AxiosResponse<string> | null, edit = false) { | ||||
|       if (response?.status !== 201) { | ||||
|         state.error = true; | ||||
|         state.loading = false; | ||||
|         return; | ||||
|       } | ||||
|       router.push(`/recipe/${response.data}?edit=${edit.toString()}`); | ||||
|     } | ||||
|  | ||||
|     const newRecipeZip = ref<File | null>(null); | ||||
|     const newRecipeZipFileName = "archive"; | ||||
|  | ||||
|     async function createByZip() { | ||||
|       if (!newRecipeZip.value) { | ||||
|         return; | ||||
|       } | ||||
|       const formData = new FormData(); | ||||
|       formData.append(newRecipeZipFileName, newRecipeZip.value); | ||||
|  | ||||
|       const { response } = await api.upload.file("/api/recipes/create-from-zip", formData); | ||||
|       handleResponse(response); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|       newRecipeZip, | ||||
|       createByZip, | ||||
|       ...toRefs(state), | ||||
|       validators, | ||||
|     }; | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
		Reference in New Issue
	
	Block a user