mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-25 15:53:26 -04:00 
			
		
		
		
	refactor(frontend): ♻️ rewrite search componenets to typescript
This commit is contained in:
		
							parent
							
								
									1981e191be
								
							
						
					
					
						commit
						bde885dc84
					
				
							
								
								
									
										45
									
								
								frontend/api/class-interfaces/categories.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								frontend/api/class-interfaces/categories.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | ||||
| import { BaseCRUDAPI } from "./_base"; | ||||
| 
 | ||||
| const prefix = "/api"; | ||||
| 
 | ||||
| export interface Category { | ||||
|   name: string; | ||||
|   id: number; | ||||
|   slug: string; | ||||
| } | ||||
| 
 | ||||
| export interface CreateCategory { | ||||
|   name: string; | ||||
| } | ||||
| 
 | ||||
| const routes = { | ||||
|   categories: `${prefix}/categories`, | ||||
|   categoriesEmpty: `${prefix}/categories/empty`, | ||||
| 
 | ||||
|   categoriesCategory: (category: string) => `${prefix}/categories/${category}`, | ||||
| }; | ||||
| 
 | ||||
| export class CategoriesAPI extends BaseCRUDAPI<Category, CreateCategory> { | ||||
|   baseRoute: string = routes.categories; | ||||
|   itemRoute = routes.categoriesCategory; | ||||
| 
 | ||||
|   /** Returns a list of categories that do not contain any recipes | ||||
|    */ | ||||
|   async getEmptyCategories() { | ||||
|     return await this.requests.get(routes.categoriesEmpty); | ||||
|   } | ||||
| 
 | ||||
|   /** Returns a list of recipes associated with the provided category. | ||||
|    */ | ||||
|   async getAllRecipesByCategory(category: string) { | ||||
|     return await this.requests.get(routes.categoriesCategory(category)); | ||||
|   } | ||||
| 
 | ||||
|   /** Removes a recipe category from the database. Deleting a | ||||
|    * category does not impact a recipe. The category will be removed | ||||
|    * from any recipes that contain it | ||||
|    */ | ||||
|   async deleteRecipeCategory(category: string) { | ||||
|     return await this.requests.delete(routes.categoriesCategory(category)); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										45
									
								
								frontend/api/class-interfaces/tags.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								frontend/api/class-interfaces/tags.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | ||||
| import { BaseCRUDAPI } from "./_base"; | ||||
| 
 | ||||
| const prefix = "/api"; | ||||
| 
 | ||||
| export interface Tag { | ||||
|   name: string; | ||||
|   id: number; | ||||
|   slug: string; | ||||
| } | ||||
| 
 | ||||
| export interface CreateTag { | ||||
|   name: string; | ||||
| } | ||||
| 
 | ||||
| const routes = { | ||||
|   tags: `${prefix}/tags`, | ||||
|   tagsEmpty: `${prefix}/tags/empty`, | ||||
| 
 | ||||
|   tagsTag: (tag: string) => `${prefix}/tags/${tag}`, | ||||
| }; | ||||
| 
 | ||||
| export class TagsAPI extends BaseCRUDAPI<Tag, CreateTag> { | ||||
|   baseRoute: string = routes.tags; | ||||
|   itemRoute = routes.tagsTag; | ||||
| 
 | ||||
|   /** Returns a list of categories that do not contain any recipes | ||||
|    */ | ||||
|   async getEmptyCategories() { | ||||
|     return await this.requests.get(routes.tagsEmpty); | ||||
|   } | ||||
| 
 | ||||
|   /** Returns a list of recipes associated with the provided category. | ||||
|    */ | ||||
|   async getAllRecipesByCategory(category: string) { | ||||
|     return await this.requests.get(routes.tagsTag(category)); | ||||
|   } | ||||
| 
 | ||||
|   /** Removes a recipe category from the database. Deleting a | ||||
|    * category does not impact a recipe. The category will be removed | ||||
|    * from any recipes that contain it | ||||
|    */ | ||||
|   async deleteRecipeCategory(category: string) { | ||||
|     return await this.requests.delete(routes.tagsTag(category)); | ||||
|   } | ||||
| } | ||||
| @ -5,6 +5,8 @@ import { DebugAPI } from "./class-interfaces/debug"; | ||||
| import { EventsAPI } from "./class-interfaces/events"; | ||||
| import { BackupAPI } from "./class-interfaces/backups"; | ||||
| import { UploadFile } from "./class-interfaces/upload"; | ||||
| import { CategoriesAPI } from "./class-interfaces/categories"; | ||||
| import { TagsAPI } from "./class-interfaces/tags"; | ||||
| import { ApiRequestInstance } from "~/types/api"; | ||||
| 
 | ||||
| class Api { | ||||
| @ -15,6 +17,10 @@ class Api { | ||||
|   public debug: DebugAPI; | ||||
|   public events: EventsAPI; | ||||
|   public backups: BackupAPI; | ||||
|   public categories: CategoriesAPI; | ||||
|   public tags: TagsAPI; | ||||
| 
 | ||||
|   // Utils
 | ||||
|   public upload: UploadFile; | ||||
| 
 | ||||
|   constructor(requests: ApiRequestInstance) { | ||||
| @ -22,12 +28,21 @@ class Api { | ||||
|       return Api.instance; | ||||
|     } | ||||
| 
 | ||||
|     // Recipes
 | ||||
|     this.recipes = new RecipeAPI(requests); | ||||
|     this.categories = new CategoriesAPI(requests); | ||||
|     this.tags = new TagsAPI(requests); | ||||
| 
 | ||||
|     // Users
 | ||||
|     this.users = new UserApi(requests); | ||||
|     this.groups = new GroupAPI(requests); | ||||
| 
 | ||||
|     // Admin
 | ||||
|     this.debug = new DebugAPI(requests); | ||||
|     this.events = new EventsAPI(requests); | ||||
|     this.backups = new BackupAPI(requests); | ||||
| 
 | ||||
|     // Utils
 | ||||
|     this.upload = new UploadFile(requests); | ||||
| 
 | ||||
|     Object.freeze(this); | ||||
|  | ||||
| @ -1,6 +1,5 @@ | ||||
| import axios, { AxiosResponse } from "axios"; | ||||
| 
 | ||||
| 
 | ||||
| interface RequestResponse<T> { | ||||
|   response: AxiosResponse<T> | null; | ||||
|   data: T | null; | ||||
| @ -21,9 +20,9 @@ const request = { | ||||
| }; | ||||
| 
 | ||||
| export const requests = { | ||||
|   async get<T>(url: string, queryParams = {}): Promise<RequestResponse<T>> { | ||||
|   async get<T>(url: string, params = {}): Promise<RequestResponse<T>> { | ||||
|     let error = null; | ||||
|     const response = await axios.get<T>(url, { params: { queryParams } }).catch((e) => { | ||||
|     const response = await axios.get<T>(url, { ...params }).catch((e) => { | ||||
|       error = e; | ||||
|     }); | ||||
|     if (response != null) { | ||||
|  | ||||
| @ -126,7 +126,7 @@ export default { | ||||
|       default: null, | ||||
|     }, | ||||
|     hardLimit: { | ||||
|       type: Number, | ||||
|       type: [String, Number], | ||||
|       default: 99999, | ||||
|     }, | ||||
|     mobileCards: { | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <slot> | ||||
|       <v-btn icon @click="dialog = true" class="mt-n1"> | ||||
|       <v-btn icon class="mt-n1" @click="dialog = true"> | ||||
|         <v-icon :color="color">{{ $globals.icons.create }}</v-icon> | ||||
|       </v-btn> | ||||
|     </slot> | ||||
| @ -21,12 +21,12 @@ | ||||
|         <v-card-title> </v-card-title> | ||||
|         <v-form @submit.prevent="select"> | ||||
|           <v-card-text> | ||||
|             <v-text-field dense :label="inputLabel" v-model="itemName" :rules="[rules.required]"></v-text-field> | ||||
|             <v-text-field v-model="itemName" dense :label="inputLabel" :rules="[rules.required]"></v-text-field> | ||||
|           </v-card-text> | ||||
|           <v-card-actions> | ||||
|             <TheButton cancel @click="dialog = false" /> | ||||
|             <BaseButton cancel @click="dialog = false" /> | ||||
|             <v-spacer></v-spacer> | ||||
|             <TheButton type="submit" create :disabled="!itemName" /> | ||||
|             <BaseButton type="submit" create :disabled="!itemName" /> | ||||
|           </v-card-actions> | ||||
|         </v-form> | ||||
|       </v-card> | ||||
| @ -35,25 +35,39 @@ | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import { api } from "@/api"; | ||||
| import { defineComponent } from "vue-demi"; | ||||
| import { useApiSingleton } from "~/composables/use-api"; | ||||
| const CREATED_ITEM_EVENT = "created-item"; | ||||
| export default { | ||||
| export default defineComponent({ | ||||
|   props: { | ||||
|     buttonText: String, | ||||
|     value: String, | ||||
|     buttonText: { | ||||
|       type: String, | ||||
|       default: "Add", | ||||
|     }, | ||||
|     value: { | ||||
|       type: String, | ||||
|       default: "", | ||||
|     }, | ||||
|     color: { | ||||
|       type: String, | ||||
|       default: null, | ||||
|     }, | ||||
|     tagDialog: { | ||||
|       type: Boolean, | ||||
|       default: true, | ||||
|     }, | ||||
|   }, | ||||
|   setup() { | ||||
|     const api = useApiSingleton(); | ||||
| 
 | ||||
|     return { api }; | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       dialog: false, | ||||
|       itemName: "", | ||||
|       rules: { | ||||
|         required: val => !!val || "A Name is Required", | ||||
|         required: (val) => !!val || "A Name is Required", | ||||
|       }, | ||||
|     }; | ||||
|   }, | ||||
| @ -79,10 +93,10 @@ export default { | ||||
|     async select() { | ||||
|       const newItem = await (async () => { | ||||
|         if (this.tagDialog) { | ||||
|           const newItem = await api.tags.create(this.itemName); | ||||
|           const newItem = await this.api.tags.createOne({ name: this.itemName }); | ||||
|           return newItem; | ||||
|         } else { | ||||
|           const newItem = await api.categories.create(this.itemName); | ||||
|           const newItem = await this.api.categories.createOne({ name: this.itemName }); | ||||
|           return newItem; | ||||
|         } | ||||
|       })(); | ||||
| @ -91,7 +105,7 @@ export default { | ||||
|       this.dialog = false; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style></style> | ||||
							
								
								
									
										150
									
								
								frontend/components/Domain/Recipe/RecipeCategoryTagSelector.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								frontend/components/Domain/Recipe/RecipeCategoryTagSelector.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,150 @@ | ||||
| <template> | ||||
|   <v-autocomplete | ||||
|     v-model="selected" | ||||
|     :items="activeItems" | ||||
|     :value="value" | ||||
|     :label="inputLabel" | ||||
|     chips | ||||
|     deletable-chips | ||||
|     :dense="dense" | ||||
|     item-text="name" | ||||
|     persistent-hint | ||||
|     multiple | ||||
|     :hint="hint" | ||||
|     :solo="solo" | ||||
|     :return-object="returnObject" | ||||
|     :flat="flat" | ||||
|     @input="emitChange" | ||||
|   > | ||||
|     <template #selection="data"> | ||||
|       <v-chip | ||||
|         :key="data.index" | ||||
|         class="ma-1" | ||||
|         :input-value="data.selected" | ||||
|         close | ||||
|         label | ||||
|         color="accent" | ||||
|         dark | ||||
|         @click:close="removeByIndex(data.index)" | ||||
|       > | ||||
|         {{ data.item.name || data.item }} | ||||
|       </v-chip> | ||||
|     </template> | ||||
|     <template #append-outer=""> | ||||
|       <RecipeCategoryTagDialog v-if="showAdd" :tag-dialog="tagSelector" @created-item="pushToItem" /> | ||||
|     </template> | ||||
|   </v-autocomplete> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import RecipeCategoryTagDialog from "./RecipeCategoryTagDialog"; | ||||
| import { useApiSingleton } from "~/composables/use-api"; | ||||
| import { useTags, useCategories } from "~/composables/use-tags-categories"; | ||||
| const MOUNTED_EVENT = "mounted"; | ||||
| export default { | ||||
|   components: { | ||||
|     RecipeCategoryTagDialog, | ||||
|   }, | ||||
|   props: { | ||||
|     value: { | ||||
|       type: Array, | ||||
|       required: true, | ||||
|     }, | ||||
|     solo: { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     }, | ||||
|     dense: { | ||||
|       type: Boolean, | ||||
|       default: true, | ||||
|     }, | ||||
|     returnObject: { | ||||
|       type: Boolean, | ||||
|       default: true, | ||||
|     }, | ||||
|     tagSelector: { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     }, | ||||
|     hint: { | ||||
|       type: String, | ||||
|       default: null, | ||||
|     }, | ||||
|     showAdd: { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     }, | ||||
|     showLabel: { | ||||
|       type: Boolean, | ||||
|       default: true, | ||||
|     }, | ||||
|   }, | ||||
| 
 | ||||
|   setup() { | ||||
|     const api = useApiSingleton(); | ||||
| 
 | ||||
|     const { allTags, useAsyncGetAll: getAllTags } = useTags(); | ||||
|     const { allCategories, useAsyncGetAll: getAllCategories } = useCategories(); | ||||
|     getAllCategories(); | ||||
|     getAllTags(); | ||||
| 
 | ||||
|     return { api, allTags, allCategories }; | ||||
|   }, | ||||
| 
 | ||||
|   data() { | ||||
|     return { | ||||
|       selected: [], | ||||
|     }; | ||||
|   }, | ||||
| 
 | ||||
|   computed: { | ||||
|     inputLabel() { | ||||
|       if (!this.showLabel) return null; | ||||
|       return this.tagSelector ? this.$t("tag.tags") : this.$t("recipe.categories"); | ||||
|     }, | ||||
|     activeItems() { | ||||
|       let ItemObjects = []; | ||||
|       if (this.tagSelector) ItemObjects = this.allTags; | ||||
|       else { | ||||
|         ItemObjects = this.allCategories; | ||||
|       } | ||||
|       if (this.returnObject) return ItemObjects; | ||||
|       else { | ||||
|         return ItemObjects.map((x) => x.name); | ||||
|       } | ||||
|     }, | ||||
|     flat() { | ||||
|       if (this.selected) { | ||||
|         return this.selected.length > 0 && this.solo; | ||||
|       } | ||||
|       return false; | ||||
|     }, | ||||
|   }, | ||||
| 
 | ||||
|   watch: { | ||||
|     value(val) { | ||||
|       this.selected = val; | ||||
|     }, | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.$emit(MOUNTED_EVENT); | ||||
|     this.setInit(this.value); | ||||
|   }, | ||||
|   methods: { | ||||
|     emitChange() { | ||||
|       this.$emit("input", this.selected); | ||||
|     }, | ||||
|     setInit(val) { | ||||
|       this.selected = val; | ||||
|     }, | ||||
|     removeByIndex(index) { | ||||
|       this.selected.splice(index, 1); | ||||
|     }, | ||||
|     pushToItem(createdItem) { | ||||
|       createdItem = this.returnObject ? createdItem : createdItem.name; | ||||
|       this.selected.push(createdItem); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| @ -0,0 +1,57 @@ | ||||
| <template> | ||||
|   <v-toolbar dense flat> | ||||
|     <v-btn-toggle v-model="selected" tile group color="primary accent-3" mandatory @change="emitMulti"> | ||||
|       <v-btn small :value="false"> | ||||
|         {{ $t("search.include") }} | ||||
|       </v-btn> | ||||
| 
 | ||||
|       <v-btn small :value="true"> | ||||
|         {{ $t("search.exclude") }} | ||||
|       </v-btn> | ||||
|     </v-btn-toggle> | ||||
|     <v-spacer></v-spacer> | ||||
|     <v-btn-toggle v-model="match" tile group color="primary accent-3" mandatory @change="emitMulti"> | ||||
|       <v-btn small :value="false"> | ||||
|         {{ $t("search.and") }} | ||||
|       </v-btn> | ||||
|       <v-btn small :value="true"> | ||||
|         {{ $t("search.or") }} | ||||
|       </v-btn> | ||||
|     </v-btn-toggle> | ||||
|   </v-toolbar> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from "vue-demi"; | ||||
| 
 | ||||
| type SelectionValue = "include" | "exclude" | "any"; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
|   props: { | ||||
|     value: { | ||||
|       type: String as () => SelectionValue, | ||||
|       default: "include", | ||||
|     }, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       selected: false, | ||||
|       match: false, | ||||
|     }; | ||||
|   }, | ||||
|   methods: { | ||||
|     emitChange() { | ||||
|       this.$emit("input", this.selected); | ||||
|     }, | ||||
|     emitMulti() { | ||||
|       const updateData = { | ||||
|         exclude: this.selected, | ||||
|         matchAny: this.match, | ||||
|       }; | ||||
|       this.$emit("update", updateData); | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped></style> | ||||
| @ -23,9 +23,9 @@ const request = { | ||||
| 
 | ||||
| function getRequests(axoisInstance: NuxtAxiosInstance): ApiRequestInstance { | ||||
|   const requests = { | ||||
|     async get<T>(url: string, queryParams = {}): Promise<RequestResponse<T>> { | ||||
|     async get<T>(url: string, params = {}): Promise<RequestResponse<T>> { | ||||
|       let error = null; | ||||
|       const response = await axoisInstance.get<T>(url, { params: { queryParams } }).catch((e) => { | ||||
|       const response = await axoisInstance.get<T>(url, params).catch((e) => { | ||||
|         error = e; | ||||
|       }); | ||||
|       if (response != null) { | ||||
| @ -53,12 +53,9 @@ function getRequests(axoisInstance: NuxtAxiosInstance): ApiRequestInstance { | ||||
|   return requests; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| export const useApiSingleton = function (): Api { | ||||
|   const { $axios } = useContext(); | ||||
|   const requests = getRequests($axios); | ||||
| 
 | ||||
|   return new Api(requests) | ||||
|   return new Api(requests); | ||||
| }; | ||||
|  | ||||
| @ -1,37 +1,35 @@ | ||||
| import {  useAsync, ref } from "@nuxtjs/composition-api"; | ||||
| import { useAsync, ref, reactive } from "@nuxtjs/composition-api"; | ||||
| import { useApiSingleton } from "~/composables/use-api"; | ||||
| import { Recipe } from "~/types/api-types/recipe"; | ||||
| 
 | ||||
| export const useRecipeContext = function () { | ||||
|   const api = useApiSingleton(); | ||||
|   const loading = ref(false) | ||||
| 
 | ||||
|   const loading = ref(false); | ||||
| 
 | ||||
|   function getBySlug(slug: string) { | ||||
|     loading.value = true | ||||
|     loading.value = true; | ||||
|     const recipe = useAsync(async () => { | ||||
|       const { data } = await api.recipes.getOne(slug); | ||||
|       return data; | ||||
|     }, slug); | ||||
| 
 | ||||
|     loading.value = false | ||||
|     loading.value = false; | ||||
|     return recipe; | ||||
|   } | ||||
| 
 | ||||
|   async function  deleteRecipe(slug: string) { | ||||
|     loading.value = true | ||||
|   async function deleteRecipe(slug: string) { | ||||
|     loading.value = true; | ||||
|     const { data } = await api.recipes.deleteOne(slug); | ||||
|     loading.value = false | ||||
|     loading.value = false; | ||||
|     return data; | ||||
|   } | ||||
| 
 | ||||
|   async function updateRecipe(slug: string, recipe: Recipe) { | ||||
|     loading.value = true | ||||
|     loading.value = true; | ||||
|     const { data } = await api.recipes.updateOne(slug, recipe); | ||||
|     loading.value = false | ||||
|     loading.value = false; | ||||
|     return data; | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   return {loading, getBySlug, deleteRecipe, updateRecipe} | ||||
|   return { loading, getBySlug, deleteRecipe, updateRecipe }; | ||||
| }; | ||||
|  | ||||
							
								
								
									
										51
									
								
								frontend/composables/use-recipes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								frontend/composables/use-recipes.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,51 @@ | ||||
| import { useAsync, ref } from "@nuxtjs/composition-api"; | ||||
| import { useAsyncKey } from "./use-utils"; | ||||
| import { useApiSingleton } from "~/composables/use-api"; | ||||
| import { Recipe } from "~/types/api-types/recipe"; | ||||
| 
 | ||||
| export const allRecipes = ref<Recipe[] | null>([]); | ||||
| export const recentRecipes = ref<Recipe[] | null>([]); | ||||
| 
 | ||||
| export const useRecipes = (all = false, fetchRecipes = true) => { | ||||
|   const api = useApiSingleton(); | ||||
| 
 | ||||
|   // recipes is non-reactive!!
 | ||||
|   const { recipes, start, end } = (() => { | ||||
|     if (all) { | ||||
|       return { | ||||
|         recipes: allRecipes, | ||||
|         start: 0, | ||||
|         end: 9999, | ||||
|       }; | ||||
|     } else { | ||||
|       return { | ||||
|         recipes: recentRecipes, | ||||
|         start: 0, | ||||
|         end: 30, | ||||
|       }; | ||||
|     } | ||||
|   })(); | ||||
| 
 | ||||
|   async function refreshRecipes() { | ||||
|     const { data } = await api.recipes.getAll(start, end); | ||||
|     if (data) { | ||||
|       recipes.value = data; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function getAllRecipes() { | ||||
|     useAsync(async () => { | ||||
|       await refreshRecipes(); | ||||
|     }, useAsyncKey()); | ||||
|   } | ||||
| 
 | ||||
|   function assignSorted(val: Array<Recipe>) { | ||||
|     recipes.value = val; | ||||
|   } | ||||
| 
 | ||||
|   if (fetchRecipes) { | ||||
|     getAllRecipes(); | ||||
|   } | ||||
| 
 | ||||
|   return { getAllRecipes, assignSorted }; | ||||
| }; | ||||
							
								
								
									
										60
									
								
								frontend/composables/use-tags-categories.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								frontend/composables/use-tags-categories.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,60 @@ | ||||
| import { Ref, ref, useAsync } from "@nuxtjs/composition-api"; | ||||
| import { useApiSingleton } from "./use-api"; | ||||
| import { useAsyncKey } from "./use-utils"; | ||||
| import { CategoriesAPI, Category } from "~/api/class-interfaces/categories"; | ||||
| import { Tag, TagsAPI } from "~/api/class-interfaces/tags"; | ||||
| 
 | ||||
| export const allCategories = ref<Category[] | null>([]); | ||||
| export const allTags = ref<Tag[] | null>([]); | ||||
| 
 | ||||
| function baseTagsCategories(reference: Ref<Category[] | null> | Ref<Tag[] | null>, api: TagsAPI | CategoriesAPI) { | ||||
|   function useAsyncGetAll() { | ||||
|     useAsync(async () => { | ||||
|       await refreshItems(); | ||||
|     }, useAsyncKey()); | ||||
|   } | ||||
| 
 | ||||
|   async function refreshItems() { | ||||
|     const { data } = await api.getAll(); | ||||
|     reference.value = data; | ||||
|   } | ||||
| 
 | ||||
|   async function createOne(payload: { name: string }) { | ||||
|     const { data } = await api.createOne(payload); | ||||
|     if (data) { | ||||
|       refreshItems(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async function deleteOne(slug: string) { | ||||
|     const { data } = await api.deleteOne(slug); | ||||
|     if (data) { | ||||
|       refreshItems(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async function updateOne(slug: string, payload: { name: string }) { | ||||
|     // @ts-ignore // TODO: Fix Typescript Issue - Unsure how to fix this while also keeping mixins
 | ||||
|     const { data } = await api.updateOne(slug, payload); | ||||
|     if (data) { | ||||
|       refreshItems(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   return { useAsyncGetAll, refreshItems, createOne, deleteOne, updateOne }; | ||||
| } | ||||
| 
 | ||||
| export const useTags = function () { | ||||
|   const api = useApiSingleton(); | ||||
|   return { | ||||
|     allTags, | ||||
|     ...baseTagsCategories(allTags, api.tags), | ||||
|   }; | ||||
| }; | ||||
| export const useCategories = function () { | ||||
|   const api = useApiSingleton(); | ||||
|   return { | ||||
|     allCategories, | ||||
|     ...baseTagsCategories(allCategories, api.categories), | ||||
|   }; | ||||
| }; | ||||
| @ -53,12 +53,12 @@ export default defineComponent({ | ||||
|         }, | ||||
|         { | ||||
|           icon: this.$globals.icons.tags, | ||||
|           to: "/recipes/category", | ||||
|           to: "/recipes/categories", | ||||
|           title: this.$t("sidebar.categories"), | ||||
|         }, | ||||
|         { | ||||
|           icon: this.$globals.icons.tags, | ||||
|           to: "/recipes/tag", | ||||
|           to: "/recipes/tags", | ||||
|           title: this.$t("sidebar.tags"), | ||||
|         }, | ||||
|       ], | ||||
|  | ||||
| @ -23,6 +23,7 @@ | ||||
|     "@vue/composition-api": "^1.0.5", | ||||
|     "@vueuse/core": "^5.2.0", | ||||
|     "core-js": "^3.15.1", | ||||
|     "fuse.js": "^6.4.6", | ||||
|     "nuxt": "^2.15.7", | ||||
|     "nuxt-i18n": "^6.28.0", | ||||
|     "vuedraggable": "^2.24.3", | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| // TODO: Possibly add confirmation dialog? I'm not sure that it's really requried for events... | ||||
| 
 | ||||
| <template> | ||||
|   <v-container class="mt-10"> | ||||
|   <v-container v-if="statistics" class="mt-10"> | ||||
|     <v-row v-if="statistics"> | ||||
|       <v-col cols="12" sm="12" md="4"> | ||||
|         <BaseStatCard :icon="$globals.icons.primary"> | ||||
| @ -121,7 +121,7 @@ export default defineComponent({ | ||||
|       const events = useAsync(async () => { | ||||
|         const { data } = await api.events.getEvents(); | ||||
|         return data; | ||||
|       }); | ||||
|       }, useAsyncKey()); | ||||
|       return events; | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -1,35 +1,25 @@ | ||||
| <template> | ||||
|   <v-container> | ||||
|     <RecipeCardSection | ||||
|       v-if="recentRecipes" | ||||
|       :icon="$globals.icons.primary" | ||||
|       :title="$t('general.recent')" | ||||
|       :recipes="recipes" | ||||
|       :recipes="recentRecipes" | ||||
|     ></RecipeCardSection> | ||||
|   </v-container> | ||||
| </template> | ||||
|    | ||||
|   <script lang="ts"> | ||||
| import { defineComponent, useAsync } from "@nuxtjs/composition-api"; | ||||
| import { defineComponent } from "@nuxtjs/composition-api"; | ||||
| import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue"; | ||||
| import { useApiSingleton } from "~/composables/use-api"; | ||||
| import { useRecipes, recentRecipes } from "~/composables/use-recipes"; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
|   components: { RecipeCardSection }, | ||||
|   setup() { | ||||
|     const api = useApiSingleton(); | ||||
|     const { assignSorted } = useRecipes(false); | ||||
| 
 | ||||
|     const recipes = useAsync(async () => { | ||||
|       const { data } = await api.recipes.getAll(); | ||||
|       return data; | ||||
|     }); | ||||
| 
 | ||||
|     // const recipes = ref<Recipe[] | null>([]); | ||||
|     // onMounted(async () => { | ||||
|     //   const { data } = await api.recipes.getAll(); | ||||
|     //   recipes.value = data; | ||||
|     // }); | ||||
| 
 | ||||
|     return { api, recipes }; | ||||
|     return { recentRecipes, assignSorted }; | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| @ -1,37 +1,26 @@ | ||||
| <template> | ||||
|   <v-container> | ||||
|     <RecipeCardSection | ||||
|       v-if="allRecipes" | ||||
|       :icon="$globals.icons.primary" | ||||
|       :title="$t('page.all-recipes')" | ||||
|       :recipes="recipes" | ||||
|       :recipes="allRecipes" | ||||
|       @sort="assignSorted" | ||||
|     ></RecipeCardSection> | ||||
|   </v-container> | ||||
| </template> | ||||
|    | ||||
|   <script lang="ts"> | ||||
| import { defineComponent, onMounted, ref } from "@nuxtjs/composition-api"; | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from "@nuxtjs/composition-api"; | ||||
| import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue"; | ||||
| import { useApiSingleton } from "~/composables/use-api"; | ||||
| import { Recipe } from "~/types/api-types/admin"; | ||||
| import { useRecipes, allRecipes } from "~/composables/use-recipes"; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
|   components: { RecipeCardSection }, | ||||
|   setup() { | ||||
|     const api = useApiSingleton(); | ||||
|     const { assignSorted } = useRecipes(true); | ||||
| 
 | ||||
|     const recipes = ref<Recipe[] | null>([]); | ||||
|     onMounted(async () => { | ||||
|       const { data } = await api.recipes.getAll(); | ||||
|       recipes.value = data; | ||||
|     }); | ||||
| 
 | ||||
|     return { api, recipes }; | ||||
|   }, | ||||
|   methods: { | ||||
|     assignSorted(val: Array<Recipe>) { | ||||
|       this.recipes = val; | ||||
|     }, | ||||
|     return { allRecipes, assignSorted }; | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|  | ||||
							
								
								
									
										41
									
								
								frontend/pages/recipes/categories/_slug.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								frontend/pages/recipes/categories/_slug.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | ||||
| <template> | ||||
|   <v-container> | ||||
|     <RecipeCardSection | ||||
|       v-if="category" | ||||
|       :icon="$globals.icons.tags" | ||||
|       :title="category.name" | ||||
|       :recipes="category.recipes" | ||||
|       @sort="assignSorted" | ||||
|     ></RecipeCardSection> | ||||
|   </v-container> | ||||
| </template> | ||||
|      | ||||
| <script lang="ts"> | ||||
| import { defineComponent, useAsync, useRoute } from "@nuxtjs/composition-api"; | ||||
| import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue"; | ||||
| import { useApiSingleton } from "~/composables/use-api"; | ||||
| import { Recipe } from "~/types/api-types/recipe"; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
|   components: { RecipeCardSection }, | ||||
|   setup() { | ||||
|     const api = useApiSingleton(); | ||||
|     const route = useRoute(); | ||||
|     const slug = route.value.params.slug; | ||||
| 
 | ||||
|     const category = useAsync(async () => { | ||||
|       const { data } = await api.categories.getOne(slug); | ||||
|       return data; | ||||
|     }, slug); | ||||
|     return { category }; | ||||
|   }, | ||||
|   methods: { | ||||
|     assignSorted(val: Array<Recipe>) { | ||||
|       this.category.recipes = val; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|      | ||||
| <style scoped> | ||||
| </style> | ||||
							
								
								
									
										47
									
								
								frontend/pages/recipes/categories/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								frontend/pages/recipes/categories/index.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | ||||
| <template> | ||||
|   <v-container v-if="categories"> | ||||
|     <v-app-bar color="transparent" flat class="mt-n1 rounded"> | ||||
|       <v-icon large left> | ||||
|         {{ $globals.icons.tags }} | ||||
|       </v-icon> | ||||
|       <v-toolbar-title class="headline"> {{ $t("recipe.categories") }} </v-toolbar-title> | ||||
|       <v-spacer></v-spacer> | ||||
|     </v-app-bar> | ||||
|     <v-slide-x-transition hide-on-leave> | ||||
|       <v-row> | ||||
|         <v-col v-for="item in categories" :key="item.id" cols="12" :sm="12" :md="6" :lg="4" :xl="3"> | ||||
|           <v-card hover :to="`/recipes/categories/${item.slug}`"> | ||||
|             <v-card-actions> | ||||
|               <v-icon> | ||||
|                 {{ $globals.icons.tags }} | ||||
|               </v-icon> | ||||
|               <v-card-title class="py-1">{{ item.name }}</v-card-title> | ||||
|               <v-spacer></v-spacer> | ||||
|             </v-card-actions> | ||||
|           </v-card> | ||||
|         </v-col> | ||||
|       </v-row> | ||||
|     </v-slide-x-transition> | ||||
|   </v-container> | ||||
| </template> | ||||
|    | ||||
| <script lang="ts"> | ||||
| import { defineComponent, useAsync } from "@nuxtjs/composition-api"; | ||||
| import { useApiSingleton } from "~/composables/use-api"; | ||||
| import { useAsyncKey } from "~/composables/use-utils"; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
|   setup() { | ||||
|     const api = useApiSingleton(); | ||||
| 
 | ||||
|     const categories = useAsync(async () => { | ||||
|       const { data } = await api.categories.getAll(); | ||||
|       return data; | ||||
|     }, useAsyncKey()); | ||||
|     return { categories, api }; | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|    | ||||
| <style scoped> | ||||
| </style> | ||||
| @ -1,16 +0,0 @@ | ||||
| <template> | ||||
|   <div></div> | ||||
| </template> | ||||
|    | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from "@nuxtjs/composition-api"; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
|   setup() { | ||||
|     return {}; | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|    | ||||
| <style scoped> | ||||
| </style> | ||||
| @ -1,16 +0,0 @@ | ||||
| <template> | ||||
|   <div></div> | ||||
| </template> | ||||
|    | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from "@nuxtjs/composition-api"; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
|   setup() { | ||||
|     return {}; | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|    | ||||
| <style scoped> | ||||
| </style> | ||||
							
								
								
									
										41
									
								
								frontend/pages/recipes/tags/_slug.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								frontend/pages/recipes/tags/_slug.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | ||||
| <template> | ||||
|   <v-container> | ||||
|     <RecipeCardSection | ||||
|       v-if="tag" | ||||
|       :icon="$globals.icons.tags" | ||||
|       :title="tag.name" | ||||
|       :recipes="tag.recipes" | ||||
|       @sort="assignSorted" | ||||
|     ></RecipeCardSection> | ||||
|   </v-container> | ||||
| </template> | ||||
|      | ||||
| <script lang="ts"> | ||||
| import { defineComponent, useAsync, useRoute } from "@nuxtjs/composition-api"; | ||||
| import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue"; | ||||
| import { useApiSingleton } from "~/composables/use-api"; | ||||
| import { Recipe } from "~/types/api-types/admin"; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
|   components: { RecipeCardSection }, | ||||
|   setup() { | ||||
|     const api = useApiSingleton(); | ||||
|     const route = useRoute(); | ||||
|     const slug = route.value.params.slug; | ||||
| 
 | ||||
|     const tag = useAsync(async () => { | ||||
|       const { data } = await api.tags.getOne(slug); | ||||
|       return data; | ||||
|     }, slug); | ||||
|     return { tag }; | ||||
|   }, | ||||
|   methods: { | ||||
|     assignSorted(val: Array<Recipe>) { | ||||
|       this.tag.recipes = val; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|      | ||||
| <style scoped> | ||||
| </style> | ||||
							
								
								
									
										47
									
								
								frontend/pages/recipes/tags/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								frontend/pages/recipes/tags/index.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | ||||
| <template> | ||||
|   <v-container v-if="tags"> | ||||
|     <v-app-bar color="transparent" flat class="mt-n1 rounded"> | ||||
|       <v-icon large left> | ||||
|         {{ $globals.icons.tags }} | ||||
|       </v-icon> | ||||
|       <v-toolbar-title class="headline"> {{ $t("tag.tags") }} </v-toolbar-title> | ||||
|       <v-spacer></v-spacer> | ||||
|     </v-app-bar> | ||||
|     <v-slide-x-transition hide-on-leave> | ||||
|       <v-row> | ||||
|         <v-col v-for="item in tags" :key="item.id" cols="12" :sm="12" :md="6" :lg="4" :xl="3"> | ||||
|           <v-card hover :to="`/recipes/tags/${item.slug}`"> | ||||
|             <v-card-actions> | ||||
|               <v-icon> | ||||
|                 {{ $globals.icons.tags }} | ||||
|               </v-icon> | ||||
|               <v-card-title class="py-1">{{ item.name }}</v-card-title> | ||||
|               <v-spacer></v-spacer> | ||||
|             </v-card-actions> | ||||
|           </v-card> | ||||
|         </v-col> | ||||
|       </v-row> | ||||
|     </v-slide-x-transition> | ||||
|   </v-container> | ||||
| </template> | ||||
|    | ||||
| <script lang="ts"> | ||||
| import { defineComponent, useAsync } from "@nuxtjs/composition-api"; | ||||
| import { useApiSingleton } from "~/composables/use-api"; | ||||
| import { useAsyncKey } from "~/composables/use-utils"; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
|   setup() { | ||||
|     const api = useApiSingleton(); | ||||
| 
 | ||||
|     const tags = useAsync(async () => { | ||||
|       const { data } = await api.tags.getAll(); | ||||
|       return data; | ||||
|     }, useAsyncKey()); | ||||
|     return { tags, api }; | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|    | ||||
| <style scoped> | ||||
| </style> | ||||
| @ -1,16 +1,169 @@ | ||||
| <template> | ||||
|   <div></div> | ||||
|   <v-container> | ||||
|     <v-row dense> | ||||
|       <v-col> | ||||
|         <v-text-field | ||||
|           v-model="searchString" | ||||
|           outlined | ||||
|           color="primary accent-3" | ||||
|           :placeholder="$t('search.search-placeholder')" | ||||
|           :append-icon="$globals.icons.search" | ||||
|         > | ||||
|         </v-text-field> | ||||
|       </v-col> | ||||
|       <v-col cols="12" md="2" sm="12"> | ||||
|         <v-text-field v-model="maxResults" class="mt-0 pt-0" :label="$t('search.max-results')" type="number" outlined /> | ||||
|       </v-col> | ||||
|     </v-row> | ||||
| 
 | ||||
|     <v-row dense class="my-0 flex-row align-center justify-space-around"> | ||||
|       <v-col> | ||||
|         <h3 class="pl-2 text-center headline"> | ||||
|           {{ $t("category.category-filter") }} | ||||
|         </h3> | ||||
|         <RecipeSearchFilterSelector class="mb-1" @update="updateCatParams" /> | ||||
|         <RecipeCategoryTagSelector v-model="includeCategories" :solo="true" :dense="false" :return-object="false" /> | ||||
|       </v-col> | ||||
|       <v-col> | ||||
|         <h3 class="pl-2 text-center headline"> | ||||
|           {{ $t("search.tag-filter") }} | ||||
|         </h3> | ||||
|         <RecipeSearchFilterSelector class="mb-1" @update="updateTagParams" /> | ||||
|         <RecipeCategoryTagSelector | ||||
|           v-model="includeTags" | ||||
|           :solo="true" | ||||
|           :dense="false" | ||||
|           :return-object="false" | ||||
|           :tag-selector="true" | ||||
|         /> | ||||
|       </v-col> | ||||
|     </v-row> | ||||
| 
 | ||||
|     <RecipeCardSection | ||||
|       class="mt-n9" | ||||
|       :title-icon="$globals.icons.magnify" | ||||
|       :recipes="showRecipes" | ||||
|       :hard-limit="maxResults" | ||||
|       @sort="assignFuzzy" | ||||
|     /> | ||||
|   </v-container> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from "@nuxtjs/composition-api"; | ||||
| <script> | ||||
| import Fuse from "fuse.js"; | ||||
| import { defineComponent } from "vue-demi"; | ||||
| import RecipeSearchFilterSelector from "~/components/Domain/Recipe/RecipeSearchFilterSelector.vue"; | ||||
| import RecipeCategoryTagSelector from "~/components/Domain/Recipe/RecipeCategoryTagSelector.vue"; | ||||
| import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue"; | ||||
| import { useRecipes, allRecipes } from "~/composables/use-recipes"; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
|   components: { | ||||
|     RecipeCategoryTagSelector, | ||||
|     RecipeSearchFilterSelector, | ||||
|     RecipeCardSection, | ||||
|   }, | ||||
|   setup() { | ||||
|     return {}; | ||||
|     const { assignSorted } = useRecipes(true); | ||||
| 
 | ||||
|     return { assignSorted, allRecipes }; | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       maxResults: 21, | ||||
|       searchResults: [], | ||||
|       catFilter: { | ||||
|         exclude: false, | ||||
|         matchAny: false, | ||||
|       }, | ||||
|       tagFilter: { | ||||
|         exclude: false, | ||||
|         matchAny: false, | ||||
|       }, | ||||
|       sortedResults: [], | ||||
|       includeCategories: [], | ||||
|       includeTags: [], | ||||
|       options: { | ||||
|         shouldSort: true, | ||||
|         threshold: 0.6, | ||||
|         location: 0, | ||||
|         distance: 100, | ||||
|         findAllMatches: true, | ||||
|         maxPatternLength: 32, | ||||
|         minMatchCharLength: 2, | ||||
|         keys: ["name", "description"], | ||||
|       }, | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     searchString: { | ||||
|       set(q) { | ||||
|         this.$router.replace({ query: { ...this.$route.query, q } }); | ||||
|       }, | ||||
|       get() { | ||||
|         return this.$route.query.q || ""; | ||||
|       }, | ||||
|     }, | ||||
|     filteredRecipes() { | ||||
|       return this.allRecipes.filter((recipe) => { | ||||
|         const includesTags = this.check(this.includeTags, recipe.tags, this.tagFilter.matchAny, this.tagFilter.exclude); | ||||
|         const includesCats = this.check( | ||||
|           this.includeCategories, | ||||
|           recipe.recipeCategory, | ||||
|           this.catFilter.matchAny, | ||||
|           this.catFilter.exclude | ||||
|         ); | ||||
|         return [includesTags, includesCats].every((x) => x === true); | ||||
|       }); | ||||
|     }, | ||||
|     fuse() { | ||||
|       return new Fuse(this.filteredRecipes, this.options); | ||||
|     }, | ||||
|     fuzzyRecipes() { | ||||
|       if (this.searchString.trim() === "") { | ||||
|         return this.filteredRecipes; | ||||
|       } | ||||
|       const result = this.fuse.search(this.searchString.trim()); | ||||
|       return result.map((x) => x.item); | ||||
|     }, | ||||
|     isSearching() { | ||||
|       return this.searchString && this.searchString.length > 0; | ||||
|     }, | ||||
|     showRecipes() { | ||||
|       if (this.sortedResults.length > 0) { | ||||
|         return this.sortedResults; | ||||
|       } else { | ||||
|         return this.fuzzyRecipes; | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     assignFuzzy(val) { | ||||
|       this.sortedResults = val; | ||||
|     }, | ||||
|     check(filterBy, recipeList, matchAny, exclude) { | ||||
|       let isMatch = true; | ||||
|       if (filterBy.length === 0) return isMatch; | ||||
| 
 | ||||
|       if (recipeList) { | ||||
|         if (matchAny) { | ||||
|           isMatch = filterBy.some((t) => recipeList.includes(t)); // Checks if some items are a match | ||||
|         } else { | ||||
|           isMatch = filterBy.every((t) => recipeList.includes(t)); // Checks if every items is a match | ||||
|         } | ||||
|         return exclude ? !isMatch : isMatch; | ||||
|       } else; | ||||
|       return false; | ||||
|     }, | ||||
| 
 | ||||
|     updateTagParams(params) { | ||||
|       this.tagFilter = params; | ||||
|     }, | ||||
|     updateCatParams(params) { | ||||
|       this.catFilter = params; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
| </style> | ||||
| <style></style> | ||||
|  | ||||
| @ -5273,6 +5273,11 @@ functional-red-black-tree@^1.0.1: | ||||
|   resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" | ||||
|   integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= | ||||
| 
 | ||||
| fuse.js@^6.4.6: | ||||
|   version "6.4.6" | ||||
|   resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-6.4.6.tgz#62f216c110e5aa22486aff20be7896d19a059b79" | ||||
|   integrity sha512-/gYxR/0VpXmWSfZOIPS3rWwU8SHgsRTwWuXhyb2O6s7aRuVtHtxCkR33bNYu3wyLyNx/Wpv0vU7FZy8Vj53VNw== | ||||
| 
 | ||||
| gensync@^1.0.0-beta.2: | ||||
|   version "1.0.0-beta.2" | ||||
|   resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user