1
0
mirror of https://github.com/TandoorRecipes/recipes.git synced 2026-01-11 09:07:12 -05:00

meal plan and model editors

- changed signature to options object
- added ability to set defaults
- meal plan clickable item creation
This commit is contained in:
vabene1111
2024-10-11 17:45:47 +02:00
parent 7d531d18d4
commit cbcddfbcd1
9 changed files with 131 additions and 72 deletions

View File

@@ -1,15 +1,16 @@
<template> <template>
<v-dialog max-width="1400" :activator="activator" v-model="dialog"> <v-dialog max-width="1400" :activator="dialogActivator" v-model="model">
<component :is="editorComponent" :item="item" @create="createEvent" @save="saveEvent" @delete="deleteEvent" dialog @close="dialog = false"></component> <component :is="editorComponent" :item="item" @create="createEvent" @save="saveEvent" @delete="deleteEvent" dialog @close="dialog = false" :itemDefaults="itemDefaults"></component>
</v-dialog> </v-dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {defineAsyncComponent, PropType, ref, shallowRef, watch} from "vue"; import {defineAsyncComponent, PropType, shallowRef, watch} from "vue";
import {EditorSupportedModels, getGenericModelFromString} from "@/types/Models"; import {EditorSupportedModels, getGenericModelFromString} from "@/types/Models";
import {useI18n} from "vue-i18n"; import {useI18n} from "vue-i18n";
import {MealPlan} from "@/openapi";
const {t} = useI18n() const {t} = useI18n()
@@ -19,6 +20,7 @@ const props = defineProps({
model: { type: String as PropType<EditorSupportedModels>, required: true, }, model: { type: String as PropType<EditorSupportedModels>, required: true, },
activator: {default: 'parent'}, activator: {default: 'parent'},
item: {default: null}, item: {default: null},
itemDefaults: {required: false},
disabledFields: {default: []}, disabledFields: {default: []},
closeAfterCreate: {default: true}, closeAfterCreate: {default: true},
closeAfterSave: {default: true}, closeAfterSave: {default: true},
@@ -27,36 +29,41 @@ const props = defineProps({
const editorComponent = shallowRef(defineAsyncComponent(() => import(`@/components/model_editors/${getGenericModelFromString(props.model, t).model.name}Editor.vue`))) const editorComponent = shallowRef(defineAsyncComponent(() => import(`@/components/model_editors/${getGenericModelFromString(props.model, t).model.name}Editor.vue`)))
const dialog = ref(false) const model = defineModel<Boolean|undefined>({default: undefined})
const model = defineModel<Boolean>({default: false}) const dialogActivator = (model.value !== undefined) ? undefined : props.activator
/** /**
* Allow opening the model edit dialog trough v-model property of the dialog by watching for model changes * Allow opening the model edit dialog trough v-model property of the dialog by watching for model changes
*/ */
watch(model, (value, oldValue, onCleanup) => { watch(model, (value, oldValue, onCleanup) => {
console.log('model changed to ', value)
dialog.value = !!value
})
watch(dialog, (value, oldValue, onCleanup) => {
console.log('dialog changed to ', value)
model.value = !!value model.value = !!value
}) })
/**
* forward event to parent component and handle closing the editor if configured to do so
* @param arg model object from editor component
*/
function createEvent(arg: any) { function createEvent(arg: any) {
emit('create', arg) emit('create', arg)
dialog.value = dialog.value && !props.closeAfterCreate model.value = model.value && !props.closeAfterCreate
} }
/**
* forward event to parent component and handle closing the editor if configured to do so
* @param arg model object from editor component
*/
function saveEvent(arg: any) { function saveEvent(arg: any) {
emit('save', arg) emit('save', arg)
dialog.value = dialog.value && !props.closeAfterSave model.value = model.value && !props.closeAfterSave
} }
/**
* forward event to parent component and handle closing the editor if configured to do so
* @param arg model object from editor component
*/
function deleteEvent(arg: any) { function deleteEvent(arg: any) {
emit('delete', arg) emit('delete', arg)
dialog.value = dialog.value && !props.closeAfterDelete model.value = model.value && !props.closeAfterDelete
} }
</script> </script>

View File

@@ -8,7 +8,7 @@
:item-content-height="calendarItemHeight" :item-content-height="calendarItemHeight"
:enable-drag-drop="true" :enable-drag-drop="true"
@dropOnDate="dropCalendarItemOnDate" @dropOnDate="dropCalendarItemOnDate"
@click-date="newPlanDialog = true"> @click-date="(date : Date, calendarItems: [], windowEvent: any) => { newPlanDialogDefaultItem.fromDate = date; newPlanDialogDefaultItem.toDate = date; newPlanDialog = true }">
<template #header="{ headerProps }"> <template #header="{ headerProps }">
<CalendarViewHeader :header-props="headerProps"/> <CalendarViewHeader :header-props="headerProps"/>
</template> </template>
@@ -24,21 +24,20 @@
</template> </template>
</CalendarView> </CalendarView>
<model-edit-dialog model="MealPlan" v-model="newPlanDialog"></model-edit-dialog> <model-edit-dialog model="MealPlan" v-model="newPlanDialog" :itemDefaults="newPlanDialogDefaultItem" @create="(arg: any) => useMealPlanStore().plans.set(arg.id, arg)"></model-edit-dialog>
</v-col> </v-col>
</v-row> </v-row>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {CalendarView, CalendarViewHeader} from "vue-simple-calendar" import {CalendarView, CalendarViewHeader} from "vue-simple-calendar"
import "vue-simple-calendar/dist/style.css" import "vue-simple-calendar/dist/style.css"
import "vue-simple-calendar/dist/css/default.css" import "vue-simple-calendar/dist/css/default.css"
import MealPlanCalendarItem from "@/components/display/MealPlanCalendarItem.vue"; import MealPlanCalendarItem from "@/components/display/MealPlanCalendarItem.vue";
import {IMealPlanCalendarItem, IMealPlanNormalizedCalendarItem} from "@/types/MealPlan"; import {IMealPlanCalendarItem, IMealPlanNormalizedCalendarItem} from "@/types/MealPlan";
import {computed, nextTick, onMounted, ref, useTemplateRef} from "vue"; import {computed, onMounted, ref} from "vue";
import {DateTime} from "luxon"; import {DateTime} from "luxon";
import {useDisplay} from "vuetify"; import {useDisplay} from "vuetify";
import {useMealPlanStore} from "@/stores/MealPlanStore"; import {useMealPlanStore} from "@/stores/MealPlanStore";
@@ -50,6 +49,7 @@ const {lgAndUp} = useDisplay()
const currentlyDraggedMealplan = ref({} as IMealPlanNormalizedCalendarItem) const currentlyDraggedMealplan = ref({} as IMealPlanNormalizedCalendarItem)
const newPlanDialog = ref(false) const newPlanDialog = ref(false)
const newPlanDialogDefaultItem = ref({} as MealPlan)
/** /**
* computed property that converts array of MealPlan object to * computed property that converts array of MealPlan object to

View File

@@ -49,9 +49,11 @@ const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading,
onMounted(() => { onMounted(() => {
setupState(props.item, props.itemId, () => { setupState(props.item, props.itemId, {
editingObj.value.expires = DateTime.now().plus({year: 1}).toJSDate() newItemFunction: () => {
editingObj.value.scope = 'read write' editingObj.value.expires = DateTime.now().plus({year: 1}).toJSDate()
editingObj.value.scope = 'read write'
}
}) })
}) })

View File

@@ -52,21 +52,23 @@ const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading,
// object specific data (for selects/display) // object specific data (for selects/display)
const AUTOMATION_TYPES = [ const AUTOMATION_TYPES = [
{ value: "FOOD_ALIAS", title: t("Food_Alias") }, {value: "FOOD_ALIAS", title: t("Food_Alias")},
{ value: "UNIT_ALIAS", title: t("Unit_Alias") }, {value: "UNIT_ALIAS", title: t("Unit_Alias")},
{ value: "KEYWORD_ALIAS", title: t("Keyword_Alias") }, {value: "KEYWORD_ALIAS", title: t("Keyword_Alias")},
{ value: "NAME_REPLACE", title: t("Name_Replace") }, {value: "NAME_REPLACE", title: t("Name_Replace")},
{ value: "DESCRIPTION_REPLACE", title: t("Description_Replace") }, {value: "DESCRIPTION_REPLACE", title: t("Description_Replace")},
{ value: "INSTRUCTION_REPLACE", title: t("Instruction_Replace") }, {value: "INSTRUCTION_REPLACE", title: t("Instruction_Replace")},
{ value: "FOOD_REPLACE", title: t("Food_Replace") }, {value: "FOOD_REPLACE", title: t("Food_Replace")},
{ value: "UNIT_REPLACE", title: t("Unit_Replace") }, {value: "UNIT_REPLACE", title: t("Unit_Replace")},
{ value: "NEVER_UNIT", title: t("Never_Unit") }, {value: "NEVER_UNIT", title: t("Never_Unit")},
{ value: "TRANSPOSE_WORDS", title: t("Transpose_Words") } {value: "TRANSPOSE_WORDS", title: t("Transpose_Words")}
] ]
onMounted(() => { onMounted(() => {
setupState(props.item, props.itemId, () => { setupState(props.item, props.itemId, {
editingObj.value.order = 0 newItemFunction: () => {
editingObj.value.order = 0
}
}) })
}) })

View File

@@ -189,9 +189,11 @@ const stopConversionsWatcher = watch(tab, (value, oldValue, onCleanup) => {
onMounted(() => { onMounted(() => {
setupState(props.item, props.itemId, () => { setupState(props.item, props.itemId, {
editingObj.value.propertiesFoodAmount = 100 newItemFunction: () => {
editingObj.value.propertiesFoodUnit = {name: 'g'} as Unit // TODO properly fetch default unit editingObj.value.propertiesFoodAmount = 100
editingObj.value.propertiesFoodUnit = {name: 'g'} as Unit // TODO properly fetch default unit
}
}) })
}) })

View File

@@ -50,9 +50,11 @@ onMounted(() => {
api.apiGroupList().then(r => { api.apiGroupList().then(r => {
groups.value = r groups.value = r
setupState(props.item, props.itemId, () => { setupState(props.item, props.itemId, {
editingObj.value.validUntil = DateTime.now().plus({month: 1}).toJSDate() newItemFunction: () => {
editingObj.value.group = groups.value[0] editingObj.value.validUntil = DateTime.now().plus({month: 1}).toJSDate()
editingObj.value.group = groups.value[0]
}
}) })
}).catch(err => { }).catch(err => {

View File

@@ -73,12 +73,13 @@ import {MessageType, useMessageStore} from "@/stores/MessageStore";
const props = defineProps({ const props = defineProps({
item: {type: {} as PropType<MealPlan>, required: false, default: null}, item: {type: {} as PropType<MealPlan>, required: false, default: null},
itemDefaults: {type: {} as PropType<MealPlan>, required: false, default: {} as MealPlan},
itemId: {type: [Number, String], required: false, default: undefined}, itemId: {type: [Number, String], required: false, default: undefined},
dialog: {type: Boolean, default: false} dialog: {type: Boolean, default: false}
}) })
const emit = defineEmits(['create', 'save', 'delete', 'close']) const emit = defineEmits(['create', 'save', 'delete', 'close'])
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, modelClass} = useModelEditorFunctions<MealPlan>('MealPlan', emit) const {setupState, deleteObject, saveObject, isUpdate, editingObjName, applyItemDefaults, loading, editingObj, modelClass} = useModelEditorFunctions<MealPlan>('MealPlan', emit)
// object specific data (for selects/display) // object specific data (for selects/display)
const dateRangeValue = ref([] as Date[]) const dateRangeValue = ref([] as Date[])
@@ -87,32 +88,35 @@ onMounted(() => {
const api = new ApiApi() const api = new ApiApi()
api.apiMealTypeList().then(r => { api.apiMealTypeList().then(r => {
// TODO remove this once moved to user preference from MealType property
let defaultMealType = {} as MealType let defaultMealType = {} as MealType
r.results.forEach(r => { r.results.forEach(r => {
if (r._default) { if (r._default) {
defaultMealType = r defaultMealType = r
} }
}) })
if (Object.keys(defaultMealType).length == 0 && r.results.length > 0) {
defaultMealType = r.results[0]
}
setupState(props.item, props.itemId, () => { setupState(props.item, props.itemId, {
editingObj.value.fromDate = DateTime.now().toJSDate() newItemFunction: () => {
editingObj.value.toDate = DateTime.now().toJSDate() console.log('running new Item Function')
editingObj.value.shared = useUserPreferenceStore().userSettings.planShare editingObj.value.fromDate = DateTime.now().toJSDate()
editingObj.value.servings = 1 editingObj.value.toDate = DateTime.now().toJSDate()
editingObj.value.mealType = defaultMealType editingObj.value.shared = useUserPreferenceStore().userSettings.planShare
editingObj.value.servings = 1
editingObj.value.mealType = defaultMealType
// initialize date range slider applyItemDefaults(props.itemDefaults)
dateRangeValue.value.push(editingObj.value.fromDate)
}, () => { initializeDateRange()
dateRangeValue.value.push(editingObj.value.fromDate) console.log(editingObj.value)
if(editingObj.value.toDate && editingObj.value.toDate != editingObj.value.fromDate) { }, existingItemFunction: () => {
let currentDate = DateTime.fromJSDate(editingObj.value.fromDate).plus({day: 1}).toJSDate() initializeDateRange()
while(currentDate <= editingObj.value.toDate){
dateRangeValue.value.push(currentDate)
currentDate = DateTime.fromJSDate(currentDate).plus({day: 1}).toJSDate()
}
} }
}) },)
}) })
}) })
@@ -131,6 +135,22 @@ function updateDate() {
} }
} }
/**
* initialize the dateRange selector when the editingObject is initialized
*/
function initializeDateRange() {
if (editingObj.value.toDate && DateTime.fromJSDate(editingObj.value.toDate).diff(DateTime.fromJSDate(editingObj.value.fromDate), 'days').toObject().days! >= 1) {
dateRangeValue.value = [editingObj.value.fromDate]
let currentDate = DateTime.fromJSDate(editingObj.value.fromDate).plus({day: 1}).toJSDate()
while (currentDate <= editingObj.value.toDate) {
dateRangeValue.value.push(currentDate)
currentDate = DateTime.fromJSDate(currentDate).plus({day: 1}).toJSDate()
}
} else {
dateRangeValue.value = [editingObj.value.fromDate, editingObj.value.fromDate]
}
}
</script> </script>
<style scoped> <style scoped>

View File

@@ -122,10 +122,12 @@ onMounted(() => {
api.apiSupermarketCategoryList({pageSize: 100}).then(r => { api.apiSupermarketCategoryList({pageSize: 100}).then(r => {
supermarketCategories.value = r.results supermarketCategories.value = r.results
setupState(props.item, props.itemId, undefined, () => { setupState(props.item, props.itemId, {
editingObj.value.categoryToSupermarket.forEach(cTS => { existingItemFunction: () => {
editingObjSupermarketCategories.value.push(cTS.category) editingObj.value.categoryToSupermarket.forEach(cTS => {
}) editingObjSupermarketCategories.value.push(cTS.category)
})
}
}) })
}) })
}) })

View File

@@ -22,6 +22,15 @@ export function useModelEditorFunctions<T>(modelName: EditorSupportedModels, emi
modelClass.value = getGenericModelFromString(modelName, t) modelClass.value = getGenericModelFromString(modelName, t)
}) })
function applyItemDefaults(itemDefaults: T) {
if (Object.keys(itemDefaults).length > 0) {
Object.keys(itemDefaults).forEach(k => {
console.log('applying default ', k)
editingObj.value[k] = itemDefaults[k]
})
}
}
/** /**
* if given an item or itemId, sets up the editingObj with that item or loads the data from the API using the ID * if given an item or itemId, sets up the editingObj with that item or loads the data from the API using the ID
* once finished loading updates the loading state to false, indicating finished initialization * once finished loading updates the loading state to false, indicating finished initialization
@@ -29,23 +38,35 @@ export function useModelEditorFunctions<T>(modelName: EditorSupportedModels, emi
* @throws Error if an error if neither item or itemId are given and create is disabled * @throws Error if an error if neither item or itemId are given and create is disabled
* @param item item object to set as editingObj * @param item item object to set as editingObj
* @param itemId id of object to be retrieved and set as editingObj * @param itemId id of object to be retrieved and set as editingObj
* @param newItemFunction optional function to execute if no object is given (by either item or itemId) * @param options optional parameters
* @param existingItemFunction optional function to execute once the existing item was loaded (instantly with item, async with itemId) * newItemFunction: called when no item is given. When overriding you must implement applyItemDefaults if you want them to be applied.
* existingItemFunction: called when some kind of item is passed
* @return promise resolving to either the editingObj or undefined if errored * @return promise resolving to either the editingObj or undefined if errored
*/ */
function setupState(item: T | null, itemId: number | string | undefined, function setupState(item: T | null, itemId: number | string | undefined, options: {
newItemFunction: () => void = () => { itemDefaults?: T,
}, newItemFunction?: () => void,
existingItemFunction: () => void = () => { existingItemFunction?: () => void,
}): Promise<T | undefined> { } = {}
): Promise<T | undefined> {
const {
itemDefaults = {} as T,
newItemFunction = () => {
applyItemDefaults(itemDefaults)
},
existingItemFunction = () => {
}
} = options
if (item === null && (itemId === undefined || itemId == '')) { if (item === null && (itemId === undefined || itemId == '')) {
// neither item nor itemId given => new item // neither item nor itemId given => new item
if (modelClass.value.model.disableCreate) { if (modelClass.value.model.disableCreate) {
throw Error('Trying to use a ModelEditor without an item and a model that does not allow object creation!') throw Error('Trying to use a ModelEditor without an item and a model that does not allow object creation!')
} }
newItemFunction() newItemFunction()
loading.value = false loading.value = false
return Promise.resolve(editingObj.value) return Promise.resolve(editingObj.value)
} else if (item !== null) { } else if (item !== null) {
@@ -72,11 +93,12 @@ export function useModelEditorFunctions<T>(modelName: EditorSupportedModels, emi
} else { } else {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err) useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
} }
return undefined return Promise.resolve(undefined)
}).finally(() => { }).finally(() => {
loading.value = false loading.value = false
}) })
} }
return Promise.resolve(undefined)
} }
/** /**
@@ -150,5 +172,5 @@ export function useModelEditorFunctions<T>(modelName: EditorSupportedModels, emi
}) })
} }
return {setupState, saveObject, deleteObject, isUpdate, editingObjName, loading, editingObj, modelClass} return {setupState, saveObject, deleteObject, isUpdate, editingObjName, applyItemDefaults, loading, editingObj, modelClass}
} }