models, messages and multiselects

This commit is contained in:
vabene1111
2024-05-01 10:04:19 +02:00
parent 32b75250dc
commit 569b7e78fe
13 changed files with 607 additions and 226 deletions

View File

@@ -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>

View 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>

View File

@@ -61,7 +61,7 @@ const calendarItemHeight = computed(() => {
onMounted(() => {
let api = new ApiApi()
api.apiMealPlanList().then(r => {
mealPlans.value = r
mealPlans.value = r.results
})
})

View 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>

View File

@@ -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>
<!-- &lt;!&ndash;TODO Problems: 1. behind other cards when those are underneath the element, making card overflow visible breaks cards &ndash;&gt;-->
<!-- <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>