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> { | function postOrPutLearningPath(isPut: boolean): (req: AuthenticatedRequest, res: Response) => Promise<void> { | ||||||
|     return async (req, res) => { |     return async (req, res) => { | ||||||
|         const path: LearningPath = req.body; |         const path: LearningPath = req.body; | ||||||
|  |         const teacher = await getTeacher(req.auth!.username); | ||||||
|         if (isPut) { |         if (isPut) { | ||||||
|             if (req.params.hruid !== path.hruid || req.params.language !== path.language) { |             if (req.params.hruid !== path.hruid || req.params.language !== path.language) { | ||||||
|                 throw new BadRequestException("id_not_matching_query_params"); |                 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])); | ||||||
|         res.json(await learningPathService.createNewLearningPath(path, [teacher], isPut)); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -8,7 +8,10 @@ import { EntityAlreadyExistsException } from '../../exceptions/entity-already-ex | ||||||
| 
 | 
 | ||||||
| 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> { | ||||||
|         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, |                 language: language, | ||||||
|                 $or: [{ title: { $like: `%${query}%` } }, { description: { $like: `%${query}%` } }], |                 $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: { |                 admins: { | ||||||
|                     username: adminUsername |                     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 { LearningPath } from './learning-path.entity.js'; | ||||||
| import { LearningPathTransition } from './learning-path-transition.entity.js'; | import { LearningPathTransition } from './learning-path-transition.entity.js'; | ||||||
| import { Language } from '@dwengo-1/common/util/language'; | import { Language } from '@dwengo-1/common/util/language'; | ||||||
|  | @ -26,7 +26,7 @@ export class LearningPathNode { | ||||||
|     @Property({ type: 'bool' }) |     @Property({ type: 'bool' }) | ||||||
|     startNode!: boolean; |     startNode!: boolean; | ||||||
| 
 | 
 | ||||||
|     @OneToMany({ entity: () => LearningPathTransition, mappedBy: 'node' }) |     @OneToMany({ entity: () => LearningPathTransition, mappedBy: 'node', cascade: [Cascade.ALL] }) | ||||||
|     transitions!: Collection<LearningPathTransition>; |     transitions!: Collection<LearningPathTransition>; | ||||||
| 
 | 
 | ||||||
|     @Property({ length: 3 }) |     @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 { Teacher } from '../users/teacher.entity.js'; | ||||||
| import { LearningPathRepository } from '../../data/content/learning-path-repository.js'; | import { LearningPathRepository } from '../../data/content/learning-path-repository.js'; | ||||||
| import { LearningPathNode } from './learning-path-node.entity.js'; | import { LearningPathNode } from './learning-path-node.entity.js'; | ||||||
|  | @ -24,6 +24,6 @@ export class LearningPath { | ||||||
|     @Property({ type: 'blob', nullable: true }) |     @Property({ type: 'blob', nullable: true }) | ||||||
|     image: Buffer | null = null; |     image: Buffer | null = null; | ||||||
| 
 | 
 | ||||||
|     @OneToMany({ entity: () => LearningPathNode, mappedBy: 'learningPath' }) |     @OneToMany({ entity: () => LearningPathNode, mappedBy: 'learningPath', cascade: [Cascade.ALL] }) | ||||||
|     nodes: Collection<LearningPathNode> = new Collection<LearningPathNode>(this); |     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 learningPathService from "../../../services/learning-paths/learning-path-service"; | ||||||
| import { authorize } from "../auth"; | import { authorize } from "../auth"; | ||||||
| import { AuthenticatedRequest } from "../authenticated-request"; | import { AuthenticatedRequest } from "../authenticated-request"; | ||||||
|  | @ -5,8 +6,8 @@ import { AuthenticationInfo } from "../authentication-info"; | ||||||
| 
 | 
 | ||||||
| export const onlyAdminsForLearningPath = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { | export const onlyAdminsForLearningPath = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { | ||||||
|     const adminsForLearningPath = await learningPathService.getAdmins({ |     const adminsForLearningPath = await learningPathService.getAdmins({ | ||||||
|         hruid: req.body.hruid, |         hruid: req.params.hruid, | ||||||
|         language: req.body.language |         language: req.params.language as Language | ||||||
|     }); |     }); | ||||||
|     return adminsForLearningPath && adminsForLearningPath.includes(auth.username); |     return adminsForLearningPath && adminsForLearningPath.includes(auth.username); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import express from 'express'; | import express from 'express'; | ||||||
| import { deleteLearningPath, getLearningPaths, postLearningPath, putLearningPath } from '../controllers/learning-paths.js'; | import { deleteLearningPath, getLearningPaths, postLearningPath, putLearningPath } from '../controllers/learning-paths.js'; | ||||||
| import { teachersOnly } from '../middleware/auth/auth.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(); | const router = express.Router(); | ||||||
| 
 | 
 | ||||||
|  | @ -27,7 +27,7 @@ const router = express.Router(); | ||||||
| router.get('/', getLearningPaths); | router.get('/', getLearningPaths); | ||||||
| router.post('/', teachersOnly, postLearningPath) | router.post('/', teachersOnly, postLearningPath) | ||||||
| 
 | 
 | ||||||
| router.put('/:hruid/:language', onlyAdminsForLearningObject, putLearningPath); | router.put('/:hruid/:language', onlyAdminsForLearningPath, putLearningPath); | ||||||
| router.delete('/:hruid/:language', onlyAdminsForLearningObject, deleteLearningPath); | router.delete('/:hruid/:language', onlyAdminsForLearningPath, deleteLearningPath); | ||||||
| 
 | 
 | ||||||
| export default router; | export default router; | ||||||
|  |  | ||||||
|  | @ -72,6 +72,8 @@ const learningObjectService = { | ||||||
|             learningObject.hruid = getEnvVar(envVars.UserContentPrefix) + learningObject.hruid; |             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.
 |         // Lookup the admin teachers based on their usernames and add them to the admins of the learning object.
 | ||||||
|         const teacherRepo = getTeacherRepository(); |         const teacherRepo = getTeacherRepository(); | ||||||
|         const adminTeachers = await Promise.all( |         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 { Group } from '../../entities/assignments/group.entity'; | ||||||
| import { Collection } from '@mikro-orm/core'; | import { Collection } from '@mikro-orm/core'; | ||||||
| import { v4 } from 'uuid'; | 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 |  * 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>; |     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
 |                 !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.
 |                 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 { |     return { | ||||||
|         _id: learningObject.uuid, |         _id: learningObject.uuid, | ||||||
|         language: learningObject.language, |         language: learningObject.language, | ||||||
|  |  | ||||||
|  | @ -130,13 +130,12 @@ const learningPathService = { | ||||||
|      * Add a new learning path to the database. |      * Add a new learning path to the database. | ||||||
|      * @param dto Learning path DTO from which the learning path will be created. |      * @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 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. |      * @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 repo = getLearningPathRepository(); | ||||||
|         const path = mapToLearningPath(dto, admins); |         const path = mapToLearningPath(dto, admins); | ||||||
|         await repo.save(path, { preventOverwrite: allowReplace }); |         await repo.save(path, { preventOverwrite: true }); | ||||||
|         return path; |         return path; | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -25,8 +25,8 @@ export interface LearningObjectNode { | ||||||
|     language: Language; |     language: Language; | ||||||
|     start_node?: boolean; |     start_node?: boolean; | ||||||
|     transitions: Transition[]; |     transitions: Transition[]; | ||||||
|     created_at: string; |     created_at?: string; | ||||||
|     updatedAt: string; |     updatedAt?: string; | ||||||
|     done?: boolean; // True if a submission exists for this node by the user for whom the learning path is customized.
 |     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 { 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 type { Language } from "@/data-objects/language.ts"; | ||||||
|  | import { LearningPath } from "@/data-objects/learning-paths/learning-path"; | ||||||
| import { single } from "@/utils/response-assertions.ts"; | 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 { | export class LearningPathController extends BaseController { | ||||||
|     constructor() { |     constructor() { | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import type { Language } from "@/data-objects/language.ts"; | 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 { | export class LearningPathNode { | ||||||
|     public readonly learningobjectHruid: string; |     public readonly learningobjectHruid: string; | ||||||
|  | @ -14,7 +14,7 @@ export class LearningPathNode { | ||||||
|         learningobjectHruid: string; |         learningobjectHruid: string; | ||||||
|         version: number; |         version: number; | ||||||
|         language: Language; |         language: Language; | ||||||
|         transitions: { next: LearningPathNode; default: boolean }[]; |         transitions: { next: LearningPathNode; default?: boolean }[]; | ||||||
|         createdAt: Date; |         createdAt: Date; | ||||||
|         updatedAt: Date; |         updatedAt: Date; | ||||||
|         done?: boolean; |         done?: boolean; | ||||||
|  | @ -22,7 +22,7 @@ export class LearningPathNode { | ||||||
|         this.learningobjectHruid = options.learningobjectHruid; |         this.learningobjectHruid = options.learningobjectHruid; | ||||||
|         this.version = options.version; |         this.version = options.version; | ||||||
|         this.language = options.language; |         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.createdAt = options.createdAt; | ||||||
|         this.updatedAt = options.updatedAt; |         this.updatedAt = options.updatedAt; | ||||||
|         this.done = options.done || false; |         this.done = options.done || false; | ||||||
|  | @ -50,8 +50,8 @@ export class LearningPathNode { | ||||||
|                     return undefined; |                     return undefined; | ||||||
|                 }) |                 }) | ||||||
|                 .filter((it) => it !== undefined), |                 .filter((it) => it !== undefined), | ||||||
|             createdAt: new Date(dto.created_at), |             createdAt: dto.created_at ? new Date(dto.created_at) : new Date(), | ||||||
|             updatedAt: new Date(dto.updatedAt), |             updatedAt: dto.updatedAt ? new Date(dto.updatedAt) : new Date(), | ||||||
|             done: dto.done, |             done: dto.done, | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import type { Language } from "@/data-objects/language.ts"; | import type { Language } from "@/data-objects/language.ts"; | ||||||
| import { LearningPathNode } from "@/data-objects/learning-paths/learning-path-node.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 { | export interface LearningPathNodeDTO { | ||||||
|     _id: string; |     _id: string; | ||||||
|  | @ -77,16 +77,19 @@ export class LearningPath { | ||||||
|             hruid: dto.hruid, |             hruid: dto.hruid, | ||||||
|             title: dto.title, |             title: dto.title, | ||||||
|             description: dto.description, |             description: dto.description, | ||||||
|             amountOfNodes: dto.num_nodes, |             amountOfNodes: dto.num_nodes ?? dto.nodes.length, | ||||||
|             amountOfNodesLeft: dto.num_nodes_left, |             amountOfNodesLeft: dto.num_nodes_left ?? dto.nodes.length, | ||||||
|             keywords: dto.keywords.split(" "), |             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), |             startNode: LearningPathNode.fromDTOAndOtherNodes(LearningPath.getStartNode(dto), dto.nodes), | ||||||
|             image: dto.image, |             image: dto.image, | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     static getStartNode(dto: LearningPathDTO): LearningPathNodeDTO { |     static getStartNode(dto: LearningPathDTO): LearningObjectNode { | ||||||
|         const startNodeDtos = dto.nodes.filter((it) => it.start_node === true); |         const startNodeDtos = dto.nodes.filter((it) => it.start_node === true); | ||||||
|         if (startNodeDtos.length < 1) { |         if (startNodeDtos.length < 1) { | ||||||
|             // The learning path has no starting node -> use the first node.
 |             // The learning path has no starting node -> use the first node.
 | ||||||
|  |  | ||||||
|  | @ -134,5 +134,9 @@ | ||||||
|     "invalidZip": "This is not a valid zip file.", |     "invalidZip": "This is not a valid zip file.", | ||||||
|     "emptyZip": "This zip file is empty", |     "emptyZip": "This zip file is empty", | ||||||
|     "missingMetadata": "This learning object is missing a metadata.json file.", |     "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 type { Language } from "@/data-objects/language.ts"; | ||||||
| import { useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; | import { useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; | ||||||
| import { getLearningPathController } from "@/controllers/controllers"; | import { getLearningPathController } from "@/controllers/controllers"; | ||||||
| import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; |  | ||||||
| import type { AxiosError } from "axios"; | 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"; | export const LEARNING_PATH_KEY = "learningPath"; | ||||||
| const learningPathController = getLearningPathController(); | const learningPathController = getLearningPathController(); | ||||||
|  |  | ||||||
|  | @ -106,6 +106,12 @@ const router = createRouter({ | ||||||
|             component: SingleDiscussion, |             component: SingleDiscussion, | ||||||
|             meta: { requiresAuth: true }, |             meta: { requiresAuth: true }, | ||||||
|         }, |         }, | ||||||
|  |         { | ||||||
|  |             path: "/my-content", | ||||||
|  |             name: "OwnLearningContentPage", | ||||||
|  |             component: OwnLearningContentPage, | ||||||
|  |             meta: { requiresAuth: true } | ||||||
|  |         }, | ||||||
|         { |         { | ||||||
|             path: "/learningPath", |             path: "/learningPath", | ||||||
|             children: [ |             children: [ | ||||||
|  | @ -115,18 +121,12 @@ const router = createRouter({ | ||||||
|                     component: LearningPathSearchPage, |                     component: LearningPathSearchPage, | ||||||
|                     meta: { requiresAuth: true }, |                     meta: { requiresAuth: true }, | ||||||
|                 }, |                 }, | ||||||
|                 { |  | ||||||
|                     path: "my", |  | ||||||
|                     name: "OwnLearningContentPage", |  | ||||||
|                     component: OwnLearningContentPage, |  | ||||||
|                     meta: { requiresAuth: true } |  | ||||||
|                 }, |  | ||||||
|                 { |                 { | ||||||
|                     path: ":hruid/:language/:learningObjectHruid", |                     path: ":hruid/:language/:learningObjectHruid", | ||||||
|                     name: "LearningPath", |                     name: "LearningPath", | ||||||
|                     component: LearningPathPage, |                     component: LearningPathPage, | ||||||
|                     props: true, |                     props: true, | ||||||
|                     meta: { requiresAuth: true }, |                     meta: { requiresAuth: true } | ||||||
|                 }, |                 }, | ||||||
|             ], |             ], | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|  | @ -247,7 +247,7 @@ | ||||||
|                     </template> |                     </template> | ||||||
|                 </v-list-item> |                 </v-list-item> | ||||||
|                 <v-divider></v-divider> |                 <v-divider></v-divider> | ||||||
|                 <div v-if="props.learningObjectHruid"> |                 <div> | ||||||
|                     <using-query-result |                     <using-query-result | ||||||
|                         :query-result="learningObjectListQueryResult" |                         :query-result="learningObjectListQueryResult" | ||||||
|                         v-slot="learningObjects: { data: LearningObject[] }" |                         v-slot="learningObjects: { data: LearningObject[] }" | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ | ||||||
|     import { ref, type Ref } from "vue"; |     import { ref, type Ref } from "vue"; | ||||||
|     import { useI18n } from "vue-i18n"; |     import { useI18n } from "vue-i18n"; | ||||||
|     import { useGetAllLearningPathsByAdminQuery } from "@/queries/learning-paths"; |     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(); |     const { t } = useI18n(); | ||||||
| 
 | 
 | ||||||
|  | @ -43,7 +43,7 @@ import type { LearningPathDTO } from "@/data-objects/learning-paths/learning-pat | ||||||
|                     :query-result="learningPathsQuery" |                     :query-result="learningPathsQuery" | ||||||
|                     v-slot="response: { data: LearningPathDTO[] }" |                     v-slot="response: { data: LearningPathDTO[] }" | ||||||
|                 > |                 > | ||||||
|                     <own-learning-paths-view :learning-paths="response.data"/> |                     <own-learning-paths-view :learningPaths="response.data"/> | ||||||
|                 </using-query-result> |                 </using-query-result> | ||||||
|             </v-tabs-window-item> |             </v-tabs-window-item> | ||||||
|         </v-tabs-window> |         </v-tabs-window> | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
|     import type { LearningObject } from '@/data-objects/learning-objects/learning-object'; |     import type { LearningObject } from '@/data-objects/learning-objects/learning-object'; | ||||||
|     import LearningObjectUploadButton from '@/views/own-learning-content/learning-objects/LearningObjectUploadButton.vue' |     import LearningObjectUploadButton from '@/views/own-learning-content/learning-objects/LearningObjectUploadButton.vue' | ||||||
|     import LearningObjectPreviewCard from './LearningObjectPreviewCard.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'; |     import { useI18n } from 'vue-i18n'; | ||||||
| 
 | 
 | ||||||
|     const { t } = useI18n(); |     const { t } = useI18n(); | ||||||
|  | @ -19,6 +19,8 @@ | ||||||
| 
 | 
 | ||||||
|     const selectedLearningObjects: Ref<LearningObject[]> = ref([]); |     const selectedLearningObjects: Ref<LearningObject[]> = ref([]); | ||||||
| 
 | 
 | ||||||
|  |     watch(() => props.learningObjects, () => selectedLearningObjects.value = []); | ||||||
|  | 
 | ||||||
|     const selectedLearningObject = computed(() => |     const selectedLearningObject = computed(() => | ||||||
|         selectedLearningObjects.value ? selectedLearningObjects.value[0] : undefined |         selectedLearningObjects.value ? selectedLearningObjects.value[0] : undefined | ||||||
|     ); |     ); | ||||||
|  | @ -27,6 +29,7 @@ | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|     <div class="root"> |     <div class="root"> | ||||||
|  |         <div class="table-container"> | ||||||
|             <v-data-table |             <v-data-table | ||||||
|                 class="table" |                 class="table" | ||||||
|                 v-model="selectedLearningObjects" |                 v-model="selectedLearningObjects" | ||||||
|  | @ -36,8 +39,11 @@ | ||||||
|                 show-select |                 show-select | ||||||
|                 return-object |                 return-object | ||||||
|             /> |             /> | ||||||
|  |         </div> | ||||||
|  |         <div class="preview-container" v-if="selectedLearningObject"> | ||||||
|             <learning-object-preview-card class="preview" :selectedLearningObject="selectedLearningObject"/> |             <learning-object-preview-card class="preview" :selectedLearningObject="selectedLearningObject"/> | ||||||
|         </div> |         </div> | ||||||
|  |     </div> | ||||||
|     <div class="fab"> |     <div class="fab"> | ||||||
|         <learning-object-upload-button/> |         <learning-object-upload-button/> | ||||||
|     </div> |     </div> | ||||||
|  | @ -55,11 +61,17 @@ | ||||||
|         padding: 20px; |         padding: 20px; | ||||||
|         flex-wrap: wrap; |         flex-wrap: wrap; | ||||||
|     } |     } | ||||||
|     .preview { |     .preview-container { | ||||||
|         flex: 1; |         flex: 1; | ||||||
|         min-width: 400px; |         min-width: 400px; | ||||||
|     } |     } | ||||||
|     .table { |     .table-container { | ||||||
|         flex: 1; |         flex: 1; | ||||||
|     } |     } | ||||||
|  |     .preview { | ||||||
|  |         width: 100%; | ||||||
|  |     } | ||||||
|  |     .table { | ||||||
|  |         width: 100%; | ||||||
|  |     } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -1,48 +1,80 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|     import UsingQueryResult from '@/components/UsingQueryResult.vue'; |  | ||||||
|     import { useI18n } from 'vue-i18n'; |     import { useI18n } from 'vue-i18n'; | ||||||
|     import type { LearningPathDTO } from '@/data-objects/learning-paths/learning-path-dto'; |     import { computed, ref, watch, type Ref } from 'vue'; | ||||||
|     import { computed, ref, watch } from 'vue'; |  | ||||||
|     import JsonEditorVue from 'json-editor-vue' |     import JsonEditorVue from 'json-editor-vue' | ||||||
| import { useMutation } from '@tanstack/vue-query'; |     import { useDeleteLearningPathMutation, usePostLearningPathMutation, usePutLearningPathMutation } from '@/queries/learning-paths'; | ||||||
| import { 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 { t } = useI18n(); | ||||||
| 
 | 
 | ||||||
|     const props = defineProps<{ |     const props = defineProps<{ | ||||||
|         selectedLearningPath?: LearningPathDTO |         selectedLearningPath?: LearningPath | ||||||
|     }>(); |     }>(); | ||||||
| 
 | 
 | ||||||
|     const INDENT = 4; |     const { isPending, mutate, error: deleteError, isSuccess: deleteSuccess } = useDeleteLearningPathMutation(); | ||||||
|     const DEFAULT_LEARNING_PATH: LearningPathDTO = { | 
 | ||||||
|         language: '', |     const DEFAULT_LEARNING_PATH: LearningPath = { | ||||||
|         hruid: '', |         language: 'en', | ||||||
|         title: '', |         hruid: '...', | ||||||
|         description: '', |         title: '...', | ||||||
|         num_nodes: 0, |         description: '...', | ||||||
|         num_nodes_left: 0, |         nodes: [ | ||||||
|         nodes: [], |             { | ||||||
|         keywords: '', |                 learningobject_hruid: '...', | ||||||
|         target_ages: [], |                 language: Language.English, | ||||||
|         min_age: 0, |                 version: 1, | ||||||
|         max_age: 0, |                 start_node: true, | ||||||
|         __order: 0 |                 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: isPostPending, error: postError, mutate: doPost } = usePostLearningPathMutation(); | ||||||
|     const { isPending: isPutPending, mutate: doPut } = usePutLearningPathMutation(); |     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); |     watch(() => props.selectedLearningPath, () => learningPath.value = props.selectedLearningPath ?? DEFAULT_LEARNING_PATH); | ||||||
| 
 | 
 | ||||||
|     function uploadLearningPath(): void { |     function uploadLearningPath(): void { | ||||||
|         if (props.selectedLearningPath) { |         if (props.selectedLearningPath) { | ||||||
|             doPut({ learningPath: learningPath.value }); |             doPut({ learningPath: parsedLearningPath.value }); | ||||||
|         } else { |         } 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> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|  | @ -51,9 +83,27 @@ import { usePostLearningPathMutation, usePutLearningPathMutation } from '@/queri | ||||||
|     > |     > | ||||||
|         <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-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> | ||||||
|         <template v-slot:actions> |         <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> |         </template> | ||||||
|     </v-card> |     </v-card> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|  | @ -1,8 +1,8 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|     import LearningPathPreviewCard from './LearningPathPreviewCard.vue'; |     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 { 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 { t } = useI18n(); | ||||||
|     const props = defineProps<{ |     const props = defineProps<{ | ||||||
|  | @ -10,7 +10,7 @@ | ||||||
|     }>(); |     }>(); | ||||||
| 
 | 
 | ||||||
|     const tableHeaders = [ |     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("language"), width: "50px", key: "language" }, | ||||||
|         { title: t("title"), key: "title" } |         { title: t("title"), key: "title" } | ||||||
|     ]; |     ]; | ||||||
|  | @ -21,10 +21,12 @@ | ||||||
|         selectedLearningPaths.value ? selectedLearningPaths.value[0] : undefined |         selectedLearningPaths.value ? selectedLearningPaths.value[0] : undefined | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|  |     watch(() => props.learningPaths, () => selectedLearningPaths.value = []); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|     <div class="root"> |     <div class="root"> | ||||||
|  |         <div class="table-container"> | ||||||
|             <v-data-table |             <v-data-table | ||||||
|                 class="table" |                 class="table" | ||||||
|                 v-model="selectedLearningPaths" |                 v-model="selectedLearningPaths" | ||||||
|  | @ -34,8 +36,11 @@ | ||||||
|                 show-select |                 show-select | ||||||
|                 return-object |                 return-object | ||||||
|             /> |             /> | ||||||
|  |         </div> | ||||||
|  |         <div class="preview-container"> | ||||||
|             <learning-path-preview-card class="preview" :selectedLearningPath="selectedLearningPath"/> |             <learning-path-preview-card class="preview" :selectedLearningPath="selectedLearningPath"/> | ||||||
|         </div> |         </div> | ||||||
|  |     </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
|  | @ -50,11 +55,17 @@ | ||||||
|         padding: 20px; |         padding: 20px; | ||||||
|         flex-wrap: wrap; |         flex-wrap: wrap; | ||||||
|     } |     } | ||||||
|     .preview { |     .preview-container { | ||||||
|         flex: 1; |         flex: 1; | ||||||
|         min-width: 400px; |         min-width: 400px; | ||||||
|     } |     } | ||||||
|     .table { |     .table-container { | ||||||
|         flex: 1; |         flex: 1; | ||||||
|     } |     } | ||||||
|  |     .preview { | ||||||
|  |         width: 100%; | ||||||
|  |     } | ||||||
|  |     .table { | ||||||
|  |         width: 100%; | ||||||
|  |     } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger