mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2025-12-24 02:39:20 -05:00
models, messages and multiselects
This commit is contained in:
8
.idea/watcherTasks.xml
generated
8
.idea/watcherTasks.xml
generated
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectTasksOptions">
|
||||
<TaskOptions isEnabled="true">
|
||||
<TaskOptions isEnabled="false">
|
||||
<option name="arguments" value="-m flake8 $FilePath$ --config $ContentRoot$\.flake8" />
|
||||
<option name="checkSyntaxErrors" value="true" />
|
||||
<option name="description" />
|
||||
@@ -27,7 +27,7 @@
|
||||
<option name="workingDir" value="" />
|
||||
<envs />
|
||||
</TaskOptions>
|
||||
<TaskOptions isEnabled="true">
|
||||
<TaskOptions isEnabled="false">
|
||||
<option name="arguments" value="-m isort $FilePath$" />
|
||||
<option name="checkSyntaxErrors" value="true" />
|
||||
<option name="description" />
|
||||
@@ -47,7 +47,7 @@
|
||||
<option name="workingDir" value="" />
|
||||
<envs />
|
||||
</TaskOptions>
|
||||
<TaskOptions isEnabled="true">
|
||||
<TaskOptions isEnabled="false">
|
||||
<option name="arguments" value="-m yapf -i $FilePath$" />
|
||||
<option name="checkSyntaxErrors" value="true" />
|
||||
<option name="description" />
|
||||
@@ -67,7 +67,7 @@
|
||||
<option name="workingDir" value="" />
|
||||
<envs />
|
||||
</TaskOptions>
|
||||
<TaskOptions isEnabled="true">
|
||||
<TaskOptions isEnabled="false">
|
||||
<option name="arguments" value="--cwd $ProjectFileDir$\vue prettier -w --config $ProjectFileDir$\.prettierrc $FilePath$" />
|
||||
<option name="checkSyntaxErrors" value="true" />
|
||||
<option name="description" />
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"vue-router": "4",
|
||||
"vue-simple-calendar": "^7.1.0",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"vuetify": "^3.5.16"
|
||||
"vuetify": "^3.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<v-app>
|
||||
|
||||
<v-app-bar color="tandoor" flat density="comfortable">
|
||||
<router-link :to="{name: 'view_home', params: {}}">
|
||||
<router-link :to="{ name: 'view_home', params: {} }">
|
||||
<v-img src="../../assets/brand_logo.svg" width="140px" class="ms-2"></v-img>
|
||||
</router-link>
|
||||
<v-spacer></v-spacer>
|
||||
<global-search-dialog></global-search-dialog>
|
||||
<v-btn>DEBUG <message-list-dialog></message-list-dialog></v-btn>
|
||||
|
||||
<v-avatar color="cyan" class="me-2">V</v-avatar>
|
||||
</v-app-bar>
|
||||
|
||||
@@ -17,14 +18,15 @@
|
||||
<v-navigation-drawer v-if="lgAndUp">
|
||||
<v-list-item title="My Application" subtitle="Vuetify"></v-list-item>
|
||||
<v-divider></v-divider>
|
||||
<v-list-item prepend-icon="fas fa-book" title="Home" :to="{name: 'view_home', params: {}}"></v-list-item>
|
||||
<v-list-item prepend-icon="fas fa-calendar-alt" title="Mealplan" :to="{name: 'view_mealplan', params: {}}"></v-list-item>
|
||||
<v-list-item prepend-icon="fas fa-shopping-cart" title="Shopping" :to="{name: 'view_shopping', params: {}}"></v-list-item>
|
||||
<v-list-item prepend-icon="fas fa-bars" title="More" :to="{name: 'view_books', params: {}}"></v-list-item> <!-- TODO link -->
|
||||
<v-list-item prepend-icon="fas fa-book" title="Home" :to="{ name: 'view_home', params: {} }"></v-list-item>
|
||||
<v-list-item prepend-icon="fas fa-calendar-alt" title="Mealplan" :to="{ name: 'view_mealplan', params: {} }"></v-list-item>
|
||||
<v-list-item prepend-icon="fas fa-shopping-cart" title="Shopping" :to="{ name: 'view_shopping', params: {} }"></v-list-item>
|
||||
<v-list-item prepend-icon="fas fa-bars" title="More" :to="{ name: 'view_books', params: {} }"></v-list-item>
|
||||
<!-- TODO link -->
|
||||
</v-navigation-drawer>
|
||||
|
||||
<v-bottom-navigation grow v-if="!lgAndUp">
|
||||
<v-btn value="recent" :to="{name: 'view_home', params: {}}">
|
||||
<v-btn value="recent" :to="{ name: 'view_home', params: {} }">
|
||||
<v-icon icon="fa-fw fas fa-book "/>
|
||||
<span>Recipes</span>
|
||||
</v-btn>
|
||||
@@ -40,33 +42,31 @@
|
||||
|
||||
<span>Shopping</span>
|
||||
</v-btn>
|
||||
<v-btn value="nearby" to="/books"> <!-- TODO link -->
|
||||
<v-btn value="nearby" to="/books">
|
||||
<!-- TODO link -->
|
||||
<v-icon icon="fa-fw fas fa-bars"></v-icon>
|
||||
|
||||
<span>More</span>
|
||||
</v-btn>
|
||||
</v-bottom-navigation>
|
||||
|
||||
<v-snackbar-queued
|
||||
:vertical="true"
|
||||
location="top center"
|
||||
></v-snackbar-queued>
|
||||
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import GlobalSearchDialog from "@/components/inputs/GlobalSearchDialog.vue"
|
||||
|
||||
import GlobalSearchDialog from "@/components/inputs/GlobalSearchDialog.vue";
|
||||
import {ref} from "vue";
|
||||
import {useDisplay} from "vuetify";
|
||||
import {useDisplay} from "vuetify"
|
||||
import VSnackbarQueued from "@/components/display/VSnackbarQueued.vue";
|
||||
import MessageListDialog from "@/components/dialogs/MessageListDialog.vue";
|
||||
|
||||
const {lgAndUp} = useDisplay()
|
||||
|
||||
const drawer = ref(true)
|
||||
const rail = ref(true)
|
||||
const overlay = ref(false)
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -2,39 +2,35 @@
|
||||
<v-dialog activator="parent" v-model="dialog">
|
||||
<template v-slot:default="{ isActive }">
|
||||
<v-card style="overflow: auto">
|
||||
<v-card-title>Meal Plan Edit <i class="mt-2 float-right fas fa-times" @click="isActive.value = false"></i></v-card-title>
|
||||
<v-card-title>Meal Plan Edit
|
||||
<v-btn icon="fas fa-times" variant="flat" size="x-small" class="mt-2 float-right " @click="isActive.value = false"></v-btn>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field label="Title"></v-text-field>
|
||||
<v-text-field label="From Date" type="date"></v-text-field>
|
||||
<v-text-field label="Meal Type"></v-text-field>
|
||||
<v-number-input control-variant="split" :min="0"></v-number-input>
|
||||
<v-text-field label="Share"></v-text-field>
|
||||
<v-text-field label="Title" v-model="mutableMealPlan.title"></v-text-field>
|
||||
<v-date-input
|
||||
v-model="dateRangeValue"
|
||||
label="Plan Date"
|
||||
multiple="range"
|
||||
prepend-icon=""
|
||||
prepend-inner-icon="$calendar"
|
||||
></v-date-input>
|
||||
<ModelSelect model="MealType" v-model="mutableMealPlan.mealType"></ModelSelect>
|
||||
<v-number-input control-variant="split" :min="0" v-model="mutableMealPlan.servings"></v-number-input>
|
||||
<v-text-field label="Share" v-model="mutableMealPlan.shared"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<Multiselect
|
||||
name="recipe"
|
||||
:columns="{ sm: 12, md : 6}"
|
||||
label="Recipe"
|
||||
label-prop="name"
|
||||
value-prop="id"
|
||||
:object="true"
|
||||
:strict="false"
|
||||
:search="true"
|
||||
:items="recipeSearch"
|
||||
:delay="300"
|
||||
rules="required"
|
||||
></Multiselect>
|
||||
<v-text-field label="To Date" type="date"></v-text-field>
|
||||
<ModelSelect model="recipe" v-model="mutableMealPlan.recipe"></ModelSelect>
|
||||
<!-- <v-number-input label="Days" control-variant="split" :min="1"></v-number-input>--> <!--TODO create days input with +/- snyced to date -->
|
||||
<recipe-card :recipe="mutableMealPlan.recipe" v-if="mutableMealPlan && mutableMealPlan.recipe"></recipe-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-textarea label="Note"></v-textarea>
|
||||
<v-textarea label="Note" v-model="mutableMealPlan.note"></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
@@ -60,7 +56,10 @@ import {DateTime} from "luxon";
|
||||
import RecipeCard from "@/components/display/RecipeCard.vue";
|
||||
import {useMealPlanStore} from "@/stores/MealPlanStore";
|
||||
import {VNumberInput} from 'vuetify/labs/VNumberInput' //TODO remove once component is out of labs
|
||||
import {VDateInput} from 'vuetify/labs/VDateInput' //TODO remove once component is out of labs
|
||||
import Multiselect from '@vueform/multiselect'
|
||||
import ModelSelect from "@/components/inputs/ModelSelect.vue";
|
||||
import {useMessageStore} from "@/stores/MessageStore";
|
||||
|
||||
const props = defineProps(
|
||||
{
|
||||
@@ -69,12 +68,16 @@ const props = defineProps(
|
||||
)
|
||||
|
||||
const dialog = ref(false)
|
||||
let mutableMealPlan = ref(props.mealPlan)
|
||||
let mutableMealPlan = ref(newMealPlan())
|
||||
const dateRangeValue = ref([] as Date[])
|
||||
|
||||
if (props.mealPlan != undefined) {
|
||||
mutableMealPlan.value = props.mealPlan
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.mealPlan != undefined) {
|
||||
mutableMealPlan.value = props.mealPlan
|
||||
|
||||
} else {
|
||||
mutableMealPlan.value = newMealPlan()
|
||||
}
|
||||
@@ -84,6 +87,14 @@ function saveMealPlan() {
|
||||
|
||||
if (mutableMealPlan.value != undefined) {
|
||||
mutableMealPlan.value.recipe = mutableMealPlan.value.recipe as RecipeOverview
|
||||
if (dateRangeValue.value != null) {
|
||||
mutableMealPlan.value.fromDate = dateRangeValue.value[0]
|
||||
mutableMealPlan.value.toDate = dateRangeValue.value[dateRangeValue.value.length-1]
|
||||
} else {
|
||||
useMessageStore().addError('Missing Dates')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('calling save method')
|
||||
useMealPlanStore().createOrUpdate(mutableMealPlan.value).catch(err => {
|
||||
// TODO handle error
|
||||
@@ -100,23 +111,6 @@ function newMealPlan() {
|
||||
} as MealPlan
|
||||
}
|
||||
|
||||
async function mealTypeSearch(searchQuery: string) {
|
||||
console.log('called search')
|
||||
const api = new ApiApi()
|
||||
return await api.apiMealTypeList()
|
||||
}
|
||||
|
||||
async function shareUserSearch(searchQuery: string) {
|
||||
console.log('called su search')
|
||||
const api = new ApiApi()
|
||||
return await api.apiUserList()
|
||||
}
|
||||
|
||||
async function recipeSearch(searchQuery: string) {
|
||||
console.log('called recipe search')
|
||||
const api = new ApiApi()
|
||||
return (await api.apiRecipeList({query: searchQuery})).results
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
156
vue3/src/components/dialogs/MessageListDialog.vue
Normal file
156
vue3/src/components/dialogs/MessageListDialog.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<v-dialog max-width="70vw" min-height="80vh" activator="parent">
|
||||
<template v-slot:default="{ isActive }">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
Nachrichten <v-btn icon="fas fa-times" variant="flat" size="x-small" class="mt-2 float-right " @click="isActive.value = false"></v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<h4>Filter</h4>
|
||||
|
||||
<v-text-field
|
||||
class="mt-2"
|
||||
v-model="search"
|
||||
label="Search"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="outlined"
|
||||
clearable
|
||||
hide-details
|
||||
single-line
|
||||
></v-text-field>
|
||||
|
||||
<v-btn-toggle
|
||||
class="mt-2"
|
||||
v-model="typeFilter"
|
||||
variant="outlined"
|
||||
divided
|
||||
multiple>
|
||||
<v-btn :value="MessageType.SUCCESS">
|
||||
<v-icon icon="fas fa-eye" class="me-2" color="success"></v-icon>
|
||||
Success
|
||||
</v-btn>
|
||||
<v-btn :value="MessageType.INFO">
|
||||
<v-icon icon="fas fa-eye" class="me-2" color="info"></v-icon>
|
||||
Info
|
||||
</v-btn>
|
||||
<v-btn :value="MessageType.WARNING">
|
||||
<v-icon icon="fas fa-eye" class="me-2" color="warning"></v-icon>
|
||||
Warning
|
||||
</v-btn>
|
||||
<v-btn :value="MessageType.ERROR">
|
||||
<v-icon icon="fas fa-eye" class="me-2" color="error"></v-icon>
|
||||
Error
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
|
||||
<v-data-table
|
||||
:headers="tableHeaders"
|
||||
:items="displayItems"
|
||||
:sort-by="sortBy"
|
||||
:search="search"
|
||||
>
|
||||
|
||||
<template v-slot:item.type="{ value }">
|
||||
<v-chip :color="value">
|
||||
{{ value }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-icon icon="fas fa-search" @click="showDetailDialog = true; detailItem = item"></v-icon>
|
||||
<v-icon class="ms-1" icon="fas fa-copy" @click="copy(JSON.stringify({'type': item.type, 'createdAt': item.createdAt, 'msg': item.msg, 'data': item.data}));"></v-icon>
|
||||
</template>
|
||||
</v-data-table>
|
||||
|
||||
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="useMessageStore().deleteAllMessages()" color="error">Alle Löschen</v-btn>
|
||||
<v-btn @click="addTestMessage()" color="warning">Test Nachricht</v-btn>
|
||||
<v-btn @click="isActive.value = false">Close</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog v-model="showDetailDialog" max-width="50vw">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
Nachricht Details <small>{{ DateTime.fromSeconds(detailItem.createdAt).toLocaleString(DateTime.DATETIME_MED) }}</small>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-label>Typ</v-label>
|
||||
<br/>
|
||||
<v-chip :color="detailItem.type">{{ detailItem.type }}</v-chip>
|
||||
<br/>
|
||||
|
||||
<v-label class="mt-2">Nachricht</v-label>
|
||||
<p>{{ detailItem.msg }}</p>
|
||||
|
||||
<v-label class="mt-2">Data</v-label>
|
||||
<pre style="white-space: pre-wrap;" v-if="detailItem.data != null">{{ detailItem.data }}</pre>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn
|
||||
text="Close Dialog"
|
||||
@click="showDetailDialog = false"
|
||||
></v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, ref} from 'vue'
|
||||
import {Message, MessageType, useMessageStore} from "@/stores/MessageStore";
|
||||
import {DateTime} from "luxon";
|
||||
import {useClipboard} from "@vueuse/core";
|
||||
|
||||
const {copy} = useClipboard()
|
||||
|
||||
const displayItems = computed(() => {
|
||||
let items = [] as Message[]
|
||||
useMessageStore().messages.forEach(m => {
|
||||
if (typeFilter.value.includes(m.type)) {
|
||||
items.push(m)
|
||||
}
|
||||
})
|
||||
return items
|
||||
})
|
||||
|
||||
const sortBy = ref([{key: 'createdAt', order: 'desc'}])
|
||||
const search = ref('')
|
||||
const tableHeaders = ref([
|
||||
{title: 'Type', key: 'type'},
|
||||
{
|
||||
title: 'Created',
|
||||
key: 'createdAt',
|
||||
value: (item: Message) => `${DateTime.fromSeconds(item.createdAt).toLocaleString(DateTime.DATETIME_MED)}`,
|
||||
},
|
||||
{title: 'Message', key: 'msg'},
|
||||
{title: 'Actions', key: 'actions', align: 'end'},
|
||||
])
|
||||
const typeFilter = ref([MessageType.SUCCESS, MessageType.INFO, MessageType.WARNING, MessageType.ERROR])
|
||||
const detailItem = ref({} as Message)
|
||||
const showDetailDialog = ref(false)
|
||||
|
||||
function addTestMessage() {
|
||||
let types = [MessageType.SUCCESS, MessageType.ERROR, MessageType.INFO, MessageType.WARNING]
|
||||
useMessageStore().addMessage(types[Math.floor(Math.random() * types.length)], `Lorem Ipsum ${Math.random() * 1000}`, 5000, {json: "data", 'msg': 'whatever', data: 1})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -61,7 +61,7 @@ const calendarItemHeight = computed(() => {
|
||||
onMounted(() => {
|
||||
let api = new ApiApi()
|
||||
api.apiMealPlanList().then(r => {
|
||||
mealPlans.value = r
|
||||
mealPlans.value = r.results
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
87
vue3/src/components/display/VSnackbarQueued.vue
Normal file
87
vue3/src/components/display/VSnackbarQueued.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<v-snackbar
|
||||
v-model="showSnackbar"
|
||||
:timer="true"
|
||||
:timeout="visibleMessage.showTimeout"
|
||||
:color="visibleMessage.type"
|
||||
:vertical="vertical"
|
||||
:location="location"
|
||||
multi-line
|
||||
>
|
||||
<small>{{ DateTime.fromSeconds(visibleMessage.createdAt).toLocaleString(DateTime.DATETIME_MED) }}</small> <br/>
|
||||
{{ visibleMessage.msg }}
|
||||
|
||||
<template v-slot:actions>
|
||||
|
||||
<v-btn variant="text">View <message-list-dialog></message-list-dialog> </v-btn>
|
||||
<v-btn variant="text" @click="removeItem()">
|
||||
<span v-if="useMessageStore().snackbarQueue.length > 1">Next ({{ useMessageStore().snackbarQueue.length - 1 }})</span>
|
||||
<span v-else>Close</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref} from 'vue'
|
||||
import {Message, useMessageStore} from "@/stores/MessageStore";
|
||||
import {DateTime} from "luxon";
|
||||
import MessageListDialog from "@/components/dialogs/MessageListDialog.vue";
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* passed to VSnackbar location prop https://vuetifyjs.com/en/api/v-snackbar/#props-location
|
||||
* Specifies the anchor point for positioning the component, using directional cues to align it either horizontally, vertically, or both…
|
||||
*/
|
||||
location: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: 'bottom'
|
||||
},
|
||||
/**
|
||||
* passed to VSnackbar vertical prop https://vuetifyjs.com/en/api/v-snackbar/#props-vertical
|
||||
* Stacks snackbar content on top of the actions (button).
|
||||
*/
|
||||
vertical: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const timeoutId = ref(-1)
|
||||
const visibleMessage = ref({} as Message)
|
||||
const showSnackbar = ref(false)
|
||||
|
||||
useMessageStore().$subscribe((mutation, state) => {
|
||||
if ('snackbarQueue' in state && mutation.events.type == 'add') {
|
||||
processQueue()
|
||||
}
|
||||
})
|
||||
|
||||
function processQueue() {
|
||||
if (timeoutId.value == -1 && useMessageStore().snackbarQueue.length > 0) {
|
||||
visibleMessage.value = useMessageStore().snackbarQueue[0]
|
||||
showSnackbar.value = true
|
||||
timeoutId.value = setTimeout(() => {
|
||||
useMessageStore().snackbarQueue.shift()
|
||||
timeoutId.value = -1
|
||||
processQueue()
|
||||
}, visibleMessage.value.showTimeout + 50)
|
||||
}
|
||||
}
|
||||
|
||||
function removeItem() {
|
||||
showSnackbar.value = false
|
||||
clearTimeout(timeoutId.value)
|
||||
timeoutId.value = -1
|
||||
useMessageStore().snackbarQueue.shift()
|
||||
processQueue()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,98 +1,117 @@
|
||||
<template>
|
||||
<v-input>
|
||||
<!--TODO Problems: 1. behind other cards when those are underneath the element, making card overflow visible breaks cards -->
|
||||
<VueMultiselect
|
||||
:id="id"
|
||||
v-model="selected_items"
|
||||
:options="items"
|
||||
:close-on-select="true"
|
||||
:clear-on-select="true"
|
||||
:hide-selected="multiple"
|
||||
:preserve-search="true"
|
||||
:internal-search="false"
|
||||
:limit="limit"
|
||||
:placeholder="model"
|
||||
:label="label"
|
||||
track-by="id"
|
||||
:multiple="multiple"
|
||||
:taggable="allowCreate"
|
||||
tag-placeholder="TODO CREATE PLACEHOLDER"
|
||||
:loading="search_loading"
|
||||
@search-change="debouncedSearchFunction"
|
||||
@input="selectionChanged"
|
||||
@tag="addItem"
|
||||
@open="search('')"
|
||||
:disabled="disabled"
|
||||
>
|
||||
</VueMultiselect>
|
||||
</v-input>
|
||||
<!-- <!–TODO Problems: 1. behind other cards when those are underneath the element, making card overflow visible breaks cards –>-->
|
||||
<!-- <VueMultiselect-->
|
||||
<!-- :id="id"-->
|
||||
<!-- v-model="selected_items"-->
|
||||
<!-- :options="items"-->
|
||||
<!-- :close-on-select="true"-->
|
||||
<!-- :clear-on-select="true"-->
|
||||
<!-- :hide-selected="multiple"-->
|
||||
<!-- :preserve-search="true"-->
|
||||
<!-- :internal-search="false"-->
|
||||
<!-- :limit="limit"-->
|
||||
<!-- :placeholder="model"-->
|
||||
<!-- :label="label"-->
|
||||
<!-- track-by="id"-->
|
||||
<!-- :multiple="multiple"-->
|
||||
<!-- :taggable="allowCreate"-->
|
||||
<!-- tag-placeholder="TODO CREATE PLACEHOLDER"-->
|
||||
<!-- :loading="search_loading"-->
|
||||
<!-- @search-change="debouncedSearchFunction"-->
|
||||
<!-- @input="selectionChanged"-->
|
||||
<!-- @tag="addItem"-->
|
||||
<!-- @open="search('')"-->
|
||||
<!-- :disabled="disabled"-->
|
||||
<!-- class="material-multiselect"-->
|
||||
|
||||
<!-- >-->
|
||||
<!-- </VueMultiselect>-->
|
||||
|
||||
<Multiselect
|
||||
|
||||
class="material-multiselect z-max"
|
||||
v-model="model"
|
||||
:options="search"
|
||||
:delay="300"
|
||||
:object="true"
|
||||
valueProp="id"
|
||||
:label="label"
|
||||
:searchable="true"
|
||||
:strict="false"
|
||||
:disabled="disabled"
|
||||
|
||||
|
||||
/>
|
||||
|
||||
</v-input>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {computed, onMounted, ref, Ref,} from 'vue'
|
||||
import {ApiApi} from "@/openapi/index.js";
|
||||
import {useDebounceFn} from "@vueuse/core";
|
||||
import {GenericModel, getModelFromStr} from "@/types/Models";
|
||||
import VueMultiselect from 'vue-multiselect'
|
||||
import {computed, onMounted, PropType, ref, Ref} from "vue"
|
||||
import {ApiApi} from "@/openapi/index.js"
|
||||
import {useDebounceFn} from "@vueuse/core"
|
||||
import {GenericModel, getModelFromStr} from "@/types/Models"
|
||||
import Multiselect from '@vueform/multiselect'
|
||||
|
||||
const props = defineProps(
|
||||
{
|
||||
model: {type: String, required: true},
|
||||
multiple: {type: Boolean, default: true},
|
||||
limit: {type: Number, default: 25},
|
||||
allowCreate: {type: Boolean, default: false},
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
id: {type: String, required: false, default: Math.random().toString()},
|
||||
const props = defineProps({
|
||||
model: {type: String, required: true},
|
||||
|
||||
// not verified
|
||||
search_on_load: {type: Boolean, default: false},
|
||||
id: {type: String, required: false, default: Math.random().toString()},
|
||||
|
||||
// not verified
|
||||
|
||||
clearable: {type: Boolean, default: false,},
|
||||
chips: {type: Boolean, default: undefined,},
|
||||
multiple: {type: Boolean, default: true},
|
||||
limit: {type: Number, default: 25},
|
||||
allowCreate: {type: Boolean, default: false},
|
||||
|
||||
itemName: {type: String, default: 'name'},
|
||||
itemValue: {type: String, default: 'id'},
|
||||
search_on_load: {type: Boolean, default: false},
|
||||
|
||||
clearable: {type: Boolean, default: false},
|
||||
chips: {type: Boolean, default: undefined},
|
||||
|
||||
placeholder: {type: String, default: undefined},
|
||||
label: {type: String, default: "name"},
|
||||
parent_variable: {type: String, default: undefined},
|
||||
itemName: {type: String, default: "name"},
|
||||
itemValue: {type: String, default: "id"},
|
||||
|
||||
sticky_options: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
placeholder: {type: String, default: undefined},
|
||||
label: {type: String, default: "name"},
|
||||
parent_variable: {type: String, default: undefined},
|
||||
|
||||
sticky_options: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
initial_selection: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
initial_single_selection: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
initial_selection: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
initial_single_selection: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
|
||||
disabled: {type: Boolean, default: false},
|
||||
})
|
||||
|
||||
disabled: {type: Boolean, default: false,},
|
||||
}
|
||||
)
|
||||
|
||||
const model = defineModel()
|
||||
const model_class = ref({} as GenericModel<any>)
|
||||
const items: Ref<Array<any>> = ref([])
|
||||
const selected_items: Ref<Array<any> | any> = ref(undefined)
|
||||
const search_query = ref('')
|
||||
const search_query = ref("")
|
||||
const search_loading = ref(false)
|
||||
|
||||
const elementId = ref((Math.random() * 100000).toString())
|
||||
|
||||
onMounted(() => {
|
||||
model_class.value = getModelFromStr(props.model)
|
||||
if (props.search_on_load) {
|
||||
debouncedSearchFunction('')
|
||||
debouncedSearchFunction("")
|
||||
}
|
||||
})
|
||||
|
||||
@@ -108,16 +127,9 @@ const debouncedSearchFunction = useDebounceFn((query: string) => {
|
||||
* @param query input to search for on the API
|
||||
*/
|
||||
function search(query: string) {
|
||||
|
||||
search_loading.value = true
|
||||
model_class.value.list(query).then(r => {
|
||||
items.value = r
|
||||
if (props.allowCreate && search_query.value != '') {
|
||||
// TODO check if search_query is already in items
|
||||
items.value.unshift({id: null, name: `Create "${search_query.value}"`})
|
||||
}
|
||||
|
||||
}).catch(err => {
|
||||
return model_class.value.list(query).then((r) => {
|
||||
return r
|
||||
}).catch((err) => {
|
||||
//useMessageStore().addMessage(MessageType.ERROR, err, 8000)
|
||||
}).finally(() => {
|
||||
search_loading.value = false
|
||||
@@ -129,7 +141,7 @@ function addItem(item: string) {
|
||||
const api = new ApiApi()
|
||||
api.apiKeywordList()
|
||||
|
||||
model_class.value.create(item).then(createdObj => {
|
||||
model_class.value.create(item).then((createdObj) => {
|
||||
//StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_CREATE)
|
||||
if (selected_items.value instanceof Array) {
|
||||
selected_items.value.push(createdObj)
|
||||
@@ -146,12 +158,19 @@ function addItem(item: string) {
|
||||
}
|
||||
|
||||
function selectionChanged() {
|
||||
//this.$emit("change", { var: this.parent_variable, val: this.selected_objects })
|
||||
emit('update:modelValue', selected_items)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
|
||||
<style src="@vueform/multiselect/themes/default.css"></style>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
.material-multiselect {
|
||||
--ms-line-height: 2.5;
|
||||
--ms-bg: rgba(235, 235, 235, 0.75);
|
||||
--ms-border-color: 0;
|
||||
--ms-border-color-active: 0;
|
||||
border-bottom: 4px #0f0f0f;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,77 +1,80 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-container>
|
||||
|
||||
<horizontal-meal-plan-window></horizontal-meal-plan-window>
|
||||
|
||||
<!--TODO ideas for "start page": new recipes, meal plan, "last year/month/cooked long ago", high rated, random keyword -->
|
||||
<!--TODO if nothing comes up for a category, hide the element, probably move fetch logic into component -->
|
||||
<horizontal-recipe-scroller title="New Recipes" :skeletons="4" :recipes="new_recipes" icon="fas fa-calendar-alt"></horizontal-recipe-scroller>
|
||||
<horizontal-recipe-scroller title="Top Rated" :skeletons="2" :recipes="high_rated_recipes" icon="fas fa-star"></horizontal-recipe-scroller>
|
||||
<horizontal-recipe-scroller :title="random_keyword.label" :skeletons="4" :recipes="random_keyword_recipes" icon="fas fa-tags" v-if="random_keyword.label"></horizontal-recipe-scroller>
|
||||
|
||||
</v-container>
|
||||
<horizontal-meal-plan-window></horizontal-meal-plan-window>
|
||||
|
||||
<!--TODO ideas for "start page": new recipes, meal plan, "last year/month/cooked long ago", high rated, random keyword -->
|
||||
<!--TODO if nothing comes up for a category, hide the element, probably move fetch logic into component -->
|
||||
<horizontal-recipe-scroller title="New Recipes" :skeletons="4" :recipes="new_recipes" icon="fas fa-calendar-alt"></horizontal-recipe-scroller>
|
||||
<horizontal-recipe-scroller title="Top Rated" :skeletons="2" :recipes="high_rated_recipes" icon="fas fa-star"></horizontal-recipe-scroller>
|
||||
<horizontal-recipe-scroller
|
||||
:title="random_keyword.label"
|
||||
:skeletons="4"
|
||||
:recipes="random_keyword_recipes"
|
||||
icon="fas fa-tags"
|
||||
v-if="random_keyword.label"
|
||||
></horizontal-recipe-scroller>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, ref} from 'vue'
|
||||
import {ApiApi, Keyword, Recipe, RecipeOverview} from "@/openapi";
|
||||
import KeywordsComponent from "@/components/display/KeywordsBar.vue";
|
||||
import RecipeCardComponent from "@/components/display/RecipeCard.vue";
|
||||
import GlobalSearchDialog from "@/components/inputs/GlobalSearchDialog.vue";
|
||||
import RecipeCard from "@/components/display/RecipeCard.vue";
|
||||
import HorizontalRecipeScroller from "@/components/display/HorizontalRecipeWindow.vue";
|
||||
import {DateTime} from "luxon";
|
||||
import {useMealPlanStore} from "@/stores/MealPlanStore";
|
||||
import HorizontalMealPlanWindow from "@/components/display/HorizontalMealPlanWindow.vue";
|
||||
import MealPlanDialog from "@/components/dialogs/MealPlanDialog.vue";
|
||||
|
||||
import { defineComponent, ref } from "vue"
|
||||
import { ApiApi, Keyword, Recipe, RecipeOverview } from "@/openapi"
|
||||
import KeywordsComponent from "@/components/display/KeywordsBar.vue"
|
||||
import RecipeCardComponent from "@/components/display/RecipeCard.vue"
|
||||
import GlobalSearchDialog from "@/components/inputs/GlobalSearchDialog.vue"
|
||||
import RecipeCard from "@/components/display/RecipeCard.vue"
|
||||
import HorizontalRecipeScroller from "@/components/display/HorizontalRecipeWindow.vue"
|
||||
import { DateTime } from "luxon"
|
||||
import { useMealPlanStore } from "@/stores/MealPlanStore"
|
||||
import HorizontalMealPlanWindow from "@/components/display/HorizontalMealPlanWindow.vue"
|
||||
import MealPlanDialog from "@/components/dialogs/MealPlanDialog.vue"
|
||||
import ModelSelect from "@/components/inputs/ModelSelect.vue"
|
||||
|
||||
export default defineComponent({
|
||||
name: "StartPage",
|
||||
components: {MealPlanDialog, HorizontalMealPlanWindow, HorizontalRecipeScroller, RecipeCard, GlobalSearchDialog, RecipeCardComponent, KeywordsComponent},
|
||||
computed: { },
|
||||
data() {
|
||||
return {
|
||||
recipes: [] as Recipe[],
|
||||
items: Array.from({length: 50}, (k, v) => v + 1),
|
||||
name: "StartPage",
|
||||
components: { ModelSelect, MealPlanDialog, HorizontalMealPlanWindow, HorizontalRecipeScroller, RecipeCard, GlobalSearchDialog, RecipeCardComponent, KeywordsComponent },
|
||||
computed: {},
|
||||
data() {
|
||||
return {
|
||||
recipes: [] as Recipe[],
|
||||
items: Array.from({ length: 50 }, (k, v) => v + 1),
|
||||
|
||||
new_recipes: [] as RecipeOverview[],
|
||||
high_rated_recipes: [] as RecipeOverview[],
|
||||
random_keyword: {} as Keyword,
|
||||
random_keyword_recipes: [] as RecipeOverview[],
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const api = new ApiApi()
|
||||
new_recipes: [] as RecipeOverview[],
|
||||
high_rated_recipes: [] as RecipeOverview[],
|
||||
random_keyword: {} as Keyword,
|
||||
random_keyword_recipes: [] as RecipeOverview[],
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const api = new ApiApi()
|
||||
|
||||
api.apiRecipeList({_new: 'true', pageSize: 16}).then(r => {
|
||||
if (r.results != undefined) { // TODO openapi generator makes arrays nullable for some reason
|
||||
this.new_recipes = r.results
|
||||
}
|
||||
api.apiRecipeList({ _new: "true", pageSize: 16 }).then((r) => {
|
||||
if (r.results != undefined) {
|
||||
// TODO openapi generator makes arrays nullable for some reason
|
||||
this.new_recipes = r.results
|
||||
}
|
||||
})
|
||||
|
||||
api.apiRecipeList({ rating: 4, pageSize: 16 }).then((r) => {
|
||||
if (r.results != undefined) {
|
||||
this.high_rated_recipes = r.results
|
||||
}
|
||||
})
|
||||
|
||||
api.apiKeywordList({ random: "true", limit: "1" }).then((r) => {
|
||||
if (r.results != undefined && r.results.length > 0) {
|
||||
this.random_keyword = r.results[0]
|
||||
api.apiRecipeList({ keywords: r.results[0].id }).then((r) => {
|
||||
if (r.results != undefined) {
|
||||
this.random_keyword_recipes = r.results
|
||||
}
|
||||
})
|
||||
|
||||
api.apiRecipeList({rating: 4, pageSize: 16}).then(r => {
|
||||
if (r.results != undefined) {
|
||||
this.high_rated_recipes = r.results
|
||||
}
|
||||
})
|
||||
|
||||
api.apiKeywordList({random: 'true', limit: '1'}).then(r => {
|
||||
if (r.results != undefined && r.results.length > 0) {
|
||||
this.random_keyword = r.results[0]
|
||||
api.apiRecipeList({keywords: r.results[0].id}).then(r => {
|
||||
if (r.results != undefined) {
|
||||
this.random_keyword_recipes = r.results
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -2,6 +2,7 @@ import {acceptHMRUpdate, defineStore} from "pinia"
|
||||
import {ApiApi, MealPlan} from "@/openapi";
|
||||
import {computed, ref} from "vue";
|
||||
import {DateTime} from "luxon";
|
||||
import {ErrorMessageType, MessageType, useMessageStore} from "@/stores/MessageStore";
|
||||
|
||||
|
||||
const _STORE_ID = "meal_plan_store"
|
||||
@@ -54,10 +55,12 @@ export const useMealPlanStore = defineStore(_STORE_ID, () => {
|
||||
|
||||
const api = new ApiApi()
|
||||
return api.apiMealPlanList({fromDate: DateTime.fromJSDate(from_date).toISODate() as string, toDate: DateTime.fromJSDate(to_date).toISODate() as string}).then(r => {
|
||||
r.forEach((p) => {
|
||||
r.results.forEach((p) => {
|
||||
plans.value.set(p.id, p)
|
||||
})
|
||||
currently_updating.value = [new Date(0), new Date(0)]
|
||||
}).catch((err) => {
|
||||
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
||||
})
|
||||
}
|
||||
return new Promise(() => {
|
||||
@@ -65,7 +68,7 @@ export const useMealPlanStore = defineStore(_STORE_ID, () => {
|
||||
}
|
||||
|
||||
function createOrUpdate(object: MealPlan) {
|
||||
if(object.id == undefined){
|
||||
if (object.id == undefined) {
|
||||
return createObject(object)
|
||||
} else {
|
||||
return updateObject(object)
|
||||
@@ -74,32 +77,32 @@ export const useMealPlanStore = defineStore(_STORE_ID, () => {
|
||||
|
||||
function createObject(object: MealPlan) {
|
||||
const api = new ApiApi()
|
||||
return api.apiMealPlanCreate({mealPlanRequest: object}).then((r) => {
|
||||
//StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_CREATE)
|
||||
return api.apiMealPlanCreate({mealPlan: object}).then((r) => {
|
||||
useMessageStore().addMessage(MessageType.SUCCESS, 'Created successfully', 7000, object)
|
||||
plans.value.set(r.id, r)
|
||||
return r
|
||||
}).catch((err) => {
|
||||
//StandardToasts.makeStandardToast(this, StandardToasts.FAIL_CREATE, err)
|
||||
useMessageStore().addError(ErrorMessageType.CREATE_ERROR, err)
|
||||
})
|
||||
}
|
||||
|
||||
function updateObject(object: MealPlan) {
|
||||
const api = new ApiApi()
|
||||
return api.apiMealPlanUpdate({id: object.id, mealPlanRequest: object}).then((r) => {
|
||||
//StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
|
||||
useMessageStore().addMessage(MessageType.SUCCESS, 'Updated successfully', 7000, object)
|
||||
plans.value.set(r.id, r)
|
||||
}).catch((err) => {
|
||||
//StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
|
||||
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
|
||||
})
|
||||
}
|
||||
|
||||
function deleteObject(object: MealPlan) {
|
||||
const api = new ApiApi()
|
||||
return api.apiMealPlanDestroy({id: object.id}).then((r) => {
|
||||
//StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_DELETE)
|
||||
useMessageStore().addMessage(MessageType.INFO, 'Deleted successfully', 7000, object)
|
||||
plans.value.delete(object.id)
|
||||
}).catch((err) => {
|
||||
//StandardToasts.makeStandardToast(this, StandardToasts.FAIL_DELETE, err)
|
||||
useMessageStore().addError(ErrorMessageType.DELETE_ERROR, err)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
96
vue3/src/stores/MessageStore.ts
Normal file
96
vue3/src/stores/MessageStore.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import {acceptHMRUpdate, defineStore} from 'pinia'
|
||||
import {ref} from "vue";
|
||||
import {useStorage} from "@vueuse/core";
|
||||
import {DateTime} from "luxon";
|
||||
|
||||
/** @enum {string} different message types */
|
||||
export enum MessageType {
|
||||
ERROR = 'error',
|
||||
WARNING = 'warning',
|
||||
INFO = 'info',
|
||||
SUCCESS = 'success',
|
||||
}
|
||||
|
||||
/** @enum {string} pre defined error messages */
|
||||
export enum ErrorMessageType {
|
||||
FETCH_ERROR = 'Fetch Error',
|
||||
UPDATE_ERROR = 'Update Error',
|
||||
CREATE_ERROR = 'Update Error',
|
||||
DELETE_ERROR = 'Update Error',
|
||||
}
|
||||
|
||||
/**
|
||||
* Type Message holding all required contents of a message
|
||||
*/
|
||||
export class Message {
|
||||
type = {} as MessageType
|
||||
createdAt = -1
|
||||
showTimeout = 0
|
||||
msg = ""
|
||||
data = {} as any
|
||||
code = ''
|
||||
|
||||
constructor(type: MessageType, msg: string, showTimeout?: number, data?: any) {
|
||||
if (typeof showTimeout === 'undefined') {
|
||||
showTimeout = 0
|
||||
}
|
||||
if (typeof data === 'undefined') {
|
||||
data = {}
|
||||
}
|
||||
|
||||
this.type = type
|
||||
this.msg = msg
|
||||
this.showTimeout = showTimeout
|
||||
this.data = data
|
||||
this.createdAt = DateTime.now().toSeconds()
|
||||
}
|
||||
|
||||
toString() {
|
||||
return {'type': this.type, 'createdAt': this.createdAt, 'msg': this.msg, 'data': this.data}
|
||||
}
|
||||
}
|
||||
|
||||
export const useMessageStore = defineStore('message_store', () => {
|
||||
let messages = useStorage('LOCAL_MESSAGES', [] as Message[])
|
||||
let snackbarQueue = ref([] as Message[])
|
||||
|
||||
/**
|
||||
* Add a message to the message store. If showTimeout is greater than 0 it is also added to the display queue.
|
||||
* @param {MessageType} type type of message
|
||||
* @param {String} msg message text
|
||||
* @param {number} showTimeout optional number of ms to show message to user, set to 0 or leave undefined for silent message
|
||||
* @param {string} data optional additional data only shown in log
|
||||
*/
|
||||
function addMessage(type: MessageType, msg: string, showTimeout?: number, data?: any) {
|
||||
|
||||
let message = new Message(type, msg, showTimeout, data)
|
||||
|
||||
messages.value.push(message)
|
||||
if (message.showTimeout > 0) {
|
||||
snackbarQueue.value.push(message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* shorthand function to quickly add an error message
|
||||
* @param errorType pre defined error type
|
||||
* @param data optional error data
|
||||
*/
|
||||
function addError(errorType: ErrorMessageType | string, data?: any) {
|
||||
addMessage(MessageType.ERROR, errorType, 7000, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* delete all messages from store
|
||||
*/
|
||||
function deleteAllMessages() {
|
||||
messages.value = [] as Message[]
|
||||
}
|
||||
|
||||
return {snackbarQueue, messages, addMessage, addError, deleteAllMessages}
|
||||
})
|
||||
|
||||
// enable hot reload for store
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useMessageStore, import.meta.hot))
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ApiApi, Keyword as IKeyword, Food as IFood, RecipeOverview as IRecipeOverview, Recipe as IRecipe, Unit as IUnit} from "@/openapi";
|
||||
import {ApiApi, Keyword as IKeyword, Food as IFood, RecipeOverview as IRecipeOverview, Recipe as IRecipe, Unit as IUnit, MealType as IMealType} from "@/openapi";
|
||||
|
||||
export function getModelFromStr(model_name: String) {
|
||||
switch (model_name.toLowerCase()) {
|
||||
@@ -14,6 +14,9 @@ export function getModelFromStr(model_name: String) {
|
||||
case 'recipe': {
|
||||
return new Recipe
|
||||
}
|
||||
case 'mealtype': {
|
||||
return new MealType
|
||||
}
|
||||
default: {
|
||||
throw Error(`Invalid Model ${model_name}, did you forget to register it in Models.ts?`)
|
||||
}
|
||||
@@ -99,4 +102,24 @@ export class Recipe extends GenericModel<IRecipeOverview> {
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class MealType extends GenericModel<IMealType> {
|
||||
create(name: string) {
|
||||
const api = new ApiApi()
|
||||
return api.apiMealTypeCreate({mealType: {name: name} as IMealType}).then(r => {
|
||||
return r as unknown as IRecipeOverview
|
||||
})
|
||||
}
|
||||
|
||||
list(query: string) {
|
||||
const api = new ApiApi()
|
||||
return api.apiMealTypeList({}).then(r => {
|
||||
if (r.results) {
|
||||
return r.results
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1212,10 +1212,10 @@ vuedraggable@^4.1.0:
|
||||
dependencies:
|
||||
sortablejs "1.14.0"
|
||||
|
||||
vuetify@^3.5.16:
|
||||
version "3.5.16"
|
||||
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.5.16.tgz#5046aab39bfa536f0d99c5be4f9d91a7245c3246"
|
||||
integrity sha512-jyApfATreFMkgjvK0bL7ntZnr+p9TU73+4E3kX6fIvUitdAP9fltG7yj+v3k14HLqZRSNhTL1GhQ95DFx631zw==
|
||||
vuetify@^3.6.1:
|
||||
version "3.6.1"
|
||||
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.6.1.tgz#bd26f1ee53c532a4f9880a5f376064f53efd9947"
|
||||
integrity sha512-fzcY9LNuLZUwXG4XyklkGJwFv/ejQaERAgE5e+U5M9dtbF9ZRs56mGi2uqOhQOv6o+vbikpHZ/rfHfhb/XlO0g==
|
||||
|
||||
w3c-xmlserializer@^4.0.0:
|
||||
version "4.0.0"
|
||||
|
||||
Reference in New Issue
Block a user