fix: Problemen met PUT op leerpaden en verschillende kleinere problemen
This commit is contained in:
		
							parent
							
								
									2db5d77296
								
							
						
					
					
						commit
						96821c40ab
					
				
					 21 changed files with 205 additions and 103 deletions
				
			
		|  | @ -1,8 +1,8 @@ | |||
| import { BaseController } from "@/controllers/base-controller.ts"; | ||||
| import { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; | ||||
| import type { Language } from "@/data-objects/language.ts"; | ||||
| import { LearningPath } from "@/data-objects/learning-paths/learning-path"; | ||||
| import { single } from "@/utils/response-assertions.ts"; | ||||
| import type { LearningPathDTO } from "@/data-objects/learning-paths/learning-path-dto.ts"; | ||||
| import type { LearningPath as LearningPathDTO } from "@dwengo-1/common/interfaces/learning-content"; | ||||
| 
 | ||||
| export class LearningPathController extends BaseController { | ||||
|     constructor() { | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import type { Language } from "@/data-objects/language.ts"; | ||||
| import type { LearningPathNodeDTO } from "@/data-objects/learning-paths/learning-path.ts"; | ||||
| import type { LearningObjectNode as LearningPathNodeDTO } from "@dwengo-1/common/interfaces/learning-content"; | ||||
| 
 | ||||
| export class LearningPathNode { | ||||
|     public readonly learningobjectHruid: string; | ||||
|  | @ -14,7 +14,7 @@ export class LearningPathNode { | |||
|         learningobjectHruid: string; | ||||
|         version: number; | ||||
|         language: Language; | ||||
|         transitions: { next: LearningPathNode; default: boolean }[]; | ||||
|         transitions: { next: LearningPathNode; default?: boolean }[]; | ||||
|         createdAt: Date; | ||||
|         updatedAt: Date; | ||||
|         done?: boolean; | ||||
|  | @ -22,7 +22,7 @@ export class LearningPathNode { | |||
|         this.learningobjectHruid = options.learningobjectHruid; | ||||
|         this.version = options.version; | ||||
|         this.language = options.language; | ||||
|         this.transitions = options.transitions; | ||||
|         this.transitions = options.transitions.map(it => ({ next: it.next, default: it.default ?? false })); | ||||
|         this.createdAt = options.createdAt; | ||||
|         this.updatedAt = options.updatedAt; | ||||
|         this.done = options.done || false; | ||||
|  | @ -50,8 +50,8 @@ export class LearningPathNode { | |||
|                     return undefined; | ||||
|                 }) | ||||
|                 .filter((it) => it !== undefined), | ||||
|             createdAt: new Date(dto.created_at), | ||||
|             updatedAt: new Date(dto.updatedAt), | ||||
|             createdAt: dto.created_at ? new Date(dto.created_at) : new Date(), | ||||
|             updatedAt: dto.updatedAt ? new Date(dto.updatedAt) : new Date(), | ||||
|             done: dto.done, | ||||
|         }); | ||||
|     } | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import type { Language } from "@/data-objects/language.ts"; | ||||
| import { LearningPathNode } from "@/data-objects/learning-paths/learning-path-node.ts"; | ||||
| import type { LearningPathDTO } from "@/data-objects/learning-paths/learning-path-dto.ts"; | ||||
| import type { LearningObjectNode, LearningPath as LearningPathDTO } from "@dwengo-1/common/interfaces/learning-content"; | ||||
| 
 | ||||
| export interface LearningPathNodeDTO { | ||||
|     _id: string; | ||||
|  | @ -77,16 +77,19 @@ export class LearningPath { | |||
|             hruid: dto.hruid, | ||||
|             title: dto.title, | ||||
|             description: dto.description, | ||||
|             amountOfNodes: dto.num_nodes, | ||||
|             amountOfNodesLeft: dto.num_nodes_left, | ||||
|             amountOfNodes: dto.num_nodes ?? dto.nodes.length, | ||||
|             amountOfNodesLeft: dto.num_nodes_left ?? dto.nodes.length, | ||||
|             keywords: dto.keywords.split(" "), | ||||
|             targetAges: { min: dto.min_age, max: dto.max_age }, | ||||
|             targetAges: { | ||||
|                 min: dto.min_age ?? NaN, | ||||
|                 max: dto.max_age ?? NaN | ||||
|             }, | ||||
|             startNode: LearningPathNode.fromDTOAndOtherNodes(LearningPath.getStartNode(dto), dto.nodes), | ||||
|             image: dto.image, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     static getStartNode(dto: LearningPathDTO): LearningPathNodeDTO { | ||||
|     static getStartNode(dto: LearningPathDTO): LearningObjectNode { | ||||
|         const startNodeDtos = dto.nodes.filter((it) => it.start_node === true); | ||||
|         if (startNodeDtos.length < 1) { | ||||
|             // The learning path has no starting node -> use the first node.
 | ||||
|  |  | |||
|  | @ -134,5 +134,9 @@ | |||
|     "invalidZip": "This is not a valid zip file.", | ||||
|     "emptyZip": "This zip file is empty", | ||||
|     "missingMetadata": "This learning object is missing a metadata.json file.", | ||||
|     "missingContent": "This learning object is missing a content.* file." | ||||
|     "missingContent": "This learning object is missing a content.* file.", | ||||
|     "open": "open", | ||||
|     "editLearningPath": "Edit learning path", | ||||
|     "newLearningPath": "Create a new learning path", | ||||
|     "saveChanges": "Save changes" | ||||
| } | ||||
|  |  | |||
|  | @ -2,9 +2,9 @@ import { type MaybeRefOrGetter, toValue } from "vue"; | |||
| import type { Language } from "@/data-objects/language.ts"; | ||||
| import { useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; | ||||
| import { getLearningPathController } from "@/controllers/controllers"; | ||||
| import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; | ||||
| import type { AxiosError } from "axios"; | ||||
| import type { LearningPathDTO } from "@/data-objects/learning-paths/learning-path-dto"; | ||||
| import type { LearningPath as LearningPathDTO } from "@dwengo-1/common/interfaces/learning-content"; | ||||
| import type { LearningPath } from "@/data-objects/learning-paths/learning-path"; | ||||
| 
 | ||||
| export const LEARNING_PATH_KEY = "learningPath"; | ||||
| const learningPathController = getLearningPathController(); | ||||
|  |  | |||
|  | @ -106,6 +106,12 @@ const router = createRouter({ | |||
|             component: SingleDiscussion, | ||||
|             meta: { requiresAuth: true }, | ||||
|         }, | ||||
|         { | ||||
|             path: "/my-content", | ||||
|             name: "OwnLearningContentPage", | ||||
|             component: OwnLearningContentPage, | ||||
|             meta: { requiresAuth: true } | ||||
|         }, | ||||
|         { | ||||
|             path: "/learningPath", | ||||
|             children: [ | ||||
|  | @ -115,18 +121,12 @@ const router = createRouter({ | |||
|                     component: LearningPathSearchPage, | ||||
|                     meta: { requiresAuth: true }, | ||||
|                 }, | ||||
|                 { | ||||
|                     path: "my", | ||||
|                     name: "OwnLearningContentPage", | ||||
|                     component: OwnLearningContentPage, | ||||
|                     meta: { requiresAuth: true } | ||||
|                 }, | ||||
|                 { | ||||
|                     path: ":hruid/:language/:learningObjectHruid", | ||||
|                     name: "LearningPath", | ||||
|                     component: LearningPathPage, | ||||
|                     props: true, | ||||
|                     meta: { requiresAuth: true }, | ||||
|                     meta: { requiresAuth: true } | ||||
|                 }, | ||||
|             ], | ||||
|         }, | ||||
|  |  | |||
|  | @ -247,7 +247,7 @@ | |||
|                     </template> | ||||
|                 </v-list-item> | ||||
|                 <v-divider></v-divider> | ||||
|                 <div v-if="props.learningObjectHruid"> | ||||
|                 <div> | ||||
|                     <using-query-result | ||||
|                         :query-result="learningObjectListQueryResult" | ||||
|                         v-slot="learningObjects: { data: LearningObject[] }" | ||||
|  |  | |||
|  | @ -6,9 +6,9 @@ | |||
|     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||
|     import type { LearningObject } from "@/data-objects/learning-objects/learning-object"; | ||||
|     import { ref, type Ref } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { useGetAllLearningPathsByAdminQuery } from "@/queries/learning-paths"; | ||||
| import type { LearningPathDTO } from "@/data-objects/learning-paths/learning-path-dto"; | ||||
|     import { useI18n } from "vue-i18n"; | ||||
|     import { useGetAllLearningPathsByAdminQuery } from "@/queries/learning-paths"; | ||||
|     import type { LearningPath as LearningPathDTO } from "@dwengo-1/common/interfaces/learning-content"; | ||||
| 
 | ||||
|     const { t } = useI18n(); | ||||
| 
 | ||||
|  | @ -43,7 +43,7 @@ import type { LearningPathDTO } from "@/data-objects/learning-paths/learning-pat | |||
|                     :query-result="learningPathsQuery" | ||||
|                     v-slot="response: { data: LearningPathDTO[] }" | ||||
|                 > | ||||
|                     <own-learning-paths-view :learning-paths="response.data"/> | ||||
|                     <own-learning-paths-view :learningPaths="response.data"/> | ||||
|                 </using-query-result> | ||||
|             </v-tabs-window-item> | ||||
|         </v-tabs-window> | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
|     import type { LearningObject } from '@/data-objects/learning-objects/learning-object'; | ||||
|     import LearningObjectUploadButton from '@/views/own-learning-content/learning-objects/LearningObjectUploadButton.vue' | ||||
|     import LearningObjectPreviewCard from './LearningObjectPreviewCard.vue'; | ||||
|     import { computed, ref, type Ref } from 'vue'; | ||||
|     import { computed, ref, watch, type Ref } from 'vue'; | ||||
|     import { useI18n } from 'vue-i18n'; | ||||
| 
 | ||||
|     const { t } = useI18n(); | ||||
|  | @ -19,6 +19,8 @@ | |||
| 
 | ||||
|     const selectedLearningObjects: Ref<LearningObject[]> = ref([]); | ||||
| 
 | ||||
|     watch(() => props.learningObjects, () => selectedLearningObjects.value = []); | ||||
| 
 | ||||
|     const selectedLearningObject = computed(() => | ||||
|         selectedLearningObjects.value ? selectedLearningObjects.value[0] : undefined | ||||
|     ); | ||||
|  | @ -27,16 +29,20 @@ | |||
| 
 | ||||
| <template> | ||||
|     <div class="root"> | ||||
|         <v-data-table | ||||
|             class="table" | ||||
|             v-model="selectedLearningObjects" | ||||
|             :items="props.learningObjects" | ||||
|             :headers="tableHeaders" | ||||
|             select-strategy="single" | ||||
|             show-select | ||||
|             return-object | ||||
|         /> | ||||
|         <learning-object-preview-card class="preview" :selectedLearningObject="selectedLearningObject"/> | ||||
|         <div class="table-container"> | ||||
|             <v-data-table | ||||
|                 class="table" | ||||
|                 v-model="selectedLearningObjects" | ||||
|                 :items="props.learningObjects" | ||||
|                 :headers="tableHeaders" | ||||
|                 select-strategy="single" | ||||
|                 show-select | ||||
|                 return-object | ||||
|             /> | ||||
|         </div> | ||||
|         <div class="preview-container" v-if="selectedLearningObject"> | ||||
|             <learning-object-preview-card class="preview" :selectedLearningObject="selectedLearningObject"/> | ||||
|         </div> | ||||
|     </div> | ||||
|     <div class="fab"> | ||||
|         <learning-object-upload-button/> | ||||
|  | @ -55,11 +61,17 @@ | |||
|         padding: 20px; | ||||
|         flex-wrap: wrap; | ||||
|     } | ||||
|     .preview { | ||||
|     .preview-container { | ||||
|         flex: 1; | ||||
|         min-width: 400px; | ||||
|     } | ||||
|     .table { | ||||
|     .table-container { | ||||
|         flex: 1; | ||||
|     } | ||||
|     .preview { | ||||
|         width: 100%; | ||||
|     } | ||||
|     .table { | ||||
|         width: 100%; | ||||
|     } | ||||
| </style> | ||||
|  |  | |||
|  | @ -1,48 +1,80 @@ | |||
| <script setup lang="ts"> | ||||
|     import UsingQueryResult from '@/components/UsingQueryResult.vue'; | ||||
|     import { useI18n } from 'vue-i18n'; | ||||
|     import type { LearningPathDTO } from '@/data-objects/learning-paths/learning-path-dto'; | ||||
|     import { computed, ref, watch } from 'vue'; | ||||
|     import { computed, ref, watch, type Ref } from 'vue'; | ||||
|     import JsonEditorVue from 'json-editor-vue' | ||||
| import { useMutation } from '@tanstack/vue-query'; | ||||
| import { usePostLearningPathMutation, usePutLearningPathMutation } from '@/queries/learning-paths'; | ||||
|     import { useDeleteLearningPathMutation, usePostLearningPathMutation, usePutLearningPathMutation } from '@/queries/learning-paths'; | ||||
|     import { Language } from '@/data-objects/language'; | ||||
|     import type { LearningPath } from '@dwengo-1/common/interfaces/learning-content'; | ||||
|     import type { AxiosError } from 'axios'; | ||||
| 
 | ||||
|     const { t } = useI18n(); | ||||
| 
 | ||||
|     const props = defineProps<{ | ||||
|         selectedLearningPath?: LearningPathDTO | ||||
|         selectedLearningPath?: LearningPath | ||||
|     }>(); | ||||
| 
 | ||||
|     const INDENT = 4; | ||||
|     const DEFAULT_LEARNING_PATH: LearningPathDTO = { | ||||
|         language: '', | ||||
|         hruid: '', | ||||
|         title: '', | ||||
|         description: '', | ||||
|         num_nodes: 0, | ||||
|         num_nodes_left: 0, | ||||
|         nodes: [], | ||||
|         keywords: '', | ||||
|         target_ages: [], | ||||
|         min_age: 0, | ||||
|         max_age: 0, | ||||
|         __order: 0 | ||||
|     const { isPending, mutate, error: deleteError, isSuccess: deleteSuccess } = useDeleteLearningPathMutation(); | ||||
| 
 | ||||
|     const DEFAULT_LEARNING_PATH: LearningPath = { | ||||
|         language: 'en', | ||||
|         hruid: '...', | ||||
|         title: '...', | ||||
|         description: '...', | ||||
|         nodes: [ | ||||
|             { | ||||
|                 learningobject_hruid: '...', | ||||
|                 language: Language.English, | ||||
|                 version: 1, | ||||
|                 start_node: true, | ||||
|                 transitions: [ | ||||
|                     { | ||||
|                         default: true, | ||||
|                         condition: "(remove if the transition should be unconditinal)", | ||||
|                         next: { | ||||
|                             hruid: '...', | ||||
|                             version: 1, | ||||
|                             language: '...' | ||||
|                         } | ||||
|                     } | ||||
|                 ] | ||||
|             } | ||||
|         ], | ||||
|         keywords: 'Keywords separated by spaces', | ||||
|         target_ages: [] | ||||
|     } | ||||
| 
 | ||||
|     const { isPending: isPostPending, mutate: doPost } = usePostLearningPathMutation(); | ||||
|     const { isPending: isPutPending, mutate: doPut } = usePutLearningPathMutation(); | ||||
|     const { isPending: isPostPending, error: postError, mutate: doPost } = usePostLearningPathMutation(); | ||||
|     const { isPending: isPutPending, error: putError, mutate: doPut } = usePutLearningPathMutation(); | ||||
| 
 | ||||
|     const learningPath = ref(DEFAULT_LEARNING_PATH); | ||||
|     const learningPath: Ref<LearningPath | string> = ref(DEFAULT_LEARNING_PATH); | ||||
| 
 | ||||
|     const parsedLearningPath = computed(() => | ||||
|         typeof learningPath.value === "string" ? JSON.parse(learningPath.value) as LearningPath | ||||
|                                                : learningPath.value | ||||
|     ); | ||||
| 
 | ||||
|     watch(() => props.selectedLearningPath, () => learningPath.value = props.selectedLearningPath ?? DEFAULT_LEARNING_PATH); | ||||
| 
 | ||||
|     function uploadLearningPath(): void { | ||||
|         if (props.selectedLearningPath) { | ||||
|             doPut({ learningPath: learningPath.value }); | ||||
|             doPut({ learningPath: parsedLearningPath.value }); | ||||
|         } else { | ||||
|             doPost({ learningPath: learningPath.value }); | ||||
|             doPost({ learningPath: parsedLearningPath.value }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     function deleteLearningObject(): void { | ||||
|         if (props.selectedLearningPath) { | ||||
|             mutate({ | ||||
|                 hruid: props.selectedLearningPath.hruid, | ||||
|                 language: props.selectedLearningPath.language as Language | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     function extractErrorMessage(error: AxiosError | null): string | undefined { | ||||
|         return (error?.response?.data as {error: string}).error ?? error?.message; | ||||
|     } | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|  | @ -51,9 +83,27 @@ import { usePostLearningPathMutation, usePutLearningPathMutation } from '@/queri | |||
|     > | ||||
|         <template v-slot:text> | ||||
|             <json-editor-vue v-model="learningPath"></json-editor-vue> | ||||
|             <v-alert | ||||
|                  v-if="postError || putError || deleteError" | ||||
|                  icon="mdi mdi-alert-circle" | ||||
|                  type="error" | ||||
|                  :title="t('error')" | ||||
|                  :text="t(extractErrorMessage(postError) || extractErrorMessage(putError) || extractErrorMessage(deleteError)!)" | ||||
|             ></v-alert> | ||||
|         </template> | ||||
|         <template v-slot:actions> | ||||
|             <v-btn @click="uploadLearningPath" :loading="isPostPending || isPutPending">{{ props.selectedLearningPath ? t('saveChanges') : t('create') }}</v-btn> | ||||
|             <v-btn @click="uploadLearningPath" :loading="isPostPending || isPutPending" :disabled="parsedLearningPath.hruid === DEFAULT_LEARNING_PATH.hruid"> | ||||
|                 {{ props.selectedLearningPath ? t('saveChanges') : t('create') }} | ||||
|             </v-btn> | ||||
|             <v-btn @click="deleteLearningObject" :disabled="!props.selectedLearningPath"> | ||||
|                 {{ t('delete') }} | ||||
|             </v-btn> | ||||
|             <v-btn | ||||
|                 :href="`/learningPath/${props.selectedLearningPath?.hruid}/${props.selectedLearningPath?.language}/start`" | ||||
|                 :disabled="!props.selectedLearningPath" | ||||
|             > | ||||
|                 {{ t('open') }} | ||||
|             </v-btn> | ||||
|         </template> | ||||
|     </v-card> | ||||
| </template> | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| <script setup lang="ts"> | ||||
|     import LearningPathPreviewCard from './LearningPathPreviewCard.vue'; | ||||
|     import { computed, ref, type Ref } from 'vue'; | ||||
|     import { computed, ref, watch, type Ref } from 'vue'; | ||||
|     import { useI18n } from 'vue-i18n'; | ||||
|     import type { LearningPathDTO } from '@/data-objects/learning-paths/learning-path-dto'; | ||||
|     import type { LearningPath as LearningPathDTO } from '@dwengo-1/common/interfaces/learning-content'; | ||||
| 
 | ||||
|     const { t } = useI18n(); | ||||
|     const props = defineProps<{ | ||||
|  | @ -10,7 +10,7 @@ | |||
|     }>(); | ||||
| 
 | ||||
|     const tableHeaders = [ | ||||
|         { title: t("hruid"), width: "250px", key: "key" }, | ||||
|         { title: t("hruid"), width: "250px", key: "hruid" }, | ||||
|         { title: t("language"), width: "50px", key: "language" }, | ||||
|         { title: t("title"), key: "title" } | ||||
|     ]; | ||||
|  | @ -21,20 +21,25 @@ | |||
|         selectedLearningPaths.value ? selectedLearningPaths.value[0] : undefined | ||||
|     ); | ||||
| 
 | ||||
|     watch(() => props.learningPaths, () => selectedLearningPaths.value = []); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|     <div class="root"> | ||||
|         <v-data-table | ||||
|             class="table" | ||||
|             v-model="selectedLearningPaths" | ||||
|             :items="props.learningPaths" | ||||
|             :headers="tableHeaders" | ||||
|             select-strategy="single" | ||||
|             show-select | ||||
|             return-object | ||||
|         /> | ||||
|         <learning-path-preview-card class="preview" :selectedLearningPath="selectedLearningPath"/> | ||||
|         <div class="table-container"> | ||||
|             <v-data-table | ||||
|                 class="table" | ||||
|                 v-model="selectedLearningPaths" | ||||
|                 :items="props.learningPaths" | ||||
|                 :headers="tableHeaders" | ||||
|                 select-strategy="single" | ||||
|                 show-select | ||||
|                 return-object | ||||
|             /> | ||||
|         </div> | ||||
|         <div class="preview-container"> | ||||
|             <learning-path-preview-card class="preview" :selectedLearningPath="selectedLearningPath"/> | ||||
|         </div> | ||||
|     </div> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -50,11 +55,17 @@ | |||
|         padding: 20px; | ||||
|         flex-wrap: wrap; | ||||
|     } | ||||
|     .preview { | ||||
|     .preview-container { | ||||
|         flex: 1; | ||||
|         min-width: 400px; | ||||
|     } | ||||
|     .table { | ||||
|     .table-container { | ||||
|         flex: 1; | ||||
|     } | ||||
|     .preview { | ||||
|         width: 100%; | ||||
|     } | ||||
|     .table { | ||||
|         width: 100%; | ||||
|     } | ||||
| </style> | ||||
|  |  | |||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger