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>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue