From a1aad078da901806977a63273f66f62fe99cfbce Mon Sep 17 00:00:00 2001 From: hay-kot Date: Sun, 22 Aug 2021 15:23:45 -0800 Subject: [PATCH] feat(frontend): :sparkles: Create CRUD User Interface for Units and Foods --- frontend/api/class-interfaces/_base.ts | 45 ++----- .../class-interfaces/event-notifications.ts | 5 +- frontend/api/class-interfaces/recipe-foods.ts | 22 ++++ frontend/api/class-interfaces/recipe-units.ts | 23 ++++ frontend/api/index.ts | 6 + frontend/composables/use-notifications.ts | 10 +- frontend/composables/use-recipe-foods.ts | 91 +++++++++++++ frontend/composables/use-recipe-units.ts | 94 ++++++++++++++ frontend/layouts/admin.vue | 10 ++ frontend/pages/admin/toolbox/foods.vue | 120 +++++++++++++++++ .../pages/admin/toolbox/notifications.vue | 7 +- frontend/pages/admin/toolbox/units.vue | 122 ++++++++++++++++++ frontend/plugins/globals.js | 4 + 13 files changed, 517 insertions(+), 42 deletions(-) create mode 100644 frontend/api/class-interfaces/recipe-foods.ts create mode 100644 frontend/api/class-interfaces/recipe-units.ts create mode 100644 frontend/composables/use-recipe-foods.ts create mode 100644 frontend/composables/use-recipe-units.ts create mode 100644 frontend/pages/admin/toolbox/foods.vue create mode 100644 frontend/pages/admin/toolbox/units.vue diff --git a/frontend/api/class-interfaces/_base.ts b/frontend/api/class-interfaces/_base.ts index e1ec27f7a3b7..3e945f67ec97 100644 --- a/frontend/api/class-interfaces/_base.ts +++ b/frontend/api/class-interfaces/_base.ts @@ -10,39 +10,16 @@ export interface CrudAPIInterface { // Methods } -export const crudMixins = ( - requests: ApiRequestInstance, - baseRoute: string, - itemRoute: (itemId: string) => string -) => { - async function getAll(start = 0, limit = 9999) { - return await requests.get(baseRoute, { - params: { start, limit }, - }); - } +export interface CrudAPIMethodsInterface { + // CRUD Methods + getAll(): any + createOne(): any + getOne(): any + updateOne(): any + patchOne(): any + deleteOne(): any +} - async function createOne(payload: T) { - return await requests.post(baseRoute, payload); - } - - async function getOne(itemId: string) { - return await requests.get(itemRoute(itemId)); - } - - async function updateOne(itemId: string, payload: T) { - return await requests.put(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(itemRoute(itemId)); - } - - return { getAll, getOne, updateOne, patchOne, deleteOne, createOne }; -}; export abstract class BaseAPI { requests: ApiRequestInstance; @@ -66,11 +43,11 @@ export abstract class BaseCRUDAPI extends BaseAPI implements CrudAPIInterf return await this.requests.post(this.baseRoute, payload); } - async getOne(itemId: string) { + async getOne(itemId: string | number) { return await this.requests.get(this.itemRoute(itemId)); } - async updateOne(itemId: string, payload: T) { + async updateOne(itemId: string | number, payload: T) { return await this.requests.put(this.itemRoute(itemId), payload); } diff --git a/frontend/api/class-interfaces/event-notifications.ts b/frontend/api/class-interfaces/event-notifications.ts index bbed21db6d05..6e19ab319232 100644 --- a/frontend/api/class-interfaces/event-notifications.ts +++ b/frontend/api/class-interfaces/event-notifications.ts @@ -1,4 +1,3 @@ -import { requests } from "../requests"; import { BaseCRUDAPI } from "./_base"; export type EventCategory = "general" | "recipe" | "backup" | "scheduled" | "migration" | "group" | "user"; @@ -36,7 +35,7 @@ export class NotificationsAPI extends BaseCRUDAPI `${prefix}/foods/${tag}`, +}; + +export class FoodAPI extends BaseCRUDAPI { + baseRoute: string = routes.food; + itemRoute = routes.foodsFood; +} diff --git a/frontend/api/class-interfaces/recipe-units.ts b/frontend/api/class-interfaces/recipe-units.ts new file mode 100644 index 000000000000..16627fa1f6c8 --- /dev/null +++ b/frontend/api/class-interfaces/recipe-units.ts @@ -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 { + baseRoute: string = routes.unit; + itemRoute = routes.unitsUnit; +} diff --git a/frontend/api/index.ts b/frontend/api/index.ts index e22ba4fc5796..eccf97a29e8a 100644 --- a/frontend/api/index.ts +++ b/frontend/api/index.ts @@ -9,6 +9,8 @@ import { CategoriesAPI } from "./class-interfaces/categories"; import { TagsAPI } from "./class-interfaces/tags"; import { UtilsAPI } from "./class-interfaces/utils"; 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"; class Api { @@ -23,6 +25,8 @@ class Api { public tags: TagsAPI; public utils: UtilsAPI; public notifications: NotificationsAPI; + public foods: FoodAPI; + public units: UnitAPI; // Utils public upload: UploadFile; @@ -36,6 +40,8 @@ class Api { this.recipes = new RecipeAPI(requests); this.categories = new CategoriesAPI(requests); this.tags = new TagsAPI(requests); + this.units = new UnitAPI(requests); + this.foods = new FoodAPI(requests); // Users this.users = new UserApi(requests); diff --git a/frontend/composables/use-notifications.ts b/frontend/composables/use-notifications.ts index 0228a998f283..1ebd47ce1a5a 100644 --- a/frontend/composables/use-notifications.ts +++ b/frontend/composables/use-notifications.ts @@ -62,12 +62,14 @@ export const useNotifications = function () { } } - async function testById() { - // TODO: Test by ID + async function testById(id: number) { + const {data} = await api.notifications.testNotification(id, null) + console.log(data) } - async function testByUrl() { - // TODO: Test by URL + async function testByUrl(testUrl: string) { + const {data} = await api.notifications.testNotification(null, testUrl) + console.log(data) } const notifications = getNotifications(); diff --git a/frontend/composables/use-recipe-foods.ts b/frontend/composables/use-recipe-foods.ts new file mode 100644 index 000000000000..884f5016d0bc --- /dev/null +++ b/frontend/composables/use-recipe-foods.ts @@ -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 }; +}; diff --git a/frontend/composables/use-recipe-units.ts b/frontend/composables/use-recipe-units.ts new file mode 100644 index 000000000000..f33dd35bb605 --- /dev/null +++ b/frontend/composables/use-recipe-units.ts @@ -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 }; +}; diff --git a/frontend/layouts/admin.vue b/frontend/layouts/admin.vue index 8e4e4a14a454..fedbaabb804b 100644 --- a/frontend/layouts/admin.vue +++ b/frontend/layouts/admin.vue @@ -83,6 +83,16 @@ export default defineComponent({ to: "/admin/toolbox/notifications", 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, to: "/admin/toolbox/categories", diff --git a/frontend/pages/admin/toolbox/foods.vue b/frontend/pages/admin/toolbox/foods.vue new file mode 100644 index 000000000000..b59c626a47a7 --- /dev/null +++ b/frontend/pages/admin/toolbox/foods.vue @@ -0,0 +1,120 @@ + + + + + \ No newline at end of file diff --git a/frontend/pages/admin/toolbox/notifications.vue b/frontend/pages/admin/toolbox/notifications.vue index 1c9c0bbd405e..7d09b33a6a45 100644 --- a/frontend/pages/admin/toolbox/notifications.vue +++ b/frontend/pages/admin/toolbox/notifications.vue @@ -47,7 +47,12 @@ :label="$t('events.apprise-url')" > - + {{ $t("general.test") }} diff --git a/frontend/pages/admin/toolbox/units.vue b/frontend/pages/admin/toolbox/units.vue new file mode 100644 index 000000000000..19597b32db20 --- /dev/null +++ b/frontend/pages/admin/toolbox/units.vue @@ -0,0 +1,122 @@ + + + + + \ No newline at end of file diff --git a/frontend/plugins/globals.js b/frontend/plugins/globals.js index 3d71328fc005..6803539c624e 100644 --- a/frontend/plugins/globals.js +++ b/frontend/plugins/globals.js @@ -92,6 +92,8 @@ import { mdiMinus, mdiWindowClose, mdiFolderZipOutline, + mdiFoodApple, + mdiBeakerOutline, } from "@mdi/js"; const icons = { @@ -99,6 +101,8 @@ const icons = { primary: mdiSilverwareVariant, // General + foods: mdiFoodApple, + units: mdiBeakerOutline, alert: mdiAlert, alertCircle: mdiAlertCircle, api: mdiApi,