From e12c83faf1a6ded8097d13c3e391a9b8b7acc4a7 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Fri, 15 Mar 2024 22:38:52 +0100 Subject: [PATCH] working on select components --- cookbook/serializer.py | 12 +- cookbook/views/api.py | 13 +- vue3/package.json | 1 + vue3/src/components/inputs/ModelSelect.vue | 167 +++++++--------- .../components/inputs/ModelSelectVuetify.vue | 184 ++++++++++++++++++ vue3/src/components/inputs/StepEditor.vue | 16 +- vue3/src/openapi/apis/ApiApi.ts | 67 ++++++- vue3/src/openapi/models/RecipeOverview.ts | 54 +++-- vue3/src/pages/MealPlanPage.vue | 29 ++- vue3/src/pages/RecipeEditPage.vue | 4 +- vue3/src/types/Models.ts | 102 ++++++++++ vue3/src/vuetify.ts | 5 + vue3/tsconfig.app.json | 2 +- vue3/yarn.lock | 5 + 14 files changed, 515 insertions(+), 146 deletions(-) create mode 100644 vue3/src/components/inputs/ModelSelectVuetify.vue create mode 100644 vue3/src/types/Models.ts diff --git a/cookbook/serializer.py b/cookbook/serializer.py index 42cc993574..f0090b4ba7 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -908,12 +908,12 @@ class Meta: class RecipeOverviewSerializer(RecipeBaseSerializer): - keywords = KeywordLabelSerializer(many=True) - new = serializers.SerializerMethodField('is_recipe_new') + keywords = KeywordLabelSerializer(many=True, read_only=True) + new = serializers.SerializerMethodField('is_recipe_new', read_only=True) recent = serializers.ReadOnlyField() - rating = CustomDecimalField(required=False, allow_null=True) - last_cooked = serializers.DateTimeField(required=False, allow_null=True) + rating = CustomDecimalField(required=False, allow_null=True, read_only=True) + last_cooked = serializers.DateTimeField(required=False, allow_null=True, read_only=True) def create(self, validated_data): pass @@ -928,7 +928,9 @@ class Meta: 'waiting_time', 'created_by', 'created_at', 'updated_at', '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): diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 1715ce1a1e..3a177a8e24 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -534,6 +534,17 @@ def get_queryset(self): 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): queryset = Keyword.objects model = Keyword @@ -542,7 +553,7 @@ class KeywordViewSet(viewsets.ModelViewSet, TreeMixin): pagination_class = DefaultPagination -class UnitViewSet(viewsets.ModelViewSet, MergeMixin, FuzzyFilterMixin): +class UnitViewSet(StandardFilterModelViewSet, MergeMixin): queryset = Unit.objects model = Unit serializer_class = UnitSerializer diff --git a/vue3/package.json b/vue3/package.json index bb0f8448a8..953ba59d23 100644 --- a/vue3/package.json +++ b/vue3/package.json @@ -16,6 +16,7 @@ "mavon-editor": "^3.0.1", "pinia": "^2.1.7", "vue": "^3.4.15", + "vue-multiselect": "^3.0.0-beta.3", "vue-router": "4", "vuedraggable": "^4.1.0", "vuetify": "^3.5.8" diff --git a/vue3/src/components/inputs/ModelSelect.vue b/vue3/src/components/inputs/ModelSelect.vue index 1c51909b2d..78db3f463d 100644 --- a/vue3/src/components/inputs/ModelSelect.vue +++ b/vue3/src/components/inputs/ModelSelect.vue @@ -1,87 +1,65 @@ +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 }) +} + + + \ No newline at end of file diff --git a/vue3/src/components/inputs/ModelSelectVuetify.vue b/vue3/src/components/inputs/ModelSelectVuetify.vue new file mode 100644 index 0000000000..6b32e4fd38 --- /dev/null +++ b/vue3/src/components/inputs/ModelSelectVuetify.vue @@ -0,0 +1,184 @@ + + + + + + + + \ No newline at end of file diff --git a/vue3/src/components/inputs/StepEditor.vue b/vue3/src/components/inputs/StepEditor.vue index 29acaea222..566ef0acf1 100644 --- a/vue3/src/components/inputs/StepEditor.vue +++ b/vue3/src/components/inputs/StepEditor.vue @@ -16,7 +16,10 @@ label="Step Name" > - Time + Time + Instructions + File + Recipe @@ -34,7 +37,13 @@ + + + @@ -87,10 +96,11 @@ import StepMarkdownEditor from "@/components/inputs/StepMarkdownEditor.vue"; import IngredientsTable from "@/components/display/IngredientsTable.vue"; import IngredientsTableRow from "@/components/display/IngredientsTableRow.vue"; import draggable from "vuedraggable"; +import ModelSelect from "@/components/inputs/ModelSelect.vue"; export default defineComponent({ name: "StepEditor", - components: {draggable, IngredientsTableRow, IngredientsTable, StepMarkdownEditor}, + components: {ModelSelect, draggable, IngredientsTableRow, IngredientsTable, StepMarkdownEditor}, emits: ['update:modelValue'], props: { modelValue: { diff --git a/vue3/src/openapi/apis/ApiApi.ts b/vue3/src/openapi/apis/ApiApi.ts index 9f633233ee..8c9bfb70e3 100644 --- a/vue3/src/openapi/apis/ApiApi.ts +++ b/vue3/src/openapi/apis/ApiApi.ts @@ -715,8 +715,12 @@ export interface ApiKeywordDestroyRequest { } export interface ApiKeywordListRequest { + limit?: string; page?: number; pageSize?: number; + query?: string; + random?: string; + updatedAt?: string; } export interface ApiKeywordMergeUpdateRequest { @@ -953,6 +957,11 @@ export interface ApiOpenDataVersionUpdateRequest { openDataVersion: OpenDataVersion; } +export interface ApiPlanIcalRetrieveRequest { + fromDate: string; + toDate: string; +} + export interface ApiRecipeBookCreateRequest { recipeBook: RecipeBook; } @@ -1349,8 +1358,12 @@ export interface ApiUnitDestroyRequest { } export interface ApiUnitListRequest { + limit?: string; page?: number; pageSize?: number; + query?: string; + random?: string; + updatedAt?: string; } export interface ApiUnitMergeUpdateRequest { @@ -4795,6 +4808,10 @@ export class ApiApi extends runtime.BaseAPI { async apiKeywordListRaw(requestParameters: ApiKeywordListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { const queryParameters: any = {}; + if (requestParameters['limit'] != null) { + queryParameters['limit'] = requestParameters['limit']; + } + if (requestParameters['page'] != null) { queryParameters['page'] = requestParameters['page']; } @@ -4803,6 +4820,18 @@ export class ApiApi extends runtime.BaseAPI { 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 = {}; 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> { + async apiPlanIcalRetrieveRaw(requestParameters: ApiPlanIcalRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + 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 headerParameters: runtime.HTTPHeaders = {}; @@ -7056,7 +7099,7 @@ export class ApiApi extends runtime.BaseAPI { headerParameters["Authorization"] = "Basic " + btoa(this.configuration.username + ":" + this.configuration.password); } 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', headers: headerParameters, query: queryParameters, @@ -7067,8 +7110,8 @@ export class ApiApi extends runtime.BaseAPI { /** */ - async apiPlanIcalRetrieve(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { - await this.apiPlanIcalRetrieveRaw(initOverrides); + async apiPlanIcalRetrieve(requestParameters: ApiPlanIcalRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + await this.apiPlanIcalRetrieveRaw(requestParameters, initOverrides); } /** @@ -10440,6 +10483,10 @@ export class ApiApi extends runtime.BaseAPI { async apiUnitListRaw(requestParameters: ApiUnitListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { const queryParameters: any = {}; + if (requestParameters['limit'] != null) { + queryParameters['limit'] = requestParameters['limit']; + } + if (requestParameters['page'] != null) { queryParameters['page'] = requestParameters['page']; } @@ -10448,6 +10495,18 @@ export class ApiApi extends runtime.BaseAPI { 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 = {}; if (this.configuration && (this.configuration.username !== undefined || this.configuration.password !== undefined)) { diff --git a/vue3/src/openapi/models/RecipeOverview.ts b/vue3/src/openapi/models/RecipeOverview.ts index 7474562d42..b9cfcd3937 100644 --- a/vue3/src/openapi/models/RecipeOverview.ts +++ b/vue3/src/openapi/models/RecipeOverview.ts @@ -37,13 +37,13 @@ export interface RecipeOverview { * @type {string} * @memberof RecipeOverview */ - name: string; + readonly name: string; /** * * @type {string} * @memberof RecipeOverview */ - description?: string; + readonly description: string | null; /** * * @type {string} @@ -55,19 +55,19 @@ export interface RecipeOverview { * @type {Array} * @memberof RecipeOverview */ - keywords: Array; + readonly keywords: Array; /** * * @type {number} * @memberof RecipeOverview */ - workingTime?: number; + readonly workingTime: number; /** * * @type {number} * @memberof RecipeOverview */ - waitingTime?: number; + readonly waitingTime: number; /** * * @type {number} @@ -91,31 +91,31 @@ export interface RecipeOverview { * @type {boolean} * @memberof RecipeOverview */ - internal?: boolean; + readonly internal: boolean; /** * * @type {number} * @memberof RecipeOverview */ - servings?: number; + readonly servings: number; /** * * @type {string} * @memberof RecipeOverview */ - servingsText?: string; + readonly servingsText: string; /** * * @type {string} * @memberof RecipeOverview */ - rating?: string; + readonly rating: string | null; /** * * @type {Date} * @memberof RecipeOverview */ - lastCooked?: Date; + readonly lastCooked: Date | null; /** * * @type {string} @@ -136,11 +136,19 @@ export interface RecipeOverview { export function instanceOfRecipeOverview(value: object): boolean { if (!('id' in value)) return false; if (!('name' in value)) return false; + if (!('description' in value)) return false; if (!('image' 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 (!('createdAt' 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 (!('recent' in value)) return false; return true; @@ -158,19 +166,19 @@ export function RecipeOverviewFromJSONTyped(json: any, ignoreDiscriminator: bool 'id': json['id'], 'name': json['name'], - 'description': json['description'] == null ? undefined : json['description'], + 'description': json['description'], 'image': json['image'], 'keywords': ((json['keywords'] as Array).map(KeywordLabelFromJSON)), - 'workingTime': json['working_time'] == null ? undefined : json['working_time'], - 'waitingTime': json['waiting_time'] == null ? undefined : json['waiting_time'], + 'workingTime': json['working_time'], + 'waitingTime': json['waiting_time'], 'createdBy': json['created_by'], 'createdAt': (new Date(json['created_at'])), 'updatedAt': (new Date(json['updated_at'])), - 'internal': json['internal'] == null ? undefined : json['internal'], - 'servings': json['servings'] == null ? undefined : json['servings'], - 'servingsText': json['servings_text'] == null ? undefined : json['servings_text'], - 'rating': json['rating'] == null ? undefined : json['rating'], - 'lastCooked': json['last_cooked'] == null ? undefined : (new Date(json['last_cooked'])), + 'internal': json['internal'], + 'servings': json['servings'], + 'servingsText': json['servings_text'], + 'rating': json['rating'], + 'lastCooked': (json['last_cooked'] == null ? null : new Date(json['last_cooked'])), '_new': json['new'], 'recent': json['recent'], }; @@ -182,16 +190,6 @@ export function RecipeOverviewToJSON(value?: RecipeOverview | null): any { } return { - 'name': value['name'], - 'description': value['description'], - 'keywords': ((value['keywords'] as Array).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()), }; } diff --git a/vue3/src/pages/MealPlanPage.vue b/vue3/src/pages/MealPlanPage.vue index b69686ee45..f384aba177 100644 --- a/vue3/src/pages/MealPlanPage.vue +++ b/vue3/src/pages/MealPlanPage.vue @@ -1,9 +1,23 @@ @@ -11,13 +25,12 @@ import {defineComponent} from 'vue' import ModelSelect from "@/components/inputs/ModelSelect.vue"; + export default defineComponent({ name: "MealPlanPage", components: {ModelSelect}, - data(){ - return { - - } + data() { + return {} } }) diff --git a/vue3/src/pages/RecipeEditPage.vue b/vue3/src/pages/RecipeEditPage.vue index 77a5c9981b..63a1c24cba 100644 --- a/vue3/src/pages/RecipeEditPage.vue +++ b/vue3/src/pages/RecipeEditPage.vue @@ -110,7 +110,9 @@ export default defineComponent({ const api = new ApiApi() api.apiKeywordList({page: 1, pageSize: 100}).then(r => { - this.keywords = r.results + if(r.results){ + this.keywords = r.results + } }) }, methods: { diff --git a/vue3/src/types/Models.ts b/vue3/src/types/Models.ts new file mode 100644 index 0000000000..6c54a1f565 --- /dev/null +++ b/vue3/src/types/Models.ts @@ -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 { + abstract list(query: string): Promise> + + abstract create(name: string): Promise +} + +//TODO this can probably be achieved by manipulating the client generation https://openapi-generator.tech/docs/templating/#models +export class Keyword extends GenericModel { + 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 { + 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 { + 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 { + 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 [] + } + }) + } +} \ No newline at end of file diff --git a/vue3/src/vuetify.ts b/vue3/src/vuetify.ts index 23fc01714b..916179b29b 100644 --- a/vue3/src/vuetify.ts +++ b/vue3/src/vuetify.ts @@ -7,6 +7,11 @@ import {createVuetify} from 'vuetify' // https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides 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: { defaultTheme: 'light', themes: { diff --git a/vue3/tsconfig.app.json b/vue3/tsconfig.app.json index ff68a8e4fc..283aac7f20 100644 --- a/vue3/tsconfig.app.json +++ b/vue3/tsconfig.app.json @@ -1,6 +1,6 @@ { "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__/*"], "compilerOptions": { "composite": true, diff --git a/vue3/yarn.lock b/vue3/yarn.lock index fd25408c6f..6d4a414db4 100644 --- a/vue3/yarn.lock +++ b/vue3/yarn.lock @@ -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" 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: version "4.2.5" resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.2.5.tgz#b9e3e08f1bd9ea363fdd173032620bc50cf0e98a"