feat(frontend): ✨ Create CRUD User Interface for Units and Foods
This commit is contained in:
parent
122d35ec09
commit
a1aad078da
13 changed files with 517 additions and 42 deletions
|
@ -10,39 +10,16 @@ export interface CrudAPIInterface {
|
|||
// Methods
|
||||
}
|
||||
|
||||
export const crudMixins = <T>(
|
||||
requests: ApiRequestInstance,
|
||||
baseRoute: string,
|
||||
itemRoute: (itemId: string) => string
|
||||
) => {
|
||||
async function getAll(start = 0, limit = 9999) {
|
||||
return await requests.get<T[]>(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<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 {
|
||||
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);
|
||||
}
|
||||
|
||||
async getOne(itemId: string) {
|
||||
async getOne(itemId: string | number) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<EventNotification, CreateEvent
|
|||
itemRoute = routes.aboutEventsNotificationsId;
|
||||
/** Returns the Group Data for the Current User
|
||||
*/
|
||||
async testNotification(id: number) {
|
||||
return await requests.post(routes.aboutEventsNotificationsTest, { id });
|
||||
async testNotification(id: number | null = null, testUrl: string | null = null) {
|
||||
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 { 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);
|
||||
|
|
|
@ -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();
|
||||
|
|
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",
|
||||
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",
|
||||
|
|
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')"
|
||||
></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>
|
||||
{{ $t("general.test") }}
|
||||
</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,
|
||||
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,
|
||||
|
|
Loading…
Reference in a new issue