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
				
			
		|  | @ -68,13 +68,14 @@ export async function getLearningPaths(req: Request, res: Response): Promise<voi | |||
| function postOrPutLearningPath(isPut: boolean): (req: AuthenticatedRequest, res: Response) => Promise<void> { | ||||
|     return async (req, res) => { | ||||
|         const path: LearningPath = req.body; | ||||
|         const teacher = await getTeacher(req.auth!.username); | ||||
|         if (isPut) { | ||||
|             if (req.params.hruid !== path.hruid || req.params.language !== path.language) { | ||||
|                 throw new BadRequestException("id_not_matching_query_params"); | ||||
|             } | ||||
|             await learningPathService.deleteLearningPath({hruid: path.hruid, language: path.language as Language}); | ||||
|         } | ||||
|         const teacher = await getTeacher(req.auth!.username); | ||||
|         res.json(await learningPathService.createNewLearningPath(path, [teacher], isPut)); | ||||
|         res.json(await learningPathService.createNewLearningPath(path, [teacher])); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -8,7 +8,10 @@ import { EntityAlreadyExistsException } from '../../exceptions/entity-already-ex | |||
| 
 | ||||
| export class LearningPathRepository extends DwengoEntityRepository<LearningPath> { | ||||
|     public async findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> { | ||||
|         return this.findOne({ hruid: hruid, language: language }, { populate: ['nodes', 'nodes.transitions'] }); | ||||
|         return this.findOne( | ||||
|             { hruid: hruid, language: language }, | ||||
|             { populate: ['nodes', 'nodes.transitions', 'admins'] } | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -24,7 +27,7 @@ export class LearningPathRepository extends DwengoEntityRepository<LearningPath> | |||
|                 language: language, | ||||
|                 $or: [{ title: { $like: `%${query}%` } }, { description: { $like: `%${query}%` } }], | ||||
|             }, | ||||
|             populate: ['nodes', 'nodes.transitions'], | ||||
|             populate: ['nodes', 'nodes.transitions', 'admins'], | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|  | @ -37,7 +40,8 @@ export class LearningPathRepository extends DwengoEntityRepository<LearningPath> | |||
|                 admins: { | ||||
|                     username: adminUsername | ||||
|                 } | ||||
|             } | ||||
|             }, | ||||
|             populate: ['nodes', 'nodes.transitions', 'admins'] | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property, Rel } from '@mikro-orm/core'; | ||||
| import { Cascade, Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property, Rel } from '@mikro-orm/core'; | ||||
| import { LearningPath } from './learning-path.entity.js'; | ||||
| import { LearningPathTransition } from './learning-path-transition.entity.js'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
|  | @ -26,7 +26,7 @@ export class LearningPathNode { | |||
|     @Property({ type: 'bool' }) | ||||
|     startNode!: boolean; | ||||
| 
 | ||||
|     @OneToMany({ entity: () => LearningPathTransition, mappedBy: 'node' }) | ||||
|     @OneToMany({ entity: () => LearningPathTransition, mappedBy: 'node', cascade: [Cascade.ALL] }) | ||||
|     transitions!: Collection<LearningPathTransition>; | ||||
| 
 | ||||
|     @Property({ length: 3 }) | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import { Collection, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; | ||||
| import { Cascade, Collection, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; | ||||
| import { Teacher } from '../users/teacher.entity.js'; | ||||
| import { LearningPathRepository } from '../../data/content/learning-path-repository.js'; | ||||
| import { LearningPathNode } from './learning-path-node.entity.js'; | ||||
|  | @ -24,6 +24,6 @@ export class LearningPath { | |||
|     @Property({ type: 'blob', nullable: true }) | ||||
|     image: Buffer | null = null; | ||||
| 
 | ||||
|     @OneToMany({ entity: () => LearningPathNode, mappedBy: 'learningPath' }) | ||||
|     @OneToMany({ entity: () => LearningPathNode, mappedBy: 'learningPath', cascade: [Cascade.ALL] }) | ||||
|     nodes: Collection<LearningPathNode> = new Collection<LearningPathNode>(this); | ||||
| } | ||||
|  |  | |||
|  | @ -1,3 +1,4 @@ | |||
| import { Language } from "@dwengo-1/common/util/language"; | ||||
| import learningPathService from "../../../services/learning-paths/learning-path-service"; | ||||
| import { authorize } from "../auth"; | ||||
| import { AuthenticatedRequest } from "../authenticated-request"; | ||||
|  | @ -5,8 +6,8 @@ import { AuthenticationInfo } from "../authentication-info"; | |||
| 
 | ||||
| export const onlyAdminsForLearningPath = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { | ||||
|     const adminsForLearningPath = await learningPathService.getAdmins({ | ||||
|         hruid: req.body.hruid, | ||||
|         language: req.body.language | ||||
|         hruid: req.params.hruid, | ||||
|         language: req.params.language as Language | ||||
|     }); | ||||
|     return adminsForLearningPath && adminsForLearningPath.includes(auth.username); | ||||
| }); | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import express from 'express'; | ||||
| import { deleteLearningPath, getLearningPaths, postLearningPath, putLearningPath } from '../controllers/learning-paths.js'; | ||||
| import { teachersOnly } from '../middleware/auth/auth.js'; | ||||
| import { onlyAdminsForLearningObject } from '../middleware/auth/checks/learning-object-auth-checks.js'; | ||||
| import { onlyAdminsForLearningPath } from '../middleware/auth/checks/learning-path-auth-checks.js'; | ||||
| 
 | ||||
| const router = express.Router(); | ||||
| 
 | ||||
|  | @ -27,7 +27,7 @@ const router = express.Router(); | |||
| router.get('/', getLearningPaths); | ||||
| router.post('/', teachersOnly, postLearningPath) | ||||
| 
 | ||||
| router.put('/:hruid/:language', onlyAdminsForLearningObject, putLearningPath); | ||||
| router.delete('/:hruid/:language', onlyAdminsForLearningObject, deleteLearningPath); | ||||
| router.put('/:hruid/:language', onlyAdminsForLearningPath, putLearningPath); | ||||
| router.delete('/:hruid/:language', onlyAdminsForLearningPath, deleteLearningPath); | ||||
| 
 | ||||
| export default router; | ||||
|  |  | |||
|  | @ -72,6 +72,8 @@ const learningObjectService = { | |||
|             learningObject.hruid = getEnvVar(envVars.UserContentPrefix) + learningObject.hruid; | ||||
|         } | ||||
| 
 | ||||
|         await learningObjectRepository.getEntityManager().flush(); | ||||
| 
 | ||||
|         // Lookup the admin teachers based on their usernames and add them to the admins of the learning object.
 | ||||
|         const teacherRepo = getTeacherRepository(); | ||||
|         const adminTeachers = await Promise.all( | ||||
|  |  | |||
|  | @ -16,6 +16,9 @@ import { Language } from '@dwengo-1/common/util/language'; | |||
| import { Group } from '../../entities/assignments/group.entity'; | ||||
| import { Collection } from '@mikro-orm/core'; | ||||
| import { v4 } from 'uuid'; | ||||
| import { getLogger } from '../../logging/initalize.js'; | ||||
| 
 | ||||
| const logger = getLogger(); | ||||
| 
 | ||||
| /** | ||||
|  * Fetches the corresponding learning object for each of the nodes and creates a map that maps each node to its | ||||
|  | @ -38,8 +41,13 @@ async function getLearningObjectsForNodes(nodes: Collection<LearningPathNode>): | |||
|             ) | ||||
|         ) | ||||
|     ); | ||||
|     if (Array.from(nullableNodesToLearningObjects.values()).some((it) => it === null)) { | ||||
|         throw new Error('At least one of the learning objects on this path could not be found.'); | ||||
| 
 | ||||
|     // Ignore all learning objects that cannot be found such that the rest of the learning path keeps working.
 | ||||
|     for (const [key, value] of nullableNodesToLearningObjects) { | ||||
|         if (value === null) { | ||||
|             logger.warn(`Learning object ${key.learningObjectHruid}/${key.language}/${key.version} not found!`); | ||||
|             nullableNodesToLearningObjects.delete(key); | ||||
|         } | ||||
|     } | ||||
|     return nullableNodesToLearningObjects as Map<LearningPathNode, FilteredLearningObject>; | ||||
| } | ||||
|  | @ -102,7 +110,14 @@ async function convertNode( | |||
|                 !personalizedFor || // If we do not want a personalized learning path, keep all transitions
 | ||||
|                 isTransitionPossible(trans, optionalJsonStringToObject(lastSubmission?.content)) // Otherwise remove all transitions that aren't possible.
 | ||||
|         ) | ||||
|         .map((trans, i) => convertTransition(trans, i, nodesToLearningObjects)); | ||||
|         .map((trans, i) => { | ||||
|             try { | ||||
|                 return convertTransition(trans, i, nodesToLearningObjects) | ||||
|             } catch (_: unknown) { | ||||
|                 logger.error(`Transition could not be resolved: ${JSON.stringify(trans)}`); | ||||
|                 return undefined; // Do not crash on invalid transitions, just ignore them so the rest of the learning path keeps working.
 | ||||
|             } | ||||
|         }).filter(it => it); | ||||
|     return { | ||||
|         _id: learningObject.uuid, | ||||
|         language: learningObject.language, | ||||
|  |  | |||
|  | @ -130,13 +130,12 @@ const learningPathService = { | |||
|      * Add a new learning path to the database. | ||||
|      * @param dto Learning path DTO from which the learning path will be created. | ||||
|      * @param admins Teachers who should become an admin of the learning path. | ||||
|      * @param allowReplace If this is set to true and there is already a learning path with the same identifier, it is replaced. | ||||
|      * @returns the created learning path. | ||||
|      */ | ||||
|     async createNewLearningPath(dto: LearningPath, admins: TeacherDTO[], allowReplace = false): Promise<LearningPathEntity> { | ||||
|     async createNewLearningPath(dto: LearningPath, admins: TeacherDTO[]): Promise<LearningPathEntity> { | ||||
|         const repo = getLearningPathRepository(); | ||||
|         const path = mapToLearningPath(dto, admins); | ||||
|         await repo.save(path, { preventOverwrite: allowReplace }); | ||||
|         await repo.save(path, { preventOverwrite: true }); | ||||
|         return path; | ||||
|     }, | ||||
| 
 | ||||
|  |  | |||
|  | @ -25,8 +25,8 @@ export interface LearningObjectNode { | |||
|     language: Language; | ||||
|     start_node?: boolean; | ||||
|     transitions: Transition[]; | ||||
|     created_at: string; | ||||
|     updatedAt: string; | ||||
|     created_at?: string; | ||||
|     updatedAt?: string; | ||||
|     done?: boolean; // True if a submission exists for this node by the user for whom the learning path is customized.
 | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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[] }" | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ | |||
|     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 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,6 +29,7 @@ | |||
| 
 | ||||
| <template> | ||||
|     <div class="root"> | ||||
|         <div class="table-container"> | ||||
|             <v-data-table | ||||
|                 class="table" | ||||
|                 v-model="selectedLearningObjects" | ||||
|  | @ -36,8 +39,11 @@ | |||
|                 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/> | ||||
|     </div> | ||||
|  | @ -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,10 +21,12 @@ | |||
|         selectedLearningPaths.value ? selectedLearningPaths.value[0] : undefined | ||||
|     ); | ||||
| 
 | ||||
|     watch(() => props.learningPaths, () => selectedLearningPaths.value = []); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|     <div class="root"> | ||||
|         <div class="table-container"> | ||||
|             <v-data-table | ||||
|                 class="table" | ||||
|                 v-model="selectedLearningPaths" | ||||
|  | @ -34,8 +36,11 @@ | |||
|                 show-select | ||||
|                 return-object | ||||
|             /> | ||||
|         </div> | ||||
|         <div class="preview-container"> | ||||
|             <learning-path-preview-card class="preview" :selectedLearningPath="selectedLearningPath"/> | ||||
|         </div> | ||||
|     </div> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped> | ||||
|  | @ -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