mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 10:13:32 -04:00 
			
		
		
		
	feat(frontend): ✨ Create CRUD User Interface for Units and Foods
This commit is contained in:
		| @@ -10,39 +10,16 @@ export interface CrudAPIInterface { | |||||||
|   // Methods |   // Methods | ||||||
| } | } | ||||||
|  |  | ||||||
| export const crudMixins = <T>( | export interface CrudAPIMethodsInterface { | ||||||
|   requests: ApiRequestInstance, |   // CRUD Methods | ||||||
|   baseRoute: string, |   getAll(): any | ||||||
|   itemRoute: (itemId: string) => string |   createOne(): any | ||||||
| ) => { |   getOne(): any | ||||||
|   async function getAll(start = 0, limit = 9999) { |   updateOne(): any | ||||||
|     return await requests.get<T[]>(baseRoute, { |   patchOne(): any | ||||||
|       params: { start, limit }, |   deleteOne(): any | ||||||
|     }); | } | ||||||
|   } |  | ||||||
|  |  | ||||||
|   async function createOne(payload: T) { |  | ||||||
|     return await requests.post<T>(baseRoute, payload); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   async function getOne(itemId: string) { |  | ||||||
|     return await requests.get<T>(itemRoute(itemId)); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   async function updateOne(itemId: string, payload: T) { |  | ||||||
|     return await requests.put<T>(itemRoute(itemId), payload); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   async function patchOne(itemId: string, payload: T) { |  | ||||||
|     return await requests.patch(itemRoute(itemId), payload); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   async function deleteOne(itemId: string) { |  | ||||||
|     return await requests.delete<T>(itemRoute(itemId)); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return { getAll, getOne, updateOne, patchOne, deleteOne, createOne }; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export abstract class BaseAPI { | export abstract class BaseAPI { | ||||||
|   requests: ApiRequestInstance; |   requests: ApiRequestInstance; | ||||||
| @@ -66,11 +43,11 @@ export abstract class BaseCRUDAPI<T, U> extends BaseAPI implements CrudAPIInterf | |||||||
|     return await this.requests.post<T>(this.baseRoute, payload); |     return await this.requests.post<T>(this.baseRoute, payload); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async getOne(itemId: string) { |   async getOne(itemId: string | number) { | ||||||
|     return await this.requests.get<T>(this.itemRoute(itemId)); |     return await this.requests.get<T>(this.itemRoute(itemId)); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async updateOne(itemId: string, payload: T) { |   async updateOne(itemId: string | number, payload: T) { | ||||||
|     return await this.requests.put<T>(this.itemRoute(itemId), payload); |     return await this.requests.put<T>(this.itemRoute(itemId), payload); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,3 @@ | |||||||
| import { requests } from "../requests"; |  | ||||||
| import { BaseCRUDAPI } from "./_base"; | import { BaseCRUDAPI } from "./_base"; | ||||||
|  |  | ||||||
| export type EventCategory = "general" | "recipe" | "backup" | "scheduled" | "migration" | "group" | "user"; | export type EventCategory = "general" | "recipe" | "backup" | "scheduled" | "migration" | "group" | "user"; | ||||||
| @@ -36,7 +35,7 @@ export class NotificationsAPI extends BaseCRUDAPI<EventNotification, CreateEvent | |||||||
|   itemRoute = routes.aboutEventsNotificationsId; |   itemRoute = routes.aboutEventsNotificationsId; | ||||||
|   /** Returns the Group Data for the Current User |   /** Returns the Group Data for the Current User | ||||||
|    */ |    */ | ||||||
|   async testNotification(id: number) { |   async testNotification(id: number | null = null, testUrl: string | null = null) { | ||||||
|     return await requests.post(routes.aboutEventsNotificationsTest, { id }); |     return await this.requests.post(routes.aboutEventsNotificationsTest, { id, testUrl }); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										22
									
								
								frontend/api/class-interfaces/recipe-foods.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								frontend/api/class-interfaces/recipe-foods.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | import { BaseCRUDAPI } from "./_base"; | ||||||
|  |  | ||||||
|  | const prefix = "/api"; | ||||||
|  |  | ||||||
|  | export interface CreateFood { | ||||||
|  |   name: string; | ||||||
|  |   description: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface Food extends CreateFood { | ||||||
|  |   id: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const routes = { | ||||||
|  |   food: `${prefix}/foods`, | ||||||
|  |   foodsFood: (tag: string) => `${prefix}/foods/${tag}`, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export class FoodAPI extends BaseCRUDAPI<Food, CreateFood> { | ||||||
|  |   baseRoute: string = routes.food; | ||||||
|  |   itemRoute = routes.foodsFood; | ||||||
|  | } | ||||||
							
								
								
									
										23
									
								
								frontend/api/class-interfaces/recipe-units.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								frontend/api/class-interfaces/recipe-units.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | import { BaseCRUDAPI } from "./_base"; | ||||||
|  |  | ||||||
|  | const prefix = "/api"; | ||||||
|  |  | ||||||
|  | export interface CreateUnit { | ||||||
|  |   name: string; | ||||||
|  |   abbreviation: string; | ||||||
|  |   description: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface Unit extends CreateUnit { | ||||||
|  |   id: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const routes = { | ||||||
|  |   unit: `${prefix}/units`, | ||||||
|  |   unitsUnit: (tag: string) => `${prefix}/units/${tag}`, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export class UnitAPI extends BaseCRUDAPI<Unit, CreateUnit> { | ||||||
|  |   baseRoute: string = routes.unit; | ||||||
|  |   itemRoute = routes.unitsUnit; | ||||||
|  | } | ||||||
| @@ -9,6 +9,8 @@ import { CategoriesAPI } from "./class-interfaces/categories"; | |||||||
| import { TagsAPI } from "./class-interfaces/tags"; | import { TagsAPI } from "./class-interfaces/tags"; | ||||||
| import { UtilsAPI } from "./class-interfaces/utils"; | import { UtilsAPI } from "./class-interfaces/utils"; | ||||||
| import { NotificationsAPI } from "./class-interfaces/event-notifications"; | import { NotificationsAPI } from "./class-interfaces/event-notifications"; | ||||||
|  | import { FoodAPI } from "./class-interfaces/recipe-foods"; | ||||||
|  | import { UnitAPI } from "./class-interfaces/recipe-units"; | ||||||
| import { ApiRequestInstance } from "~/types/api"; | import { ApiRequestInstance } from "~/types/api"; | ||||||
|  |  | ||||||
| class Api { | class Api { | ||||||
| @@ -23,6 +25,8 @@ class Api { | |||||||
|   public tags: TagsAPI; |   public tags: TagsAPI; | ||||||
|   public utils: UtilsAPI; |   public utils: UtilsAPI; | ||||||
|   public notifications: NotificationsAPI; |   public notifications: NotificationsAPI; | ||||||
|  |   public foods: FoodAPI; | ||||||
|  |   public units: UnitAPI; | ||||||
|  |  | ||||||
|   // Utils |   // Utils | ||||||
|   public upload: UploadFile; |   public upload: UploadFile; | ||||||
| @@ -36,6 +40,8 @@ class Api { | |||||||
|     this.recipes = new RecipeAPI(requests); |     this.recipes = new RecipeAPI(requests); | ||||||
|     this.categories = new CategoriesAPI(requests); |     this.categories = new CategoriesAPI(requests); | ||||||
|     this.tags = new TagsAPI(requests); |     this.tags = new TagsAPI(requests); | ||||||
|  |     this.units = new UnitAPI(requests); | ||||||
|  |     this.foods = new FoodAPI(requests); | ||||||
|  |  | ||||||
|     // Users |     // Users | ||||||
|     this.users = new UserApi(requests); |     this.users = new UserApi(requests); | ||||||
|   | |||||||
| @@ -62,12 +62,14 @@ export const useNotifications = function () { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async function testById() { |   async function testById(id: number) { | ||||||
|       // TODO: Test by ID |     const {data} = await api.notifications.testNotification(id, null) | ||||||
|  |     console.log(data) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async function testByUrl() { |   async function testByUrl(testUrl: string) { | ||||||
|       // TODO: Test by URL |     const {data} = await api.notifications.testNotification(null, testUrl) | ||||||
|  |     console.log(data) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const notifications = getNotifications(); |   const notifications = getNotifications(); | ||||||
|   | |||||||
							
								
								
									
										91
									
								
								frontend/composables/use-recipe-foods.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								frontend/composables/use-recipe-foods.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,91 @@ | |||||||
|  | import { useAsync, ref, reactive } from "@nuxtjs/composition-api"; | ||||||
|  | import { useAsyncKey } from "./use-utils"; | ||||||
|  | import { useApiSingleton } from "~/composables/use-api"; | ||||||
|  | import { Food } from "~/api/class-interfaces/recipe-foods"; | ||||||
|  |  | ||||||
|  | export const useFoods = function () { | ||||||
|  |   const api = useApiSingleton(); | ||||||
|  |   const loading = ref(false); | ||||||
|  |   const deleteTargetId = ref(0); | ||||||
|  |   const validForm = ref(true); | ||||||
|  |  | ||||||
|  |   const workingFoodData = reactive({ | ||||||
|  |     id: 0, | ||||||
|  |     name: "", | ||||||
|  |     description: "", | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const actions = { | ||||||
|  |     getAll() { | ||||||
|  |       loading.value = true; | ||||||
|  |       const units = useAsync(async () => { | ||||||
|  |         const { data } = await api.foods.getAll(); | ||||||
|  |         return data; | ||||||
|  |       }, useAsyncKey()); | ||||||
|  |  | ||||||
|  |       loading.value = false | ||||||
|  |       return units; | ||||||
|  |     }, | ||||||
|  |     async refreshAll() { | ||||||
|  |       loading.value = true; | ||||||
|  |       const { data } = await api.foods.getAll(); | ||||||
|  |  | ||||||
|  |       if (data) { | ||||||
|  |         foods.value = data; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       loading.value = false; | ||||||
|  |     }, | ||||||
|  |     async createOne(domForm: VForm | null = null) { | ||||||
|  |       if (domForm && !domForm.validate()) { | ||||||
|  |         validForm.value = false; | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       loading.value = true; | ||||||
|  |       const { data } = await api.foods.createOne(workingFoodData); | ||||||
|  |       if (data && foods.value) { | ||||||
|  |         foods.value.push(data); | ||||||
|  |       } else { | ||||||
|  |         this.refreshAll(); | ||||||
|  |       } | ||||||
|  |       domForm?.reset(); | ||||||
|  |       validForm.value = true; | ||||||
|  |       this.resetWorking(); | ||||||
|  |       loading.value = false; | ||||||
|  |     }, | ||||||
|  |     async updateOne() { | ||||||
|  |       if (!workingFoodData.id) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       loading.value = true; | ||||||
|  |       const { data } = await api.foods.updateOne(workingFoodData.id, workingFoodData); | ||||||
|  |       if (data && foods.value) { | ||||||
|  |         this.refreshAll(); | ||||||
|  |       } | ||||||
|  |       loading.value = false; | ||||||
|  |     }, | ||||||
|  |     async deleteOne(id: string | number) { | ||||||
|  |       loading.value = true; | ||||||
|  |       const { data } = await api.foods.deleteOne(id); | ||||||
|  |       if (data && foods.value) { | ||||||
|  |         this.refreshAll(); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     resetWorking() { | ||||||
|  |       workingFoodData.id = 0; | ||||||
|  |       workingFoodData.name = ""; | ||||||
|  |       workingFoodData.description = ""; | ||||||
|  |     }, | ||||||
|  |     setWorking(item: Food) { | ||||||
|  |       workingFoodData.id = item.id; | ||||||
|  |       workingFoodData.name = item.name; | ||||||
|  |       workingFoodData.description = item.description; | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const foods = actions.getAll(); | ||||||
|  |  | ||||||
|  |   return { foods, workingFoodData, deleteTargetId, actions, validForm }; | ||||||
|  | }; | ||||||
							
								
								
									
										94
									
								
								frontend/composables/use-recipe-units.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								frontend/composables/use-recipe-units.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | |||||||
|  | import { useAsync, ref, reactive } from "@nuxtjs/composition-api"; | ||||||
|  | import { useAsyncKey } from "./use-utils"; | ||||||
|  | import { useApiSingleton } from "~/composables/use-api"; | ||||||
|  | import { Unit } from "~/api/class-interfaces/recipe-units"; | ||||||
|  |  | ||||||
|  | export const useUnits = function () { | ||||||
|  |   const api = useApiSingleton(); | ||||||
|  |   const loading = ref(false); | ||||||
|  |   const deleteTargetId = ref(0); | ||||||
|  |   const validForm = ref(true); | ||||||
|  |  | ||||||
|  |   const workingUnitData = reactive({ | ||||||
|  |     id: 0, | ||||||
|  |     name: "", | ||||||
|  |     abbreviation: "", | ||||||
|  |     description: "", | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const actions = { | ||||||
|  |     getAll() { | ||||||
|  |       loading.value = true; | ||||||
|  |       const units = useAsync(async () => { | ||||||
|  |         const { data } = await api.units.getAll(); | ||||||
|  |         return data; | ||||||
|  |       }, useAsyncKey()); | ||||||
|  |  | ||||||
|  |       loading.value = false | ||||||
|  |       return units; | ||||||
|  |     }, | ||||||
|  |     async refreshAll() { | ||||||
|  |       loading.value = true; | ||||||
|  |       const { data } = await api.units.getAll(); | ||||||
|  |  | ||||||
|  |       if (data) { | ||||||
|  |         units.value = data; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       loading.value = false; | ||||||
|  |     }, | ||||||
|  |     async createOne(domForm: VForm | null = null) { | ||||||
|  |       if (domForm && !domForm.validate()) { | ||||||
|  |         validForm.value = false; | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       loading.value = true; | ||||||
|  |       const { data } = await api.units.createOne(workingUnitData); | ||||||
|  |       if (data && units.value) { | ||||||
|  |         units.value.push(data); | ||||||
|  |       } else { | ||||||
|  |         this.refreshAll(); | ||||||
|  |       } | ||||||
|  |       domForm?.reset(); | ||||||
|  |       validForm.value = true; | ||||||
|  |       this.resetWorking(); | ||||||
|  |       loading.value = false; | ||||||
|  |     }, | ||||||
|  |     async updateOne() { | ||||||
|  |       if (!workingUnitData.id) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       loading.value = true; | ||||||
|  |       const { data } = await api.units.updateOne(workingUnitData.id, workingUnitData); | ||||||
|  |       if (data && units.value) { | ||||||
|  |         this.refreshAll(); | ||||||
|  |       } | ||||||
|  |       loading.value = false; | ||||||
|  |     }, | ||||||
|  |     async deleteOne(id: string | number) { | ||||||
|  |       loading.value = true; | ||||||
|  |       const { data } = await api.units.deleteOne(id); | ||||||
|  |       if (data && units.value) { | ||||||
|  |         this.refreshAll(); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     resetWorking() { | ||||||
|  |       workingUnitData.id = 0; | ||||||
|  |       workingUnitData.name = ""; | ||||||
|  |       workingUnitData.abbreviation = ""; | ||||||
|  |       workingUnitData.description = ""; | ||||||
|  |     }, | ||||||
|  |     setWorking(item: Unit) { | ||||||
|  |       workingUnitData.id = item.id; | ||||||
|  |       workingUnitData.name = item.name; | ||||||
|  |       workingUnitData.abbreviation = item.abbreviation; | ||||||
|  |       workingUnitData.description = item.description; | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const units = actions.getAll(); | ||||||
|  |  | ||||||
|  |   return { units, workingUnitData, deleteTargetId, actions, validForm }; | ||||||
|  | }; | ||||||
| @@ -83,6 +83,16 @@ export default defineComponent({ | |||||||
|               to: "/admin/toolbox/notifications", |               to: "/admin/toolbox/notifications", | ||||||
|               title: this.$t("events.notification"), |               title: this.$t("events.notification"), | ||||||
|             }, |             }, | ||||||
|  |             { | ||||||
|  |               icon: this.$globals.icons.foods, | ||||||
|  |               to: "/admin/toolbox/foods", | ||||||
|  |               title: "Manage Foods", | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |               icon: this.$globals.icons.units, | ||||||
|  |               to: "/admin/toolbox/units", | ||||||
|  |               title: "Manage Units", | ||||||
|  |             }, | ||||||
|             { |             { | ||||||
|               icon: this.$globals.icons.tags, |               icon: this.$globals.icons.tags, | ||||||
|               to: "/admin/toolbox/categories", |               to: "/admin/toolbox/categories", | ||||||
|   | |||||||
							
								
								
									
										120
									
								
								frontend/pages/admin/toolbox/foods.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								frontend/pages/admin/toolbox/foods.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | |||||||
|  | <template> | ||||||
|  |   <v-container fluid> | ||||||
|  |     <BaseCardSectionTitle title="Manage Units"> </BaseCardSectionTitle> | ||||||
|  |     <v-toolbar flat> | ||||||
|  |       <BaseDialog | ||||||
|  |         ref="domFoodDialog" | ||||||
|  |         :title="dialog.title" | ||||||
|  |         :icon="$globals.icons.units" | ||||||
|  |         :submit-text="dialog.text" | ||||||
|  |         :keep-open="!validForm" | ||||||
|  |         @submit="create ? actions.createOne(domCreateFoodForm) : actions.updateOne()" | ||||||
|  |       > | ||||||
|  |         <v-card-text> | ||||||
|  |           <v-form ref="domCreateFoodForm"> | ||||||
|  |             <v-text-field v-model="workingFoodData.name" label="Name" :rules="[validators.required]"></v-text-field> | ||||||
|  |             <v-text-field v-model="workingFoodData.description" label="Description"></v-text-field> | ||||||
|  |           </v-form> | ||||||
|  |         </v-card-text> | ||||||
|  |       </BaseDialog> | ||||||
|  |  | ||||||
|  |       <BaseButton | ||||||
|  |         class="mr-1" | ||||||
|  |         @click=" | ||||||
|  |           create = true; | ||||||
|  |           actions.resetWorking(); | ||||||
|  |           domFoodDialog.open(); | ||||||
|  |         " | ||||||
|  |       ></BaseButton> | ||||||
|  |       <BaseButton secondary @click="filter = !filter"> Filter </BaseButton> | ||||||
|  |     </v-toolbar> | ||||||
|  |  | ||||||
|  |     <v-expand-transition> | ||||||
|  |       <div v-show="filter"> | ||||||
|  |         <v-text-field v-model="search" style="max-width: 500px" label="Filter" class="ml-4"> </v-text-field> | ||||||
|  |       </div> | ||||||
|  |     </v-expand-transition> | ||||||
|  |  | ||||||
|  |     <v-data-table :headers="headers" :items="foods || []" item-key="id" class="elevation-0" :search="search"> | ||||||
|  |       <template #item.actions="{ item }"> | ||||||
|  |         <div class="d-flex justify-end"> | ||||||
|  |           <BaseButton | ||||||
|  |             edit | ||||||
|  |             small | ||||||
|  |             class="mr-2" | ||||||
|  |             @click=" | ||||||
|  |               create = false; | ||||||
|  |               actions.setWorking(item); | ||||||
|  |               domFoodDialog.open(); | ||||||
|  |             " | ||||||
|  |           ></BaseButton> | ||||||
|  |           <BaseDialog :title="$t('general.confirm')" color="error" @confirm="actions.deleteOne(item.id)"> | ||||||
|  |             <template #activator="{ open }"> | ||||||
|  |               <BaseButton delete small @click="open"></BaseButton> | ||||||
|  |             </template> | ||||||
|  |             <v-card-text> | ||||||
|  |               {{ $t("general.confirm-delete-generic") }} | ||||||
|  |             </v-card-text> | ||||||
|  |           </BaseDialog> | ||||||
|  |         </div> | ||||||
|  |       </template> | ||||||
|  |     </v-data-table> | ||||||
|  |     <v-divider></v-divider> | ||||||
|  |   </v-container> | ||||||
|  | </template> | ||||||
|  |      | ||||||
|  | <script lang="ts"> | ||||||
|  | import { defineComponent, reactive, toRefs, ref, computed } from "@nuxtjs/composition-api"; | ||||||
|  | import { useFoods } from "~/composables/use-recipe-foods"; | ||||||
|  | import { validators } from "~/composables/use-validators"; | ||||||
|  | export default defineComponent({ | ||||||
|  |   layout: "admin", | ||||||
|  |   setup() { | ||||||
|  |     const { foods, actions, workingFoodData, validForm } = useFoods(); | ||||||
|  |  | ||||||
|  |     const domCreateFoodForm = ref(null); | ||||||
|  |     const domFoodDialog = ref(null); | ||||||
|  |  | ||||||
|  |     const dialog = computed(() => { | ||||||
|  |       if (state.create) { | ||||||
|  |         return { | ||||||
|  |           title: "Create Food", | ||||||
|  |           text: "Create", | ||||||
|  |         }; | ||||||
|  |       } else { | ||||||
|  |         return { | ||||||
|  |           title: "Edit Food", | ||||||
|  |           text: "Update", | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const state = reactive({ | ||||||
|  |       headers: [ | ||||||
|  |         { text: "Id", value: "id" }, | ||||||
|  |         { text: "Name", value: "name" }, | ||||||
|  |         { text: "Description", value: "description" }, | ||||||
|  |         { text: "", value: "actions", sortable: false }, | ||||||
|  |       ], | ||||||
|  |       filter: false, | ||||||
|  |       create: true, | ||||||
|  |       search: "", | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       ...toRefs(state), | ||||||
|  |       actions, | ||||||
|  |       dialog, | ||||||
|  |       domCreateFoodForm, | ||||||
|  |       domFoodDialog, | ||||||
|  |       foods, | ||||||
|  |       validators, | ||||||
|  |       validForm, | ||||||
|  |       workingFoodData, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  |      | ||||||
|  | <style scoped> | ||||||
|  | </style> | ||||||
| @@ -47,7 +47,12 @@ | |||||||
|             :label="$t('events.apprise-url')" |             :label="$t('events.apprise-url')" | ||||||
|           ></v-text-field> |           ></v-text-field> | ||||||
|  |  | ||||||
|           <BaseButton class="d-flex ml-auto" small color="info" @click="testByUrl(newNotification.notificationUrl)"> |           <BaseButton | ||||||
|  |             class="d-flex ml-auto" | ||||||
|  |             small | ||||||
|  |             color="info" | ||||||
|  |             @click="testByUrl(createNotificationData.notificationUrl)" | ||||||
|  |           > | ||||||
|             <template #icon> {{ $globals.icons.testTube }}</template> |             <template #icon> {{ $globals.icons.testTube }}</template> | ||||||
|             {{ $t("general.test") }} |             {{ $t("general.test") }} | ||||||
|           </BaseButton> |           </BaseButton> | ||||||
|   | |||||||
							
								
								
									
										122
									
								
								frontend/pages/admin/toolbox/units.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								frontend/pages/admin/toolbox/units.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | |||||||
|  | <template> | ||||||
|  |   <v-container fluid> | ||||||
|  |     <BaseCardSectionTitle title="Manage Units"> </BaseCardSectionTitle> | ||||||
|  |     <v-toolbar flat> | ||||||
|  |       <BaseDialog | ||||||
|  |         ref="domUnitDialog" | ||||||
|  |         :title="dialog.title" | ||||||
|  |         :icon="$globals.icons.units" | ||||||
|  |         :submit-text="dialog.text" | ||||||
|  |         :keep-open="!validForm" | ||||||
|  |         @submit="create ? actions.createOne(domCreateUnitForm) : actions.updateOne()" | ||||||
|  |       > | ||||||
|  |         <v-card-text> | ||||||
|  |           <v-form ref="domCreateUnitForm"> | ||||||
|  |             <v-text-field v-model="workingUnitData.name" label="Name" :rules="[validators.required]"></v-text-field> | ||||||
|  |             <v-text-field v-model="workingUnitData.abbreviation" label="Abbreviation"></v-text-field> | ||||||
|  |             <v-text-field v-model="workingUnitData.description" label="Description"></v-text-field> | ||||||
|  |           </v-form> | ||||||
|  |         </v-card-text> | ||||||
|  |       </BaseDialog> | ||||||
|  |  | ||||||
|  |       <BaseButton | ||||||
|  |         class="mr-1" | ||||||
|  |         @click=" | ||||||
|  |           create = true; | ||||||
|  |           actions.resetWorking(); | ||||||
|  |           domUnitDialog.open(); | ||||||
|  |         " | ||||||
|  |       ></BaseButton> | ||||||
|  |       <BaseButton secondary @click="filter = !filter"> Filter </BaseButton> | ||||||
|  |     </v-toolbar> | ||||||
|  |  | ||||||
|  |     <v-expand-transition> | ||||||
|  |       <div v-show="filter"> | ||||||
|  |         <v-text-field v-model="search" style="max-width: 500px" label="Filter" class="ml-4"> </v-text-field> | ||||||
|  |       </div> | ||||||
|  |     </v-expand-transition> | ||||||
|  |  | ||||||
|  |     <v-data-table :headers="headers" :items="units || []" item-key="id" class="elevation-0" :search="search"> | ||||||
|  |       <template #item.actions="{ item }"> | ||||||
|  |         <div class="d-flex justify-end"> | ||||||
|  |           <BaseButton | ||||||
|  |             edit | ||||||
|  |             small | ||||||
|  |             class="mr-2" | ||||||
|  |             @click=" | ||||||
|  |               create = false; | ||||||
|  |               actions.setWorking(item); | ||||||
|  |               domUnitDialog.open(); | ||||||
|  |             " | ||||||
|  |           ></BaseButton> | ||||||
|  |           <BaseDialog :title="$t('general.confirm')" color="error" @confirm="actions.deleteOne(item.id)"> | ||||||
|  |             <template #activator="{ open }"> | ||||||
|  |               <BaseButton delete small @click="open"></BaseButton> | ||||||
|  |             </template> | ||||||
|  |             <v-card-text> | ||||||
|  |               {{ $t("general.confirm-delete-generic") }} | ||||||
|  |             </v-card-text> | ||||||
|  |           </BaseDialog> | ||||||
|  |         </div> | ||||||
|  |       </template> | ||||||
|  |     </v-data-table> | ||||||
|  |     <v-divider></v-divider> | ||||||
|  |   </v-container> | ||||||
|  | </template> | ||||||
|  |      | ||||||
|  | <script lang="ts"> | ||||||
|  | import { defineComponent, reactive, toRefs, ref, computed } from "@nuxtjs/composition-api"; | ||||||
|  | import { useUnits } from "~/composables/use-recipe-units"; | ||||||
|  | import { validators } from "~/composables/use-validators"; | ||||||
|  | export default defineComponent({ | ||||||
|  |   layout: "admin", | ||||||
|  |   setup() { | ||||||
|  |     const { units, actions, workingUnitData, validForm } = useUnits(); | ||||||
|  |  | ||||||
|  |     const domCreateUnitForm = ref(null); | ||||||
|  |     const domUnitDialog = ref(null); | ||||||
|  |  | ||||||
|  |     const dialog = computed(() => { | ||||||
|  |       if (state.create) { | ||||||
|  |         return { | ||||||
|  |           title: "Create Unit", | ||||||
|  |           text: "Create", | ||||||
|  |         }; | ||||||
|  |       } else { | ||||||
|  |         return { | ||||||
|  |           title: "Edit Unit", | ||||||
|  |           text: "Update", | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const state = reactive({ | ||||||
|  |       headers: [ | ||||||
|  |         { text: "Id", value: "id" }, | ||||||
|  |         { text: "Name", value: "name" }, | ||||||
|  |         { text: "Abbreviation", value: "abbreviation" }, | ||||||
|  |         { text: "Description", value: "description" }, | ||||||
|  |         { text: "", value: "actions", sortable: false }, | ||||||
|  |       ], | ||||||
|  |       filter: false, | ||||||
|  |       create: true, | ||||||
|  |       search: "", | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       ...toRefs(state), | ||||||
|  |       actions, | ||||||
|  |       dialog, | ||||||
|  |       domCreateUnitForm, | ||||||
|  |       domUnitDialog, | ||||||
|  |       units, | ||||||
|  |       validators, | ||||||
|  |       validForm, | ||||||
|  |       workingUnitData, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  |      | ||||||
|  | <style scoped> | ||||||
|  | </style> | ||||||
| @@ -92,6 +92,8 @@ import { | |||||||
|   mdiMinus, |   mdiMinus, | ||||||
|   mdiWindowClose, |   mdiWindowClose, | ||||||
|   mdiFolderZipOutline, |   mdiFolderZipOutline, | ||||||
|  |   mdiFoodApple, | ||||||
|  |   mdiBeakerOutline, | ||||||
| } from "@mdi/js"; | } from "@mdi/js"; | ||||||
|  |  | ||||||
| const icons = { | const icons = { | ||||||
| @@ -99,6 +101,8 @@ const icons = { | |||||||
|   primary: mdiSilverwareVariant, |   primary: mdiSilverwareVariant, | ||||||
|  |  | ||||||
|   // General |   // General | ||||||
|  |   foods: mdiFoodApple, | ||||||
|  |   units: mdiBeakerOutline, | ||||||
|   alert: mdiAlert, |   alert: mdiAlert, | ||||||
|   alertCircle: mdiAlertCircle, |   alertCircle: mdiAlertCircle, | ||||||
|   api: mdiApi, |   api: mdiApi, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user