feat(frontend): Navigatie voor leerpad geïmplementeerd.
This commit is contained in:
		
							parent
							
								
									3c3fddb7d0
								
							
						
					
					
						commit
						07340de2e3
					
				
					 13 changed files with 216 additions and 54 deletions
				
			
		|  | @ -4,9 +4,8 @@ import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifie | ||||||
| import learningObjectService from '../services/learning-objects/learning-object-service.js'; | import learningObjectService from '../services/learning-objects/learning-object-service.js'; | ||||||
| import { EnvVars, getEnvVar } from '../util/envvars.js'; | import { EnvVars, getEnvVar } from '../util/envvars.js'; | ||||||
| import { Language } from '../entities/content/language.js'; | import { Language } from '../entities/content/language.js'; | ||||||
| import { BadRequestException } from '../exceptions.js'; | import {BadRequestException, NotFoundException} from '../exceptions.js'; | ||||||
| import attachmentService from '../services/learning-objects/attachment-service.js'; | import attachmentService from '../services/learning-objects/attachment-service.js'; | ||||||
| import { NotFoundError } from '@mikro-orm/core'; |  | ||||||
| 
 | 
 | ||||||
| function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier { | function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier { | ||||||
|     if (!req.params.hruid) { |     if (!req.params.hruid) { | ||||||
|  | @ -47,6 +46,11 @@ export async function getLearningObject(req: Request, res: Response): Promise<vo | ||||||
|     const learningObjectId = getLearningObjectIdentifierFromRequest(req); |     const learningObjectId = getLearningObjectIdentifierFromRequest(req); | ||||||
| 
 | 
 | ||||||
|     const learningObject = await learningObjectService.getLearningObjectById(learningObjectId); |     const learningObject = await learningObjectService.getLearningObjectById(learningObjectId); | ||||||
|  | 
 | ||||||
|  |     if (!learningObject) { | ||||||
|  |         throw new NotFoundException("Learning object not found"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     res.json(learningObject); |     res.json(learningObject); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -63,7 +67,7 @@ export async function getAttachment(req: Request, res: Response): Promise<void> | ||||||
|     const attachment = await attachmentService.getAttachment(learningObjectId, name); |     const attachment = await attachmentService.getAttachment(learningObjectId, name); | ||||||
| 
 | 
 | ||||||
|     if (!attachment) { |     if (!attachment) { | ||||||
|         throw new NotFoundError(`Attachment ${name} not found`); |         throw new NotFoundException(`Attachment ${name} not found`); | ||||||
|     } |     } | ||||||
|     res.setHeader('Content-Type', attachment.mimeType).send(attachment.content); |     res.setHeader('Content-Type', attachment.mimeType).send(attachment.content); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,10 +1,16 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|     import auth from "@/services/auth/auth-service.ts"; |     import auth from "@/services/auth/auth-service.ts"; | ||||||
|  |     import MenuBar from "@/components/MenuBar.vue"; | ||||||
|     auth.loadUser(); |     auth.loadUser(); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|     <router-view /> |     <v-app> | ||||||
|  |         <menu-bar></menu-bar> | ||||||
|  |         <v-main> | ||||||
|  |             <router-view /> | ||||||
|  |         </v-main> | ||||||
|  |     </v-app> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped></style> | <style scoped></style> | ||||||
|  |  | ||||||
|  | @ -37,7 +37,7 @@ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|     <main> |     <v-app-bar> | ||||||
|         <nav class="menu"> |         <nav class="menu"> | ||||||
|             <div class="left"> |             <div class="left"> | ||||||
|                 <ul> |                 <ul> | ||||||
|  | @ -133,7 +133,7 @@ | ||||||
|                 </li> |                 </li> | ||||||
|             </div> |             </div> | ||||||
|         </nav> |         </nav> | ||||||
|     </main> |     </v-app-bar> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
|  | @ -188,4 +188,8 @@ | ||||||
|     nav a.router-link-active { |     nav a.router-link-active { | ||||||
|         font-weight: bold; |         font-weight: bold; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     nav { | ||||||
|  |         width: 100%; | ||||||
|  |     } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -1,39 +1,51 @@ | ||||||
| <script setup lang="ts" generic="T"> | <script setup lang="ts" generic="T"> | ||||||
|     import {RemoteResource} from "@/services/api-client/remote-resource.ts"; | import {type ErrorState, RemoteResource, type RemoteResourceState} from "@/services/api-client/remote-resource.ts"; | ||||||
|     import {computed} from "vue"; | import {computed, onMounted, reactive, ref, type UnwrapNestedRefs, watch} from "vue"; | ||||||
|     import {useI18n} from "vue-i18n"; |     import {useI18n} from "vue-i18n"; | ||||||
| 
 | 
 | ||||||
|     const props = defineProps<{ |     const props = defineProps<{ | ||||||
|         resource: RemoteResource<T> |         resource: RemoteResource<T> | (UnwrapNestedRefs<RemoteResource<T>> & {}) | ||||||
|     }>() |     }>() | ||||||
| 
 | 
 | ||||||
|  |     const resource = reactive(props.resource as RemoteResource<T>); | ||||||
|  | 
 | ||||||
|     const { t } = useI18n(); |     const { t } = useI18n(); | ||||||
| 
 | 
 | ||||||
|     const isLoading = computed(() => props.resource.state.type === 'loading'); |     const isLoading = computed(() => resource.state.type === 'loading'); | ||||||
|     const isError = computed(() => props.resource.state.type === 'error'); |     const isError = computed(() => resource.state.type === 'error'); | ||||||
| 
 | 
 | ||||||
|     // `data` will be correctly inferred as `T` |     // `data` will be correctly inferred as `T` | ||||||
|     const data = computed(() => props.resource.data); |     const data = computed(() => resource.data); | ||||||
| 
 | 
 | ||||||
|     const error = computed(() => props.resource.state.type === "error" ? props.resource.state : null); |     const error = computed(() => resource.state.type === "error" ? resource.state as ErrorState : null); | ||||||
|     const errorMessage = computed(() => |     const errorMessage = computed(() => | ||||||
|         error.value?.message ? error.value.message : JSON.stringify(error.value?.error) |         error.value?.message ? error.value.message : JSON.stringify(error.value?.error) | ||||||
|     ); |     ); | ||||||
|  | 
 | ||||||
|  |     watch(data, (newValue, _) => { | ||||||
|  |         if (!newValue && resource.state.type !== "loading") { | ||||||
|  |             (resource as RemoteResource<T>).startRequestInBackground(); | ||||||
|  |         } | ||||||
|  |     }, {immediate: true}); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|     <div v-if="isLoading"> |     <div class="loading-div" v-if="isLoading"> | ||||||
|         <v-progress-circular indeterminate></v-progress-circular> |         <v-progress-circular indeterminate></v-progress-circular> | ||||||
|     </div> |     </div> | ||||||
|     <div v-if="isError"> |     <div v-if="isError"> | ||||||
|         <v-empty-state |         <v-empty-state | ||||||
|             icon="mdi-alert-circle-outline" |             icon="mdi-alert-circle-outline" | ||||||
|             text="errorMessage" |             :text="errorMessage" | ||||||
|             :title=t("error_title") |             :title="t('error_title')" | ||||||
|         ></v-empty-state> |         ></v-empty-state> | ||||||
|     </div> |     </div> | ||||||
|  |     <slot v-if="data" :data="data!"></slot> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
| 
 |   .loading-div { | ||||||
|  |       text-align: center; | ||||||
|  |       margin: 10px; | ||||||
|  |   } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -14,6 +14,7 @@ import UserClasses from "@/views/classes/UserClasses.vue"; | ||||||
| import UserAssignments from "@/views/classes/UserAssignments.vue"; | import UserAssignments from "@/views/classes/UserAssignments.vue"; | ||||||
| import authState from "@/services/auth/auth-service.ts"; | import authState from "@/services/auth/auth-service.ts"; | ||||||
| import LearningPathPage from "@/views/learning-paths/LearningPathPage.vue"; | import LearningPathPage from "@/views/learning-paths/LearningPathPage.vue"; | ||||||
|  | import path from "path"; | ||||||
| 
 | 
 | ||||||
| const router = createRouter({ | const router = createRouter({ | ||||||
|     history: createWebHistory(import.meta.env.BASE_URL), |     history: createWebHistory(import.meta.env.BASE_URL), | ||||||
|  | @ -104,7 +105,15 @@ const router = createRouter({ | ||||||
|             path: "/learningPath/:hruid/:language", |             path: "/learningPath/:hruid/:language", | ||||||
|             name: "LearningPath", |             name: "LearningPath", | ||||||
|             component: LearningPathPage, |             component: LearningPathPage, | ||||||
|             meta: { requiresAuth: false } |             props: true, | ||||||
|  |             meta: { requiresAuth: false }, | ||||||
|  |             children: [ | ||||||
|  |                 { | ||||||
|  |                     path: ":learningObjectHruid", | ||||||
|  |                     component: LearningPathPage, | ||||||
|  |                     props: true, | ||||||
|  |                 } | ||||||
|  |             ] | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             path: "/:catchAll(.*)", |             path: "/:catchAll(.*)", | ||||||
|  |  | ||||||
|  | @ -8,3 +8,15 @@ export class HttpErrorStatusException extends Error { | ||||||
|         this.statusCode = response.status; |         this.statusCode = response.status; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export class NotFoundException extends Error { | ||||||
|  |     constructor(message: string) { | ||||||
|  |         super(message); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export class InvalidResponseException extends Error { | ||||||
|  |     constructor(message: string) { | ||||||
|  |         super(message); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,10 @@ | ||||||
|  | import {type Params, RestEndpoint} from "@/services/api-client/endpoints/rest-endpoint.ts"; | ||||||
|  | import {RemoteResource} from "@/services/api-client/remote-resource.ts"; | ||||||
|  | 
 | ||||||
|  | export class GetHtmlEndpoint<PP extends Params, QP extends Params> extends RestEndpoint<PP, QP, undefined, Document> { | ||||||
|  |     readonly method: "GET" | "POST" | "PUT" | "DELETE" = "GET"; | ||||||
|  | 
 | ||||||
|  |     public get(pathParams: PP, queryParams: QP): RemoteResource<Document> { | ||||||
|  |         return super.request(pathParams, queryParams, undefined, "document"); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,23 +1,28 @@ | ||||||
| import {RemoteResource} from "@/services/api-client/remote-resource.ts"; | import {RemoteResource} from "@/services/api-client/remote-resource.ts"; | ||||||
| import apiClient from "@/services/api-client/api-client.ts"; | import apiClient from "@/services/api-client/api-client.ts"; | ||||||
| import {HttpErrorStatusException} from "@/services/api-client/api-exceptions"; | import {HttpErrorStatusException} from "@/services/api-client/api-exceptions.ts"; | ||||||
|  | import type {ResponseType} from "axios"; | ||||||
| 
 | 
 | ||||||
| export abstract class RestEndpoint<PP extends Params, QP extends Params, B, R> { | export abstract class RestEndpoint<PP extends Params, QP extends Params, B, R> { | ||||||
|     public abstract readonly method: "GET" | "POST" | "PUT" | "DELETE"; |     public abstract readonly method: "GET" | "POST" | "PUT" | "DELETE"; | ||||||
|     constructor(public readonly url: string) { |     constructor(public readonly url: string) { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected request(pathParams: PP, queryParams: QP, body: B): RemoteResource<R> { |     protected request(pathParams: PP, queryParams: QP, body: B, responseType?: ResponseType): RemoteResource<R> { | ||||||
|         let urlFilledIn = this.url; |         let urlFilledIn = this.url.replace(/:(\w+)(\/|$)/g, (_, key, after) => | ||||||
|         urlFilledIn.replace(/:(\w+)([/$])/g, (_, key, after) => |             (pathParams[key] ? encodeURIComponent(pathParams[key]) : `:${key}`) + after | ||||||
|             (key in pathParams ? encodeURIComponent(pathParams[key]) : `:${key}`) + after |  | ||||||
|         ); |         ); | ||||||
|  |         console.log(this.url); | ||||||
|  |         console.log(/:(\w+)(\W|$)/g.test(this.url)) | ||||||
|  |         console.log(pathParams); | ||||||
|  |         console.log("--> filled in: " + urlFilledIn); | ||||||
|         return new RemoteResource(async () => { |         return new RemoteResource(async () => { | ||||||
|             const response = await apiClient.request<R>({ |             const response = await apiClient.request<R>({ | ||||||
|                 url: urlFilledIn, |                 url: urlFilledIn, | ||||||
|                 method: this.method, |                 method: this.method, | ||||||
|                 params: queryParams, |                 params: queryParams, | ||||||
|                 data: body, |                 data: body, | ||||||
|  |                 responseType: responseType || 'json' | ||||||
|             }); |             }); | ||||||
|             if (response.status / 100 !== 2) { |             if (response.status / 100 !== 2) { | ||||||
|                 throw new HttpErrorStatusException(response); |                 throw new HttpErrorStatusException(response); | ||||||
|  | @ -27,4 +32,4 @@ export abstract class RestEndpoint<PP extends Params, QP extends Params, B, R> { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export type Params = {[key: string]: string | number | boolean}; | export type Params = {[key: string]: string | number | boolean | undefined}; | ||||||
|  |  | ||||||
|  | @ -2,22 +2,40 @@ export class RemoteResource<T> { | ||||||
|     static NOT_LOADED: NotLoadedState = {type: "notLoaded"}; |     static NOT_LOADED: NotLoadedState = {type: "notLoaded"}; | ||||||
|     static LOADING: LoadingState = {type: "loading"}; |     static LOADING: LoadingState = {type: "loading"}; | ||||||
| 
 | 
 | ||||||
|     private state: NotLoadedState | LoadingState | ErrorState | SuccessState<T> = RemoteResource.NOT_LOADED; |     private _state: RemoteResourceState<T> = RemoteResource.NOT_LOADED; | ||||||
| 
 | 
 | ||||||
|     constructor(private readonly requestFn: () => Promise<T>) { |     constructor(private readonly requestFn: () => Promise<T>) { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public static join<T>(resources: RemoteResource<T>[]): RemoteResource<T[]> { | ||||||
|  |         return new RemoteResource(async () => { | ||||||
|  |             console.log("joined fetch"); | ||||||
|  |             const promises = resources.map(it => it.request()); | ||||||
|  |             const data = await Promise.all(promises); | ||||||
|  |             const failed = resources | ||||||
|  |                 .filter(it => it.state.type === "error") | ||||||
|  |                 .map(it => it.state as ErrorState); | ||||||
|  |             if (failed.length > 0) { | ||||||
|  |                 console.log("joined error!"); | ||||||
|  |                 throw failed[0].error; | ||||||
|  |             } | ||||||
|  |             console.log("succ"); | ||||||
|  |             console.log(data); | ||||||
|  |             return data.map(it => it!); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public async request(): Promise<T | undefined> { |     public async request(): Promise<T | undefined> { | ||||||
|         this.state = RemoteResource.LOADING; |         this._state = RemoteResource.LOADING; | ||||||
|         try { |         try { | ||||||
|             let resource = await this.requestFn(); |             let resource = await this.requestFn(); | ||||||
|             this.state = { |             this._state = { | ||||||
|                 type: "success", |                 type: "success", | ||||||
|                 data: resource |                 data: resource | ||||||
|             }; |             }; | ||||||
|             return resource; |             return resource; | ||||||
|         } catch (e: any) { |         } catch (e: any) { | ||||||
|             this.state = { |             this._state = { | ||||||
|                 type: "error", |                 type: "error", | ||||||
|                 errorCode: e.statusCode, |                 errorCode: e.statusCode, | ||||||
|                 message: e.message, |                 message: e.message, | ||||||
|  | @ -31,19 +49,23 @@ export class RemoteResource<T> { | ||||||
|         return this; |         return this; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public get state(): RemoteResourceState<T> { | ||||||
|  |         return this._state; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public get data(): T | undefined { |     public get data(): T | undefined { | ||||||
|         if (this.state.type === "success") { |         if (this._state.type === "success") { | ||||||
|             return this.state.data; |             return this._state.data; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public map<U>(mappingFn: (content: T) => U): RemoteResource<U> { |     public map<U>(mappingFn: (content: T) => U): RemoteResource<U> { | ||||||
|         return new RemoteResource<U>(async () => { |         return new RemoteResource<U>(async () => { | ||||||
|             await this.request(); |             await this.request(); | ||||||
|             if (this.state.type === "success") { |             if (this._state.type === "success") { | ||||||
|                 return mappingFn(this.state.data); |                 return mappingFn(this._state.data); | ||||||
|             } else if (this.state.type === "error") { |             } else if (this._state.type === "error") { | ||||||
|                 throw this.state.error; |                 throw this._state.error; | ||||||
|             } else { |             } else { | ||||||
|                 throw new Error("Fetched resource, but afterwards, it was neither in a success nor in an error state. " + |                 throw new Error("Fetched resource, but afterwards, it was neither in a success nor in an error state. " + | ||||||
|                     "This should never happen."); |                     "This should never happen."); | ||||||
|  | @ -52,19 +74,20 @@ export class RemoteResource<T> { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type NotLoadedState = { | export type NotLoadedState = { | ||||||
|     type: "notLoaded" |     type: "notLoaded" | ||||||
| }; | }; | ||||||
| type LoadingState = { | export type LoadingState = { | ||||||
|     type: "loading" |     type: "loading" | ||||||
| }; | }; | ||||||
| type ErrorState = { | export type ErrorState = { | ||||||
|     type: "error", |     type: "error", | ||||||
|     errorCode?: number, |     errorCode?: number, | ||||||
|     message?: string, |     message?: string, | ||||||
|     error: any |     error: any | ||||||
| }; | }; | ||||||
| type SuccessState<T> = { | export type SuccessState<T> = { | ||||||
|     type: "success", |     type: "success", | ||||||
|     data: T |     data: T | ||||||
| } | }; | ||||||
|  | export type RemoteResourceState<T> = NotLoadedState | LoadingState | ErrorState | SuccessState<T>; | ||||||
|  |  | ||||||
|  | @ -1,13 +1,23 @@ | ||||||
| import {GetEndpoint} from "@/services/api-client/endpoints/get-endpoint.ts"; | import {GetEndpoint} from "@/services/api-client/endpoints/get-endpoint.ts"; | ||||||
| import {LearningPath, type LearningPathDTO} from "@/services/learning-content/learning-path.ts"; | import {LearningPath, type LearningPathDTO} from "@/services/learning-content/learning-path.ts"; | ||||||
| import type {RemoteResource} from "@/services/api-client/remote-resource.ts"; | import type {RemoteResource} from "@/services/api-client/remote-resource.ts"; | ||||||
|  | import type {Language} from "@/services/learning-content/language.ts"; | ||||||
|  | import {single} from "@/utils/response-assertions.ts"; | ||||||
| 
 | 
 | ||||||
| const searchLearningPathsEndpoint = new GetEndpoint<{}, {query: string}, LearningPathDTO[]>( | const learningPathEndpoint = new GetEndpoint<{}, {search?: string, hruid?: string, language?: Language}, LearningPathDTO[]>( | ||||||
|     "/learningObjects/:query" |     "/learningPath" | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| export function searchLearningPaths(query: string): RemoteResource<LearningPath[]> { | export function searchLearningPaths(query: string): RemoteResource<LearningPath[]> { | ||||||
|     return searchLearningPathsEndpoint |     return learningPathEndpoint | ||||||
|         .get({}, {query: query}) |         .get({}, {search: query}) | ||||||
|         .map(dtos => dtos.map(dto => LearningPath.fromDTO(dto))); |         .map(dtos => dtos.map(dto => LearningPath.fromDTO(dto))); | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export function getLearningPath(hruid: string, language: Language): RemoteResource<LearningPath> { | ||||||
|  |     console.log({hruid, language}) | ||||||
|  |     return learningPathEndpoint | ||||||
|  |         .get({}, {hruid, language}) | ||||||
|  |         .map(it => {console.log(it); return it;}) | ||||||
|  |         .map(dtos => LearningPath.fromDTO(single(dtos))); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import type {Language} from "@/services/learning-content/language.ts"; | import type {Language} from "@/services/learning-content/language.ts"; | ||||||
| import type {RemoteResource} from "@/services/api-client/remote-resource.ts"; | import {RemoteResource} from "@/services/api-client/remote-resource.ts"; | ||||||
| import type {LearningObject} from "@/services/learning-content/learning-object.ts"; | import type {LearningObject} from "@/services/learning-content/learning-object.ts"; | ||||||
| import {getLearningObjectMetadata} from "@/services/learning-content/learning-object-service.ts"; | import {getLearningObjectMetadata} from "@/services/learning-content/learning-object-service.ts"; | ||||||
| 
 | 
 | ||||||
|  | @ -98,6 +98,21 @@ export class LearningPath { | ||||||
|     ) { |     ) { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public get nodesAsList(): LearningPathNode[] { | ||||||
|  |         let list: LearningPathNode[] = []; | ||||||
|  |         let currentNode = this.startNode; | ||||||
|  |         while (currentNode) { | ||||||
|  |             list.push(currentNode); | ||||||
|  |             currentNode = currentNode.transitions.filter(it => it.default)[0]?.next | ||||||
|  |                             || currentNode.transitions[0]?.next; | ||||||
|  |         } | ||||||
|  |         return list; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public get learningObjectsAsList(): RemoteResource<LearningObject[]> { | ||||||
|  |         return RemoteResource.join(this.nodesAsList.map(node => node.learningObject)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     static fromDTO(dto: LearningPathDTO): LearningPath { |     static fromDTO(dto: LearningPathDTO): LearningPath { | ||||||
|         let startNodeDto = dto.nodes.filter(it => it.start_node); |         let startNodeDto = dto.nodes.filter(it => it.start_node); | ||||||
|         if (startNodeDto.length !== 1) { |         if (startNodeDto.length !== 1) { | ||||||
|  |  | ||||||
							
								
								
									
										11
									
								
								frontend/src/utils/response-assertions.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/src/utils/response-assertions.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | ||||||
|  | import {InvalidResponseException, NotFoundException} from "@/services/api-client/api-exceptions.ts"; | ||||||
|  | 
 | ||||||
|  | export function single<T>(list: T[]): T { | ||||||
|  |     if (list.length === 1) { | ||||||
|  |         return list[0]; | ||||||
|  |     } else if (list.length === 0) { | ||||||
|  |         throw new NotFoundException("Expected list with exactly one element, but got an empty list."); | ||||||
|  |     } else { | ||||||
|  |         throw new InvalidResponseException(`Expected list with exactly one element, but got one with ${list.length} elements.`); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,18 +1,59 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|     import {ref} from "vue"; |     import {Language} from "@/services/learning-content/language.ts"; | ||||||
|     const learningObjects = ref([ |     import {getLearningPath} from "@/services/learning-content/learning-path-service.ts"; | ||||||
|  |     import UsingRemoteResource from "@/components/UsingRemoteResource.vue"; | ||||||
|  |     import type {LearningPath} from "@/services/learning-content/learning-path.ts"; | ||||||
|  |     import {onMounted, reactive, watch} from "vue"; | ||||||
|  |     import type {LearningObject} from "@/services/learning-content/learning-object.ts"; | ||||||
|  |     import {useRouter} from "vue-router"; | ||||||
|  |     import type {SuccessState} from "@/services/api-client/remote-resource.ts"; | ||||||
|  | 
 | ||||||
|  |     const router = useRouter(); | ||||||
|  |     const props = defineProps<{hruid: string, language: Language, learningObjectHruid?: string}>() | ||||||
|  | 
 | ||||||
|  |     const learningPathResource = reactive(getLearningPath(props.hruid, props.language)); | ||||||
|  | 
 | ||||||
|  |     if (!props.learningObjectHruid) { | ||||||
|  |         watch(() => learningPathResource.state, (newValue) => { | ||||||
|  |             console.log("state changed!!"); | ||||||
|  |             if (newValue.type === "success") { | ||||||
|  |                 router.push(router.currentRoute.value.path | ||||||
|  |                     + "/" + (newValue as SuccessState<LearningPath>).data.startNode.learningobjectHruid); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     ]) |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|     <v-navigation-drawer> |     <using-remote-resource :resource="learningPathResource" v-slot="learningPath: {data: LearningPath}"> | ||||||
|         <v-list-item title="My Application" subtitle="Vuetify"></v-list-item> |         <v-navigation-drawer> | ||||||
|         <v-divider></v-divider> |             <v-list-item | ||||||
|         <v-list-item link title="List Item 1"></v-list-item> |                 :title="learningPath.data.title" | ||||||
|         <v-list-item link title="List Item 2"></v-list-item> |                 :subtitle="learningPath.data.description" | ||||||
|         <v-list-item link title="List Item 3"></v-list-item> |             ></v-list-item> | ||||||
|     </v-navigation-drawer> |             <v-divider></v-divider> | ||||||
|  | 
 | ||||||
|  |             <div v-if="props.learningObjectHruid"> | ||||||
|  |                 <using-remote-resource | ||||||
|  |                     :resource="learningPath.data.learningObjectsAsList" | ||||||
|  |                     v-slot="learningObjects: {data: LearningObject[]}" | ||||||
|  |                 > | ||||||
|  |                     <v-list-item | ||||||
|  |                         link | ||||||
|  |                         :to="node.key" | ||||||
|  |                         :title="node.title" | ||||||
|  |                         :active="node.key === props.learningObjectHruid" | ||||||
|  |                         v-for="node in learningObjects.data" | ||||||
|  |                     > | ||||||
|  |                         <template v-slot:append> | ||||||
|  |                             {{ node.estimatedTime }}' | ||||||
|  |                         </template> | ||||||
|  |                     </v-list-item> | ||||||
|  |                 </using-remote-resource> | ||||||
|  |             </div> | ||||||
|  |         </v-navigation-drawer> | ||||||
|  |     </using-remote-resource> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger