fix: UI-imperfecties & diverse bugs omtrent het verwijderen en editeren van leerpaden opgelost
This commit is contained in:
		
							parent
							
								
									9400b7f33c
								
							
						
					
					
						commit
						9a58126c7c
					
				
					 6 changed files with 126 additions and 28 deletions
				
			
		|  | @ -4,7 +4,6 @@ import { Language } from '@dwengo-1/common/util/language'; | ||||||
| import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js'; | import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js'; | ||||||
| import { RequiredEntityData } from '@mikro-orm/core'; | import { RequiredEntityData } from '@mikro-orm/core'; | ||||||
| import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js'; | import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js'; | ||||||
| import { EntityAlreadyExistsException } from '../../exceptions/entity-already-exists-exception.js'; |  | ||||||
| 
 | 
 | ||||||
| export class LearningPathRepository extends DwengoEntityRepository<LearningPath> { | export class LearningPathRepository extends DwengoEntityRepository<LearningPath> { | ||||||
|     public async findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> { |     public async findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> { | ||||||
|  | @ -53,21 +52,6 @@ export class LearningPathRepository extends DwengoEntityRepository<LearningPath> | ||||||
|         return this.em.create(LearningPathTransition, transitionData); |         return this.em.create(LearningPathTransition, transitionData); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async saveLearningPathNodesAndTransitions( |  | ||||||
|         path: LearningPath, |  | ||||||
|         nodes: LearningPathNode[], |  | ||||||
|         transitions: LearningPathTransition[], |  | ||||||
|         options?: { preventOverwrite?: boolean } |  | ||||||
|     ): Promise<void> { |  | ||||||
|         if (options?.preventOverwrite && (await this.findOne(path))) { |  | ||||||
|             throw new EntityAlreadyExistsException('A learning path with this hruid/language combination already exists.'); |  | ||||||
|         } |  | ||||||
|         const em = this.getEntityManager(); |  | ||||||
|         await em.persistAndFlush(path); |  | ||||||
|         await Promise.all(nodes.map(async (it) => em.persistAndFlush(it))); |  | ||||||
|         await Promise.all(transitions.map(async (it) => em.persistAndFlush(it))); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * Deletes the learning path with the given hruid and language. |      * Deletes the learning path with the given hruid and language. | ||||||
|      * @returns the deleted learning path or null if it was not found. |      * @returns the deleted learning path or null if it was not found. | ||||||
|  |  | ||||||
|  | @ -134,8 +134,14 @@ const learningPathService = { | ||||||
|      */ |      */ | ||||||
|     async createNewLearningPath(dto: LearningPath, admins: TeacherDTO[]): Promise<LearningPathEntity> { |     async createNewLearningPath(dto: LearningPath, admins: TeacherDTO[]): Promise<LearningPathEntity> { | ||||||
|         const repo = getLearningPathRepository(); |         const repo = getLearningPathRepository(); | ||||||
|  | 
 | ||||||
|         const path = mapToLearningPath(dto, admins); |         const path = mapToLearningPath(dto, admins); | ||||||
|         await repo.save(path, { preventOverwrite: true }); |         try { | ||||||
|  |             await repo.save(path, { preventOverwrite: true }); | ||||||
|  |         } catch (e: unknown) { | ||||||
|  |             repo.getEntityManager().clear(); | ||||||
|  |             throw e; | ||||||
|  |         } | ||||||
|         return path; |         return path; | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|  | @ -146,6 +152,7 @@ const learningPathService = { | ||||||
|      */ |      */ | ||||||
|     async deleteLearningPath(id: LearningPathIdentifier): Promise<LearningPathEntity> { |     async deleteLearningPath(id: LearningPathIdentifier): Promise<LearningPathEntity> { | ||||||
|         const repo = getLearningPathRepository(); |         const repo = getLearningPathRepository(); | ||||||
|  | 
 | ||||||
|         const deletedPath = await repo.deleteByHruidAndLanguage(id.hruid, id.language); |         const deletedPath = await repo.deleteByHruidAndLanguage(id.hruid, id.language); | ||||||
|         if (deletedPath) { |         if (deletedPath) { | ||||||
|             return deletedPath; |             return deletedPath; | ||||||
|  |  | ||||||
|  | @ -0,0 +1,61 @@ | ||||||
|  | <script setup lang="ts"> | ||||||
|  |     import { useI18n } from 'vue-i18n'; | ||||||
|  | 
 | ||||||
|  |     const props = defineProps<{ | ||||||
|  |         text: string, | ||||||
|  |         prependIcon?: string, | ||||||
|  |         appendIcon?: string, | ||||||
|  |         confirmQueryText: string, | ||||||
|  |         variant?: "flat" | "text" | "elevated" | "tonal" | "outlined" | "plain" | undefined, | ||||||
|  |         color?: string, | ||||||
|  |         disabled?: boolean | ||||||
|  |     }>(); | ||||||
|  | 
 | ||||||
|  |     const emit = defineEmits<{ (e: 'confirm'): void }>() | ||||||
|  | 
 | ||||||
|  |     const { t } = useI18n(); | ||||||
|  | 
 | ||||||
|  |     function confirm(): void { | ||||||
|  |         emit("confirm"); | ||||||
|  |     } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |     <v-dialog max-width="500"> | ||||||
|  |   <template v-slot:activator="{ props: activatorProps }"> | ||||||
|  |     <v-btn | ||||||
|  |       v-bind="activatorProps" | ||||||
|  |       :text="props.text" | ||||||
|  |       :prependIcon="props.prependIcon" | ||||||
|  |       :appendIcon="props.appendIcon" | ||||||
|  |       :variant="props.variant" | ||||||
|  |       :color="color" | ||||||
|  |       :disabled="props.disabled" | ||||||
|  |     ></v-btn> | ||||||
|  |   </template> | ||||||
|  | 
 | ||||||
|  |   <template v-slot:default="{ isActive }"> | ||||||
|  |     <v-card :title="t('confirmDialogTitle')"> | ||||||
|  |       <v-card-text> | ||||||
|  |         {{ props.confirmQueryText }} | ||||||
|  |       </v-card-text> | ||||||
|  | 
 | ||||||
|  |       <v-card-actions> | ||||||
|  |         <v-spacer></v-spacer> | ||||||
|  | 
 | ||||||
|  |         <v-btn | ||||||
|  |           :text="t('yes')" | ||||||
|  |           @click="confirm(); isActive.value = false" | ||||||
|  |         ></v-btn> | ||||||
|  |         <v-btn | ||||||
|  |           :text="t('cancel')" | ||||||
|  |           @click="isActive.value = false" | ||||||
|  |         ></v-btn> | ||||||
|  |       </v-card-actions> | ||||||
|  |     </v-card> | ||||||
|  |   </template> | ||||||
|  | </v-dialog> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | </style> | ||||||
|  | @ -139,5 +139,10 @@ | ||||||
|     "editLearningPath": "Edit learning path", |     "editLearningPath": "Edit learning path", | ||||||
|     "newLearningPath": "Create a new learning path", |     "newLearningPath": "Create a new learning path", | ||||||
|     "saveChanges": "Save changes", |     "saveChanges": "Save changes", | ||||||
|     "newLearningObject": "Upload learning object" |     "newLearningObject": "Upload learning object", | ||||||
|  |     "confirmDialogTitle": "Please confirm", | ||||||
|  |     "learningPathDeleteQuery": "Are you sure you want to delete this learning path?", | ||||||
|  |     "learningObjectDeleteQuery": "Are you sure you want to delete this learning object?", | ||||||
|  |     "learningPathCantModifyId": "The HRUID or language of a learning path cannot be modified.", | ||||||
|  |     "error": "Error" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ | ||||||
|     import type { LearningObject } from '@/data-objects/learning-objects/learning-object'; |     import type { LearningObject } from '@/data-objects/learning-objects/learning-object'; | ||||||
|     import UsingQueryResult from '@/components/UsingQueryResult.vue'; |     import UsingQueryResult from '@/components/UsingQueryResult.vue'; | ||||||
|     import LearningObjectContentView from '../../learning-paths/learning-object/content/LearningObjectContentView.vue'; |     import LearningObjectContentView from '../../learning-paths/learning-object/content/LearningObjectContentView.vue'; | ||||||
|  |     import ButtonWithConfirmation from '@/components/ButtonWithConfirmation.vue'; | ||||||
|     import { useDeleteLearningObjectMutation, useLearningObjectHTMLQuery } from '@/queries/learning-objects'; |     import { useDeleteLearningObjectMutation, useLearningObjectHTMLQuery } from '@/queries/learning-objects'; | ||||||
|     import { useI18n } from 'vue-i18n'; |     import { useI18n } from 'vue-i18n'; | ||||||
| 
 | 
 | ||||||
|  | @ -41,7 +42,13 @@ | ||||||
|             </using-query-result> |             </using-query-result> | ||||||
|         </template> |         </template> | ||||||
|         <template v-slot:actions> |         <template v-slot:actions> | ||||||
|             <v-btn text="Delete" @click="deleteLearningObject()"></v-btn> |             <button-with-confirmation | ||||||
|  |                 @confirm="deleteLearningObject" | ||||||
|  |                 prepend-icon="mdi mdi-delete" | ||||||
|  |                 color="red" | ||||||
|  |                 :text="t('delete')" | ||||||
|  |                 :confirmQueryText="t('learningObjectDeleteQuery')" | ||||||
|  |             /> | ||||||
|         </template> |         </template> | ||||||
|     </v-card> |     </v-card> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|  | @ -2,10 +2,12 @@ | ||||||
|     import { useI18n } from 'vue-i18n'; |     import { useI18n } from 'vue-i18n'; | ||||||
|     import { computed, ref, watch, type Ref } from 'vue'; |     import { computed, ref, watch, type Ref } from 'vue'; | ||||||
|     import JsonEditorVue from 'json-editor-vue' |     import JsonEditorVue from 'json-editor-vue' | ||||||
|  |     import ButtonWithConfirmation from '@/components/ButtonWithConfirmation.vue' | ||||||
|     import { useDeleteLearningPathMutation, usePostLearningPathMutation, usePutLearningPathMutation } from '@/queries/learning-paths'; |     import { useDeleteLearningPathMutation, usePostLearningPathMutation, usePutLearningPathMutation } from '@/queries/learning-paths'; | ||||||
|     import { Language } from '@/data-objects/language'; |     import { Language } from '@/data-objects/language'; | ||||||
|     import type { LearningPath } from '@dwengo-1/common/interfaces/learning-content'; |     import type { LearningPath } from '@dwengo-1/common/interfaces/learning-content'; | ||||||
|     import type { AxiosError } from 'axios'; |     import type { AxiosError } from 'axios'; | ||||||
|  | import { parse } from 'uuid'; | ||||||
| 
 | 
 | ||||||
|     const { t } = useI18n(); |     const { t } = useI18n(); | ||||||
| 
 | 
 | ||||||
|  | @ -63,7 +65,7 @@ | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     function deleteLearningObject(): void { |     function deleteLearningPath(): void { | ||||||
|         if (props.selectedLearningPath) { |         if (props.selectedLearningPath) { | ||||||
|             mutate({ |             mutate({ | ||||||
|                 hruid: props.selectedLearningPath.hruid, |                 hruid: props.selectedLearningPath.hruid, | ||||||
|  | @ -72,8 +74,28 @@ | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     function extractErrorMessage(error: AxiosError | null): string | undefined { |     function extractErrorMessage(error: AxiosError): string { | ||||||
|         return (error?.response?.data as {error: string}).error ?? error?.message; |         return (error.response?.data as {error: string}).error ?? error.message; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const isIdModified = computed(() => | ||||||
|  |         props.selectedLearningPath !== undefined && ( | ||||||
|  |             props.selectedLearningPath.hruid !== parsedLearningPath.value.hruid | ||||||
|  |             || props.selectedLearningPath.language !== parsedLearningPath.value.language | ||||||
|  |         ) | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     function getErrorMessage(): string | null { | ||||||
|  |         if (postError.value) { | ||||||
|  |             return t(extractErrorMessage(postError.value)); | ||||||
|  |         } else if (putError.value) { | ||||||
|  |             return t(extractErrorMessage(putError.value)); | ||||||
|  |         } else if (deleteError.value) { | ||||||
|  |             return t(extractErrorMessage(deleteError.value)); | ||||||
|  |         } else if (isIdModified.value) { | ||||||
|  |             return t('learningPathCantModifyId'); | ||||||
|  |         } | ||||||
|  |         return null; | ||||||
|     } |     } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | @ -84,22 +106,34 @@ | ||||||
|         <template v-slot:text> |         <template v-slot:text> | ||||||
|             <json-editor-vue v-model="learningPath"></json-editor-vue> |             <json-editor-vue v-model="learningPath"></json-editor-vue> | ||||||
|             <v-alert |             <v-alert | ||||||
|                  v-if="postError || putError || deleteError" |                  v-if="postError || putError || deleteError || isIdModified" | ||||||
|                  icon="mdi mdi-alert-circle" |                  icon="mdi mdi-alert-circle" | ||||||
|                  type="error" |                  type="error" | ||||||
|                  :title="t('error')" |                  :title="t('error')" | ||||||
|                  :text="t(extractErrorMessage(postError) || extractErrorMessage(putError) || extractErrorMessage(deleteError)!)" |                  :text="getErrorMessage()!" | ||||||
|             ></v-alert> |             ></v-alert> | ||||||
|         </template> |         </template> | ||||||
|         <template v-slot:actions> |         <template v-slot:actions> | ||||||
|             <v-btn @click="uploadLearningPath" :loading="isPostPending || isPutPending" :disabled="parsedLearningPath.hruid === DEFAULT_LEARNING_PATH.hruid"> |             <v-btn | ||||||
|  |                 @click="uploadLearningPath" | ||||||
|  |                 prependIcon="mdi mdi-check" | ||||||
|  |                 :loading="isPostPending || isPutPending" | ||||||
|  |                 :disabled="parsedLearningPath.hruid === DEFAULT_LEARNING_PATH.hruid || isIdModified" | ||||||
|  |             > | ||||||
|                 {{ props.selectedLearningPath ? t('saveChanges') : t('create') }} |                 {{ props.selectedLearningPath ? t('saveChanges') : t('create') }} | ||||||
|             </v-btn> |             </v-btn> | ||||||
|             <v-btn @click="deleteLearningObject" :disabled="!props.selectedLearningPath"> |             <button-with-confirmation | ||||||
|                 {{ t('delete') }} |                 @confirm="deleteLearningPath" | ||||||
|             </v-btn> |                 :disabled="!props.selectedLearningPath" | ||||||
|  |                 :text="t('delete')" | ||||||
|  |                 color="red" | ||||||
|  |                 prependIcon="mdi mdi-delete" | ||||||
|  |                 :confirmQueryText="t('learningPathDeleteQuery')" | ||||||
|  |             /> | ||||||
|             <v-btn |             <v-btn | ||||||
|                 :href="`/learningPath/${props.selectedLearningPath?.hruid}/${props.selectedLearningPath?.language}/start`" |                 :href="`/learningPath/${props.selectedLearningPath?.hruid}/${props.selectedLearningPath?.language}/start`" | ||||||
|  |                 target="_blank" | ||||||
|  |                 prepend-icon="mdi mdi-open-in-new" | ||||||
|                 :disabled="!props.selectedLearningPath" |                 :disabled="!props.selectedLearningPath" | ||||||
|             > |             > | ||||||
|                 {{ t('open') }} |                 {{ t('open') }} | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger