working on select components

This commit is contained in:
vabene1111
2024-03-15 22:38:52 +01:00
committed by smilerz
parent a4225769f6
commit 77a46a4ef6
14 changed files with 514 additions and 145 deletions

View File

@@ -908,12 +908,12 @@ class CommentSerializer(serializers.ModelSerializer):
class RecipeOverviewSerializer(RecipeBaseSerializer): class RecipeOverviewSerializer(RecipeBaseSerializer):
keywords = KeywordLabelSerializer(many=True) keywords = KeywordLabelSerializer(many=True, read_only=True)
new = serializers.SerializerMethodField('is_recipe_new') new = serializers.SerializerMethodField('is_recipe_new', read_only=True)
recent = serializers.ReadOnlyField() recent = serializers.ReadOnlyField()
rating = CustomDecimalField(required=False, allow_null=True) rating = CustomDecimalField(required=False, allow_null=True, read_only=True)
last_cooked = serializers.DateTimeField(required=False, allow_null=True) last_cooked = serializers.DateTimeField(required=False, allow_null=True, read_only=True)
def create(self, validated_data): def create(self, validated_data):
pass pass
@@ -928,7 +928,9 @@ class RecipeOverviewSerializer(RecipeBaseSerializer):
'waiting_time', 'created_by', 'created_at', 'updated_at', 'waiting_time', 'created_by', 'created_at', 'updated_at',
'internal', 'servings', 'servings_text', 'rating', 'last_cooked', 'new', 'recent' 'internal', 'servings', 'servings_text', 'rating', 'last_cooked', 'new', 'recent'
) )
read_only_fields = ['image', 'created_by', 'created_at'] read_only_fields = ['id', 'name', 'description', 'image', 'keywords', 'working_time',
'waiting_time', 'created_by', 'created_at', 'updated_at',
'internal', 'servings', 'servings_text', 'rating', 'last_cooked', 'new', 'recent']
class RecipeSerializer(RecipeBaseSerializer): class RecipeSerializer(RecipeBaseSerializer):

View File

@@ -534,6 +534,17 @@ class SupermarketCategoryRelationViewSet(StandardFilterModelViewSet):
return super().get_queryset() return super().get_queryset()
# TODO make TreeMixin a view type and move schema to view type
@extend_schema_view(
list=extend_schema(
parameters=[
OpenApiParameter(name='query', description='lookup if query string is contained within the name, case insensitive', type=str),
OpenApiParameter(name='updated_at', description='if model has an updated_at timestamp, filter only models updated at or after datetime', type=str), # TODO format hint
OpenApiParameter(name='limit', description='limit number of entries to return', type=str),
OpenApiParameter(name='random', description='randomly orders entries (only works together with limit)', type=str),
]
)
)
class KeywordViewSet(viewsets.ModelViewSet, TreeMixin): class KeywordViewSet(viewsets.ModelViewSet, TreeMixin):
queryset = Keyword.objects queryset = Keyword.objects
model = Keyword model = Keyword
@@ -542,7 +553,7 @@ class KeywordViewSet(viewsets.ModelViewSet, TreeMixin):
pagination_class = DefaultPagination pagination_class = DefaultPagination
class UnitViewSet(viewsets.ModelViewSet, MergeMixin, FuzzyFilterMixin): class UnitViewSet(StandardFilterModelViewSet, MergeMixin):
queryset = Unit.objects queryset = Unit.objects
model = Unit model = Unit
serializer_class = UnitSerializer serializer_class = UnitSerializer

View File

@@ -16,6 +16,7 @@
"mavon-editor": "^3.0.1", "mavon-editor": "^3.0.1",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.4.15", "vue": "^3.4.15",
"vue-multiselect": "^3.0.0-beta.3",
"vue-router": "4", "vue-router": "4",
"vuedraggable": "^4.1.0", "vuedraggable": "^4.1.0",
"vuetify": "^3.5.8" "vuetify": "^3.5.8"

View File

@@ -1,87 +1,65 @@
<template> <template>
<template v-if="allowCreate"> <v-input>
<v-combobox
label="Combobox" <VueMultiselect
:id="id"
v-model="selected_items" v-model="selected_items"
v-model:search="search_query" :options="items"
@update:search="debouncedSearchFunction" :close-on-select="true"
:items="items" :clear-on-select="true"
:loading="search_loading" :hide-selected="multiple"
:hide-no-data="!(allowCreate && search_query != '')" :preserve-search="true"
:internal-search="false"
:limit="limit"
:placeholder="model"
:label="label"
track-by="id"
:multiple="multiple" :multiple="multiple"
:clearable="clearable" :taggable="allowCreate"
item-title="name" tag-placeholder="TODO CREATE PLACEHOLDER"
item-value="id" :loading="search_loading"
:chips="renderAsChips" @search-change="debouncedSearchFunction"
:closable-chips="renderAsChips" @input="selectionChanged"
no-filter @tag="addItem"
@open="search('')"
:disabled="disabled"
> >
</VueMultiselect>
</v-input>
<template #no-data v-if="allowCreate && search_query != '' && !search_loading && multiple">
<v-list-item>
<v-list-item-title>
Press enter to create "<strong>{{ search_query }}</strong>"
</v-list-item-title>
</v-list-item>
</template>
<template v-slot:item="{ item, index, props }">
<v-list-item v-bind="props">
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item>
</template>
<template v-if="renderAsChips" v-slot:chip="{ item, index, props }">
<v-chip closable>{{ item.title }}</v-chip>
</template>
</v-combobox>
</template>
<template v-else>
<v-autocomplete
label="Autocomplete"
:items="items"
:loading="search_loading"
:multiple="multiple"
item-title="name"
item-value="id"
chips
closable-chips
no-filter
@update:search="debouncedSearchFunction"
></v-autocomplete>
</template>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {computed, onMounted, ref, Ref, watch} from 'vue' import {computed, onMounted, ref, Ref,} from 'vue'
import {ApiApi} from "@/openapi/index.js"; import {ApiApi} from "@/openapi/index.js";
import {useDebounceFn} from "@vueuse/core"; import {useDebounceFn} from "@vueuse/core";
import {GenericModel, getModelFromStr} from "@/types/Models";
import VueMultiselect from 'vue-multiselect'
const props = defineProps( const props = defineProps(
{ {
search_on_load: {type: Boolean, default: false}, model: {type: String, required: true},
multiple: {type: Boolean, default: true}, multiple: {type: Boolean, default: true},
limit: {type: Number, default: 25},
allowCreate: {type: Boolean, default: false}, allowCreate: {type: Boolean, default: false},
id: {type: String, required: false, default: Math.random().toString()},
// not verified
search_on_load: {type: Boolean, default: false},
clearable: {type: Boolean, default: false,}, clearable: {type: Boolean, default: false,},
chips: {type: Boolean, default: undefined,}, chips: {type: Boolean, default: undefined,},
itemName: {type: String, default: 'name'}, itemName: {type: String, default: 'name'},
itemValue: {type: String, default: 'id'}, itemValue: {type: String, default: 'id'},
// old props
placeholder: {type: String, default: undefined}, placeholder: {type: String, default: undefined},
model: {
type: String,
required: true,
},
label: {type: String, default: "name"}, label: {type: String, default: "name"},
parent_variable: {type: String, default: undefined}, parent_variable: {type: String, default: undefined},
limit: {type: Number, default: 25},
sticky_options: { sticky_options: {
type: Array, type: Array,
default() { default() {
@@ -104,40 +82,15 @@ const props = defineProps(
} }
) )
const model_class = ref({} as GenericModel<any>)
const items: Ref<Array<any>> = ref([]) const items: Ref<Array<any>> = ref([])
const selected_items: Ref<Array<any> | any> = ref(undefined) const selected_items: Ref<Array<any> | any> = ref(undefined)
const search_query = ref('') const search_query = ref('')
const search_loading = ref(false) const search_loading = ref(false)
const renderAsChips = computed(() => {
if (props.chips != undefined) {
return props.chips
}
return props.multiple
})
watch(selected_items, (new_items, old_items) => {
if (!(new_items instanceof Array) && !(old_items instanceof Array)) {
//TODO detect creation of single selects
} else {
if (old_items == undefined && new_items instanceof Array) {
old_items = []
}
if (new_items == undefined && old_items instanceof Array) {
new_items = []
}
if (old_items.length > new_items.length) {
// item was removed
} else if (old_items.length < new_items.length) {
console.log('items created')
}
}
})
onMounted(() => { onMounted(() => {
model_class.value = getModelFromStr(props.model)
if (props.search_on_load) { if (props.search_on_load) {
debouncedSearchFunction('') debouncedSearchFunction('')
} }
@@ -155,16 +108,15 @@ const debouncedSearchFunction = useDebounceFn((query: string) => {
* @param query input to search for on the API * @param query input to search for on the API
*/ */
function search(query: string) { function search(query: string) {
const api = new ApiApi()
search_loading.value = true search_loading.value = true
api.apiFoodList({query: query}).then(r => { model_class.value.list(query).then(r => {
if (r.results) { items.value = r
items.value = r.results if (props.allowCreate && search_query.value != '') {
if (props.allowCreate && search_query.value != '') { // TODO check if search_query is already in items
// TODO check if search_query is already in items items.value.unshift({id: null, name: `Create "${search_query.value}"`})
items.value.unshift({id: null, name: `Create "${search_query.value}"`})
}
} }
}).catch(err => { }).catch(err => {
//useMessageStore().addMessage(MessageType.ERROR, err, 8000) //useMessageStore().addMessage(MessageType.ERROR, err, 8000)
}).finally(() => { }).finally(() => {
@@ -172,9 +124,34 @@ function search(query: string) {
}) })
} }
function addItem(item: string) {
console.log("CREATEING NEW with -> ", item)
const api = new ApiApi()
api.apiKeywordList()
model_class.value.create(item).then(createdObj => {
//StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_CREATE)
if (selected_items.value instanceof Array) {
selected_items.value.push(createdObj)
} else {
selected_items.value = createdObj
}
items.value.push(createdObj)
selectionChanged()
}).catch((err) => {
//StandardToasts.makeStandardToast(this, StandardToasts.FAIL_CREATE)
}).finally(() => {
search_loading.value = false
})
}
function selectionChanged() {
//this.$emit("change", { var: this.parent_variable, val: this.selected_objects })
}
</script> </script>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
<style scoped> <style scoped>
</style> </style>

View File

@@ -0,0 +1,184 @@
<!-- WIP component that does not really work, that's why vue-multiselect is temporarily used -->
<template>
<template v-if="allowCreate">
<v-combobox
label="Combobox"
v-model="selected_items"
v-model:search="search_query"
@update:search="debouncedSearchFunction"
:items="items"
:loading="search_loading"
:hide-no-data="!(allowCreate && search_query != '')"
:multiple="multiple"
:clearable="clearable"
item-title="name"
item-value="id"
:chips="multiple"
:closable-chips="multiple"
no-filter
>
<template #no-data v-if="allowCreate && search_query != '' && !search_loading && multiple">
<v-list-item>
<v-list-item-title>
Press enter to create "<strong>{{ search_query }}</strong>"
</v-list-item-title>
</v-list-item>
</template>
<!-- <template v-slot:item="{ item, index, props }">-->
<!-- <v-list-item v-bind="props">-->
<!-- </v-list-item>-->
<!-- </template>-->
<template v-if="multiple" v-slot:chip="{ item, index, props }">
<v-chip closable>{{ item.title }}</v-chip>
</template>
</v-combobox>
</template>
<template v-else>
<v-autocomplete
label="Autocomplete"
v-model="selected_items"
v-model:search="search_query"
@update:search="debouncedSearchFunction"
:items="items"
:loading="search_loading"
:hide-no-data="!(allowCreate && search_query != '')"
:multiple="multiple"
:clearable="clearable"
item-title="name"
item-value="id"
:chips="multiple"
:closable-chips="multiple"
no-filter
></v-autocomplete>
</template>
</template>
<script lang="ts" setup>
import {computed, onMounted, PropType, ref, Ref, watch} from 'vue'
import {ApiApi} from "@/openapi/index.js";
import {useDebounceFn} from "@vueuse/core";
import {Models} from "@/types/Models";
import {VAutocomplete, VCombobox} from "vuetify/components";
const props = defineProps(
{
search_on_load: {type: Boolean, default: false},
multiple: {type: Boolean, default: true},
allowCreate: {type: Boolean, default: false},
clearable: {type: Boolean, default: false,},
chips: {type: Boolean, default: undefined,},
itemName: {type: String, default: 'name'},
itemValue: {type: String, default: 'id'},
model: {type: String as PropType<Models>, required: true},
// old props
placeholder: {type: String, default: undefined},
label: {type: String, default: "name"},
parent_variable: {type: String, default: undefined},
limit: {type: Number, default: 25},
sticky_options: {
type: Array,
default() {
return []
},
},
initial_selection: {
type: Array,
default() {
return []
},
},
initial_single_selection: {
type: Object,
default: undefined,
},
disabled: {type: Boolean, default: false,},
}
)
const items: Ref<Array<any>> = ref([])
const selected_items: Ref<Array<any> | any> = ref(undefined)
const search_query = ref('')
const search_loading = ref(false)
/**
* watch selected items to detect if new items were added or old items removed
*/
watch(selected_items, (new_items, old_items) => {
if (!(new_items instanceof Array) && !(old_items instanceof Array)) {
//TODO detect creation of single selects
} else {
if (old_items == undefined && new_items instanceof Array) {
old_items = []
}
if (new_items == undefined && old_items instanceof Array) {
new_items = []
}
if (old_items.length > new_items.length) {
// item was removed
} else if (old_items.length < new_items.length) {
console.log('items created')
}
}
})
onMounted(() => {
if (props.search_on_load) {
debouncedSearchFunction('')
}
})
/**
* debounced search function bound to search input changing
*/
const debouncedSearchFunction = useDebounceFn((query: string) => {
search(query)
}, 300)
/**
* performs the API request to search for the selected input
* @param query input to search for on the API
*/
function search(query: string) {
const api = new ApiApi()
search_loading.value = true
api[`api${props.model}List`]({query: query}).then(r => {
if (r.results) {
items.value = r.results
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 => {
//useMessageStore().addMessage(MessageType.ERROR, err, 8000)
}).finally(() => {
search_loading.value = false
})
}
</script>
<style scoped>
</style>

View File

@@ -16,7 +16,10 @@
label="Step Name" label="Step Name"
></v-text-field> ></v-text-field>
<v-chip-group> <v-chip-group>
<v-chip><i class="fas fa-plus-circle"></i> Time</v-chip> <v-chip v-if="step.time == 0"><i class="fas fa-plus-circle fa-fw mr-1"></i> Time</v-chip>
<v-chip v-if="step.instruction == ''"><i class="fas fa-plus-circle fa-fw mr-1"></i> Instructions</v-chip>
<v-chip v-if="step.file == null"><i class="fas fa-plus-circle fa-fw mr-1"></i> File</v-chip>
<v-chip v-if="step.stepRecipe == null"><i class="fas fa-plus-circle fa-fw mr-1"></i> Recipe</v-chip>
</v-chip-group> </v-chip-group>
<v-table density="compact"> <v-table density="compact">
@@ -34,7 +37,13 @@
<v-form> <v-form>
<v-text-field <v-text-field
label="Amount" label="Amount"
v-model="element.amount" v-model.number="element.amount"
></v-text-field>
<model-select model="Unit" v-model="element.unit" :multiple="false"></model-select>
<model-select model="Food" v-model="element.food" :multiple="false"></model-select>
<v-text-field
label="Note"
v-model="element.note"
></v-text-field> ></v-text-field>
</v-form> </v-form>
@@ -87,10 +96,11 @@ import StepMarkdownEditor from "@/components/inputs/StepMarkdownEditor.vue";
import IngredientsTable from "@/components/display/IngredientsTable.vue"; import IngredientsTable from "@/components/display/IngredientsTable.vue";
import IngredientsTableRow from "@/components/display/IngredientsTableRow.vue"; import IngredientsTableRow from "@/components/display/IngredientsTableRow.vue";
import draggable from "vuedraggable"; import draggable from "vuedraggable";
import ModelSelect from "@/components/inputs/ModelSelect.vue";
export default defineComponent({ export default defineComponent({
name: "StepEditor", name: "StepEditor",
components: {draggable, IngredientsTableRow, IngredientsTable, StepMarkdownEditor}, components: {ModelSelect, draggable, IngredientsTableRow, IngredientsTable, StepMarkdownEditor},
emits: ['update:modelValue'], emits: ['update:modelValue'],
props: { props: {
modelValue: { modelValue: {

View File

@@ -715,8 +715,12 @@ export interface ApiKeywordDestroyRequest {
} }
export interface ApiKeywordListRequest { export interface ApiKeywordListRequest {
limit?: string;
page?: number; page?: number;
pageSize?: number; pageSize?: number;
query?: string;
random?: string;
updatedAt?: string;
} }
export interface ApiKeywordMergeUpdateRequest { export interface ApiKeywordMergeUpdateRequest {
@@ -953,6 +957,11 @@ export interface ApiOpenDataVersionUpdateRequest {
openDataVersion: OpenDataVersion; openDataVersion: OpenDataVersion;
} }
export interface ApiPlanIcalRetrieveRequest {
fromDate: string;
toDate: string;
}
export interface ApiRecipeBookCreateRequest { export interface ApiRecipeBookCreateRequest {
recipeBook: RecipeBook; recipeBook: RecipeBook;
} }
@@ -1349,8 +1358,12 @@ export interface ApiUnitDestroyRequest {
} }
export interface ApiUnitListRequest { export interface ApiUnitListRequest {
limit?: string;
page?: number; page?: number;
pageSize?: number; pageSize?: number;
query?: string;
random?: string;
updatedAt?: string;
} }
export interface ApiUnitMergeUpdateRequest { export interface ApiUnitMergeUpdateRequest {
@@ -4795,6 +4808,10 @@ export class ApiApi extends runtime.BaseAPI {
async apiKeywordListRaw(requestParameters: ApiKeywordListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<PaginatedKeywordList>> { async apiKeywordListRaw(requestParameters: ApiKeywordListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<PaginatedKeywordList>> {
const queryParameters: any = {}; const queryParameters: any = {};
if (requestParameters['limit'] != null) {
queryParameters['limit'] = requestParameters['limit'];
}
if (requestParameters['page'] != null) { if (requestParameters['page'] != null) {
queryParameters['page'] = requestParameters['page']; queryParameters['page'] = requestParameters['page'];
} }
@@ -4803,6 +4820,18 @@ export class ApiApi extends runtime.BaseAPI {
queryParameters['page_size'] = requestParameters['pageSize']; queryParameters['page_size'] = requestParameters['pageSize'];
} }
if (requestParameters['query'] != null) {
queryParameters['query'] = requestParameters['query'];
}
if (requestParameters['random'] != null) {
queryParameters['random'] = requestParameters['random'];
}
if (requestParameters['updatedAt'] != null) {
queryParameters['updated_at'] = requestParameters['updatedAt'];
}
const headerParameters: runtime.HTTPHeaders = {}; const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && (this.configuration.username !== undefined || this.configuration.password !== undefined)) { if (this.configuration && (this.configuration.username !== undefined || this.configuration.password !== undefined)) {
@@ -7047,7 +7076,21 @@ export class ApiApi extends runtime.BaseAPI {
/** /**
*/ */
async apiPlanIcalRetrieveRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<void>> { async apiPlanIcalRetrieveRaw(requestParameters: ApiPlanIcalRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<void>> {
if (requestParameters['fromDate'] == null) {
throw new runtime.RequiredError(
'fromDate',
'Required parameter "fromDate" was null or undefined when calling apiPlanIcalRetrieve().'
);
}
if (requestParameters['toDate'] == null) {
throw new runtime.RequiredError(
'toDate',
'Required parameter "toDate" was null or undefined when calling apiPlanIcalRetrieve().'
);
}
const queryParameters: any = {}; const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {}; const headerParameters: runtime.HTTPHeaders = {};
@@ -7056,7 +7099,7 @@ export class ApiApi extends runtime.BaseAPI {
headerParameters["Authorization"] = "Basic " + btoa(this.configuration.username + ":" + this.configuration.password); headerParameters["Authorization"] = "Basic " + btoa(this.configuration.username + ":" + this.configuration.password);
} }
const response = await this.request({ const response = await this.request({
path: `/api/plan-ical/`, path: `/api/plan-ical/{from_date}/{to_date}/`.replace(`{${"from_date"}}`, encodeURIComponent(String(requestParameters['fromDate']))).replace(`{${"to_date"}}`, encodeURIComponent(String(requestParameters['toDate']))),
method: 'GET', method: 'GET',
headers: headerParameters, headers: headerParameters,
query: queryParameters, query: queryParameters,
@@ -7067,8 +7110,8 @@ export class ApiApi extends runtime.BaseAPI {
/** /**
*/ */
async apiPlanIcalRetrieve(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<void> { async apiPlanIcalRetrieve(requestParameters: ApiPlanIcalRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<void> {
await this.apiPlanIcalRetrieveRaw(initOverrides); await this.apiPlanIcalRetrieveRaw(requestParameters, initOverrides);
} }
/** /**
@@ -10440,6 +10483,10 @@ export class ApiApi extends runtime.BaseAPI {
async apiUnitListRaw(requestParameters: ApiUnitListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<PaginatedUnitList>> { async apiUnitListRaw(requestParameters: ApiUnitListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<PaginatedUnitList>> {
const queryParameters: any = {}; const queryParameters: any = {};
if (requestParameters['limit'] != null) {
queryParameters['limit'] = requestParameters['limit'];
}
if (requestParameters['page'] != null) { if (requestParameters['page'] != null) {
queryParameters['page'] = requestParameters['page']; queryParameters['page'] = requestParameters['page'];
} }
@@ -10448,6 +10495,18 @@ export class ApiApi extends runtime.BaseAPI {
queryParameters['page_size'] = requestParameters['pageSize']; queryParameters['page_size'] = requestParameters['pageSize'];
} }
if (requestParameters['query'] != null) {
queryParameters['query'] = requestParameters['query'];
}
if (requestParameters['random'] != null) {
queryParameters['random'] = requestParameters['random'];
}
if (requestParameters['updatedAt'] != null) {
queryParameters['updated_at'] = requestParameters['updatedAt'];
}
const headerParameters: runtime.HTTPHeaders = {}; const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && (this.configuration.username !== undefined || this.configuration.password !== undefined)) { if (this.configuration && (this.configuration.username !== undefined || this.configuration.password !== undefined)) {

View File

@@ -37,13 +37,13 @@ export interface RecipeOverview {
* @type {string} * @type {string}
* @memberof RecipeOverview * @memberof RecipeOverview
*/ */
name: string; readonly name: string;
/** /**
* *
* @type {string} * @type {string}
* @memberof RecipeOverview * @memberof RecipeOverview
*/ */
description?: string; readonly description: string | null;
/** /**
* *
* @type {string} * @type {string}
@@ -55,19 +55,19 @@ export interface RecipeOverview {
* @type {Array<KeywordLabel>} * @type {Array<KeywordLabel>}
* @memberof RecipeOverview * @memberof RecipeOverview
*/ */
keywords: Array<KeywordLabel>; readonly keywords: Array<KeywordLabel>;
/** /**
* *
* @type {number} * @type {number}
* @memberof RecipeOverview * @memberof RecipeOverview
*/ */
workingTime?: number; readonly workingTime: number;
/** /**
* *
* @type {number} * @type {number}
* @memberof RecipeOverview * @memberof RecipeOverview
*/ */
waitingTime?: number; readonly waitingTime: number;
/** /**
* *
* @type {number} * @type {number}
@@ -91,31 +91,31 @@ export interface RecipeOverview {
* @type {boolean} * @type {boolean}
* @memberof RecipeOverview * @memberof RecipeOverview
*/ */
internal?: boolean; readonly internal: boolean;
/** /**
* *
* @type {number} * @type {number}
* @memberof RecipeOverview * @memberof RecipeOverview
*/ */
servings?: number; readonly servings: number;
/** /**
* *
* @type {string} * @type {string}
* @memberof RecipeOverview * @memberof RecipeOverview
*/ */
servingsText?: string; readonly servingsText: string;
/** /**
* *
* @type {string} * @type {string}
* @memberof RecipeOverview * @memberof RecipeOverview
*/ */
rating?: string; readonly rating: string | null;
/** /**
* *
* @type {Date} * @type {Date}
* @memberof RecipeOverview * @memberof RecipeOverview
*/ */
lastCooked?: Date; readonly lastCooked: Date | null;
/** /**
* *
* @type {string} * @type {string}
@@ -136,11 +136,19 @@ export interface RecipeOverview {
export function instanceOfRecipeOverview(value: object): boolean { export function instanceOfRecipeOverview(value: object): boolean {
if (!('id' in value)) return false; if (!('id' in value)) return false;
if (!('name' in value)) return false; if (!('name' in value)) return false;
if (!('description' in value)) return false;
if (!('image' in value)) return false; if (!('image' in value)) return false;
if (!('keywords' in value)) return false; if (!('keywords' in value)) return false;
if (!('workingTime' in value)) return false;
if (!('waitingTime' in value)) return false;
if (!('createdBy' in value)) return false; if (!('createdBy' in value)) return false;
if (!('createdAt' in value)) return false; if (!('createdAt' in value)) return false;
if (!('updatedAt' in value)) return false; if (!('updatedAt' in value)) return false;
if (!('internal' in value)) return false;
if (!('servings' in value)) return false;
if (!('servingsText' in value)) return false;
if (!('rating' in value)) return false;
if (!('lastCooked' in value)) return false;
if (!('_new' in value)) return false; if (!('_new' in value)) return false;
if (!('recent' in value)) return false; if (!('recent' in value)) return false;
return true; return true;
@@ -158,19 +166,19 @@ export function RecipeOverviewFromJSONTyped(json: any, ignoreDiscriminator: bool
'id': json['id'], 'id': json['id'],
'name': json['name'], 'name': json['name'],
'description': json['description'] == null ? undefined : json['description'], 'description': json['description'],
'image': json['image'], 'image': json['image'],
'keywords': ((json['keywords'] as Array<any>).map(KeywordLabelFromJSON)), 'keywords': ((json['keywords'] as Array<any>).map(KeywordLabelFromJSON)),
'workingTime': json['working_time'] == null ? undefined : json['working_time'], 'workingTime': json['working_time'],
'waitingTime': json['waiting_time'] == null ? undefined : json['waiting_time'], 'waitingTime': json['waiting_time'],
'createdBy': json['created_by'], 'createdBy': json['created_by'],
'createdAt': (new Date(json['created_at'])), 'createdAt': (new Date(json['created_at'])),
'updatedAt': (new Date(json['updated_at'])), 'updatedAt': (new Date(json['updated_at'])),
'internal': json['internal'] == null ? undefined : json['internal'], 'internal': json['internal'],
'servings': json['servings'] == null ? undefined : json['servings'], 'servings': json['servings'],
'servingsText': json['servings_text'] == null ? undefined : json['servings_text'], 'servingsText': json['servings_text'],
'rating': json['rating'] == null ? undefined : json['rating'], 'rating': json['rating'],
'lastCooked': json['last_cooked'] == null ? undefined : (new Date(json['last_cooked'])), 'lastCooked': (json['last_cooked'] == null ? null : new Date(json['last_cooked'])),
'_new': json['new'], '_new': json['new'],
'recent': json['recent'], 'recent': json['recent'],
}; };
@@ -182,16 +190,6 @@ export function RecipeOverviewToJSON(value?: RecipeOverview | null): any {
} }
return { return {
'name': value['name'],
'description': value['description'],
'keywords': ((value['keywords'] as Array<any>).map(KeywordLabelToJSON)),
'working_time': value['workingTime'],
'waiting_time': value['waitingTime'],
'internal': value['internal'],
'servings': value['servings'],
'servings_text': value['servingsText'],
'rating': value['rating'],
'last_cooked': value['lastCooked'] == null ? undefined : ((value['lastCooked'] as any).toISOString()),
}; };
} }

View File

@@ -1,23 +1,36 @@
<template> <template>
<v-container>
<v-row>
<v-col>
<v-card>
<v-card-text>
multiple food
<model-select model="Food" allow-create clearable></model-select>
single food
<model-select model="Food" :multiple="false" allow-create clearable></model-select>
multiple keyowrd
<model-select model="Keyword" allow-create clearable></model-select>
<model-select model="Food" allow-create clearable></model-select> <v-autocomplete></v-autocomplete>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
<model-select model="Food" :multiple="false" allow-create clearable></model-select>
</template> </template>
<script lang="ts"> <script lang="ts">
import {defineComponent} from 'vue' import {defineComponent} from 'vue'
import ModelSelect from "@/components/inputs/ModelSelect.vue"; import ModelSelect from "@/components/inputs/ModelSelect.vue";
export default defineComponent({ export default defineComponent({
name: "MealPlanPage", name: "MealPlanPage",
components: {ModelSelect}, components: {ModelSelect},
data(){ data() {
return { return {}
}
} }
}) })
</script> </script>

View File

@@ -110,7 +110,9 @@ export default defineComponent({
const api = new ApiApi() const api = new ApiApi()
api.apiKeywordList({page: 1, pageSize: 100}).then(r => { api.apiKeywordList({page: 1, pageSize: 100}).then(r => {
this.keywords = r.results if(r.results){
this.keywords = r.results
}
}) })
}, },
methods: { methods: {

102
vue3/src/types/Models.ts Normal file
View File

@@ -0,0 +1,102 @@
import {ApiApi, Keyword as IKeyword, Food as IFood, RecipeOverview as IRecipeOverview, Recipe as IRecipe, Unit as IUnit} from "@/openapi";
export function getModelFromStr(model_name: String) {
switch (model_name.toLowerCase()) {
case 'food': {
return new Food
}
case 'unit': {
return new Unit
}
case 'keyword': {
return new Keyword
}
case 'recipe': {
return new Recipe
}
default: {
throw Error(`Invalid Model ${model_name}, did you forget to register it in Models.ts?`)
}
}
}
export abstract class GenericModel<T> {
abstract list(query: string): Promise<Array<T>>
abstract create(name: string): Promise<T>
}
//TODO this can probably be achieved by manipulating the client generation https://openapi-generator.tech/docs/templating/#models
export class Keyword extends GenericModel<IKeyword> {
create(name: string) {
const api = new ApiApi()
return api.apiKeywordCreate({keyword: {name: name} as IKeyword})
}
list(query: string) {
const api = new ApiApi()
return api.apiKeywordList({query: query}).then(r => {
if (r.results) {
return r.results
} else {
return []
}
})
}
}
export class Food extends GenericModel<IFood> {
create(name: string) {
const api = new ApiApi()
return api.apiFoodCreate({food: {name: name} as IFood})
}
list(query: string) {
const api = new ApiApi()
return api.apiFoodList({query: query}).then(r => {
if (r.results) {
return r.results
} else {
return []
}
})
}
}
export class Unit extends GenericModel<IUnit> {
create(name: string) {
const api = new ApiApi()
return api.apiUnitCreate({unit: {name: name} as IUnit})
}
list(query: string) {
const api = new ApiApi()
return api.apiUnitList({query: query}).then(r => {
if (r.results) {
return r.results
} else {
return []
}
})
}
}
export class Recipe extends GenericModel<IRecipeOverview> {
create(name: string) {
const api = new ApiApi()
return api.apiRecipeCreate({recipe: {name: name} as IRecipe}).then(r => {
return r as unknown as IRecipeOverview
})
}
list(query: string) {
const api = new ApiApi()
return api.apiRecipeList({query: query}).then(r => {
if (r.results) {
return r.results
} else {
return []
}
})
}
}

View File

@@ -7,6 +7,11 @@ import {createVuetify} from 'vuetify'
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides // https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
export default createVuetify({ export default createVuetify({
defaults: {
VCard: {
class: 'overflow-visible' // this is needed so that vue-multiselect options show above a card, vuetify uses overlay container to avoid this
}
},
theme: { theme: {
defaultTheme: 'light', defaultTheme: 'light',
themes: { themes: {

View File

@@ -1,6 +1,6 @@
{ {
"extends": "@vue/tsconfig/tsconfig.dom.json", "extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"], "include": ["env.d.ts", "src/**/*", "src/**/*.vue","src/**/*.ts"],
"exclude": ["src/**/__tests__/*"], "exclude": ["src/**/__tests__/*"],
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,

View File

@@ -889,6 +889,11 @@ vue-demi@>=0.14.5, vue-demi@>=0.14.7:
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.7.tgz#8317536b3ef74c5b09f268f7782e70194567d8f2" resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.7.tgz#8317536b3ef74c5b09f268f7782e70194567d8f2"
integrity sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA== integrity sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==
vue-multiselect@^3.0.0-beta.3:
version "3.0.0-beta.3"
resolved "https://registry.yarnpkg.com/vue-multiselect/-/vue-multiselect-3.0.0-beta.3.tgz#b1348238a84c435582c3f46f2a9c045b29bb976c"
integrity sha512-P7Fx+ovVF7WMERSZ0lw6N3p4H4bnQ3NcaY3ORjzFPv0r/6lpIqvFWmK9Xnwze9mgAvmNV1foI1VWrBmjnfBTLQ==
vue-router@4: vue-router@4:
version "4.2.5" version "4.2.5"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.2.5.tgz#b9e3e08f1bd9ea363fdd173032620bc50cf0e98a" resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.2.5.tgz#b9e3e08f1bd9ea363fdd173032620bc50cf0e98a"