feat(frontend): LearningObjectService en LearningPathService geïmplementeerd.
This commit is contained in:
		
							parent
							
								
									8b0fc4263f
								
							
						
					
					
						commit
						3c3fddb7d0
					
				
					 24 changed files with 375 additions and 84 deletions
				
			
		
							
								
								
									
										39
									
								
								frontend/src/components/UsingRemoteResource.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								frontend/src/components/UsingRemoteResource.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | |||
| <script setup lang="ts" generic="T"> | ||||
|     import {RemoteResource} from "@/services/api-client/remote-resource.ts"; | ||||
|     import {computed} from "vue"; | ||||
|     import {useI18n} from "vue-i18n"; | ||||
| 
 | ||||
|     const props = defineProps<{ | ||||
|         resource: RemoteResource<T> | ||||
|     }>() | ||||
| 
 | ||||
|     const { t } = useI18n(); | ||||
| 
 | ||||
|     const isLoading = computed(() => props.resource.state.type === 'loading'); | ||||
|     const isError = computed(() => props.resource.state.type === 'error'); | ||||
| 
 | ||||
|     // `data` will be correctly inferred as `T` | ||||
|     const data = computed(() => props.resource.data); | ||||
| 
 | ||||
|     const error = computed(() => props.resource.state.type === "error" ? props.resource.state : null); | ||||
|     const errorMessage = computed(() => | ||||
|         error.value?.message ? error.value.message : JSON.stringify(error.value?.error) | ||||
|     ); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|     <div v-if="isLoading"> | ||||
|         <v-progress-circular indeterminate></v-progress-circular> | ||||
|     </div> | ||||
|     <div v-if="isError"> | ||||
|         <v-empty-state | ||||
|             icon="mdi-alert-circle-outline" | ||||
|             text="errorMessage" | ||||
|             :title=t("error_title") | ||||
|         ></v-empty-state> | ||||
|     </div> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped> | ||||
| 
 | ||||
| </style> | ||||
|  | @ -1,3 +1,4 @@ | |||
| { | ||||
|     "welcome": "Willkommen" | ||||
|     "welcome": "Willkommen", | ||||
|     "error_title": "Fehler" | ||||
| } | ||||
|  |  | |||
|  | @ -5,5 +5,6 @@ | |||
|     "assignments": "assignments", | ||||
|     "classes": "classes", | ||||
|     "discussions": "discussions", | ||||
|     "logout": "log out" | ||||
|     "logout": "log out", | ||||
|     "error_title": "Error" | ||||
| } | ||||
|  |  | |||
|  | @ -1,3 +1,4 @@ | |||
| { | ||||
|     "welcome": "Bienvenue" | ||||
|     "welcome": "Bienvenue", | ||||
|     "error_title": "Erreur" | ||||
| } | ||||
|  |  | |||
|  | @ -5,5 +5,6 @@ | |||
|     "assignments": "opdrachten", | ||||
|     "classes": "klassen", | ||||
|     "discussions": "discussies", | ||||
|     "logout": "log uit" | ||||
|     "logout": "log uit", | ||||
|     "error_title": "Fout" | ||||
| } | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ import i18n from "./i18n/i18n.ts"; | |||
| // Components
 | ||||
| import App from "./App.vue"; | ||||
| import router from "./router"; | ||||
| import {aliases, mdi} from "vuetify/iconsets/mdi"; | ||||
| 
 | ||||
| const app = createApp(App); | ||||
| 
 | ||||
|  | @ -23,6 +24,13 @@ document.head.appendChild(link); | |||
| const vuetify = createVuetify({ | ||||
|     components, | ||||
|     directives, | ||||
|     icons: { | ||||
|         defaultSet: "mdi", | ||||
|         aliases, | ||||
|         sets: { | ||||
|             mdi | ||||
|         } | ||||
|     } | ||||
| }); | ||||
| app.use(vuetify); | ||||
| app.use(i18n); | ||||
|  |  | |||
							
								
								
									
										10
									
								
								frontend/src/services/api-client/api-exceptions.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/services/api-client/api-exceptions.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| import type {AxiosResponse} from "axios"; | ||||
| 
 | ||||
| export class HttpErrorStatusException extends Error { | ||||
|     public readonly statusCode: number; | ||||
| 
 | ||||
|     constructor(response: AxiosResponse<any, any>) { | ||||
|         super(`${response.statusText} (${response.status})`); | ||||
|         this.statusCode = response.status; | ||||
|     } | ||||
| } | ||||
|  | @ -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 DeleteEndpoint<PP extends Params, QP extends Params, R> extends RestEndpoint<PP, QP, undefined, R> { | ||||
|     readonly method = "GET"; | ||||
| 
 | ||||
|     public delete(pathParams: PP, queryParams: QP): RemoteResource<R> { | ||||
|         return super.request(pathParams, queryParams, undefined); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										10
									
								
								frontend/src/services/api-client/endpoints/get-endpoint.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/services/api-client/endpoints/get-endpoint.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 GetEndpoint<PP extends Params, QP extends Params, R> extends RestEndpoint<PP, QP, undefined, R> { | ||||
|     readonly method = "GET"; | ||||
| 
 | ||||
|     public get(pathParams: PP, queryParams: QP): RemoteResource<R> { | ||||
|         return super.request(pathParams, queryParams, undefined); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										10
									
								
								frontend/src/services/api-client/endpoints/post-endpoint.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/services/api-client/endpoints/post-endpoint.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 PostEndpoint<PP extends Params, QP extends Params, B, R> extends RestEndpoint<PP, QP, B, R> { | ||||
|     readonly method = "POST"; | ||||
| 
 | ||||
|     public post(pathParams: PP, queryParams: QP, body: B): RemoteResource<R> { | ||||
|         return super.request(pathParams, queryParams, body); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										30
									
								
								frontend/src/services/api-client/endpoints/rest-endpoint.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								frontend/src/services/api-client/endpoints/rest-endpoint.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | |||
| import {RemoteResource} from "@/services/api-client/remote-resource.ts"; | ||||
| import apiClient from "@/services/api-client/api-client.ts"; | ||||
| import {HttpErrorStatusException} from "@/services/api-client/api-exceptions"; | ||||
| 
 | ||||
| export abstract class RestEndpoint<PP extends Params, QP extends Params, B, R> { | ||||
|     public abstract readonly method: "GET" | "POST" | "PUT" | "DELETE"; | ||||
|     constructor(public readonly url: string) { | ||||
|     } | ||||
| 
 | ||||
|     protected request(pathParams: PP, queryParams: QP, body: B): RemoteResource<R> { | ||||
|         let urlFilledIn = this.url; | ||||
|         urlFilledIn.replace(/:(\w+)([/$])/g, (_, key, after) => | ||||
|             (key in pathParams ? encodeURIComponent(pathParams[key]) : `:${key}`) + after | ||||
|         ); | ||||
|         return new RemoteResource(async () => { | ||||
|             const response = await apiClient.request<R>({ | ||||
|                 url: urlFilledIn, | ||||
|                 method: this.method, | ||||
|                 params: queryParams, | ||||
|                 data: body, | ||||
|             }); | ||||
|             if (response.status / 100 !== 2) { | ||||
|                 throw new HttpErrorStatusException(response); | ||||
|             } | ||||
|             return response.data; | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export type Params = {[key: string]: string | number | boolean}; | ||||
							
								
								
									
										70
									
								
								frontend/src/services/api-client/remote-resource.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								frontend/src/services/api-client/remote-resource.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,70 @@ | |||
| export class RemoteResource<T> { | ||||
|     static NOT_LOADED: NotLoadedState = {type: "notLoaded"}; | ||||
|     static LOADING: LoadingState = {type: "loading"}; | ||||
| 
 | ||||
|     private state: NotLoadedState | LoadingState | ErrorState | SuccessState<T> = RemoteResource.NOT_LOADED; | ||||
| 
 | ||||
|     constructor(private readonly requestFn: () => Promise<T>) { | ||||
|     } | ||||
| 
 | ||||
|     public async request(): Promise<T | undefined> { | ||||
|         this.state = RemoteResource.LOADING; | ||||
|         try { | ||||
|             let resource = await this.requestFn(); | ||||
|             this.state = { | ||||
|                 type: "success", | ||||
|                 data: resource | ||||
|             }; | ||||
|             return resource; | ||||
|         } catch (e: any) { | ||||
|             this.state = { | ||||
|                 type: "error", | ||||
|                 errorCode: e.statusCode, | ||||
|                 message: e.message, | ||||
|                 error: e | ||||
|             }; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public startRequestInBackground(): RemoteResource<T> { | ||||
|         this.request().then(); | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     public get data(): T | undefined { | ||||
|         if (this.state.type === "success") { | ||||
|             return this.state.data; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public map<U>(mappingFn: (content: T) => U): RemoteResource<U> { | ||||
|         return new RemoteResource<U>(async () => { | ||||
|             await this.request(); | ||||
|             if (this.state.type === "success") { | ||||
|                 return mappingFn(this.state.data); | ||||
|             } else if (this.state.type === "error") { | ||||
|                 throw this.state.error; | ||||
|             } else { | ||||
|                 throw new Error("Fetched resource, but afterwards, it was neither in a success nor in an error state. " + | ||||
|                     "This should never happen."); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| type NotLoadedState = { | ||||
|     type: "notLoaded" | ||||
| }; | ||||
| type LoadingState = { | ||||
|     type: "loading" | ||||
| }; | ||||
| type ErrorState = { | ||||
|     type: "error", | ||||
|     errorCode?: number, | ||||
|     message?: string, | ||||
|     error: any | ||||
| }; | ||||
| type SuccessState<T> = { | ||||
|     type: "success", | ||||
|     data: T | ||||
| } | ||||
|  | @ -1,4 +1,4 @@ | |||
| import apiClient from "@/services/api-client.ts"; | ||||
| import apiClient from "@/services/api-client/api-client.ts"; | ||||
| import type { FrontendAuthConfig } from "@/services/auth/auth.d.ts"; | ||||
| 
 | ||||
| /** | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ import { User, UserManager } from "oidc-client-ts"; | |||
| import { loadAuthConfig } from "@/services/auth/auth-config-loader.ts"; | ||||
| import authStorage from "./auth-storage.ts"; | ||||
| import { loginRoute } from "@/config.ts"; | ||||
| import apiClient from "@/services/api-client.ts"; | ||||
| import apiClient from "@/services/api-client/api-client.ts"; | ||||
| import router from "@/router"; | ||||
| import type { AxiosError } from "axios"; | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,13 @@ | |||
| import {GetEndpoint} from "@/services/api-client/endpoints/get-endpoint.ts"; | ||||
| import type {LearningObject} from "@/services/learning-content/learning-object.ts"; | ||||
| import type {Language} from "@/services/learning-content/language.ts"; | ||||
| import type {RemoteResource} from "@/services/api-client/remote-resource.ts"; | ||||
| 
 | ||||
| const getLearningObjectMetadataEndpoint = new GetEndpoint<{hruid: string}, {language: Language, version: number}, LearningObject>( | ||||
|     "/learningObject/:hruid" | ||||
| ); | ||||
| 
 | ||||
| export function getLearningObjectMetadata(hruid: string, language: Language, version: number): RemoteResource<LearningObject> { | ||||
|     return getLearningObjectMetadataEndpoint | ||||
|         .get({hruid}, {language, version}); | ||||
| } | ||||
							
								
								
									
										33
									
								
								frontend/src/services/learning-content/learning-object.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								frontend/src/services/learning-content/learning-object.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | |||
| import type {Language} from "@/services/learning-content/language.ts"; | ||||
| 
 | ||||
| export interface EducationalGoal { | ||||
|     source: string; | ||||
|     id: string; | ||||
| } | ||||
| 
 | ||||
| export interface ReturnValue { | ||||
|     callback_url: string; | ||||
|     callback_schema: Record<string, any>; | ||||
| } | ||||
| 
 | ||||
| export interface LearningObject { | ||||
|     key: string; | ||||
|     _id: string; | ||||
|     uuid: string; | ||||
|     version: number; | ||||
|     title: string; | ||||
|     htmlUrl: string; | ||||
|     language: Language; | ||||
|     difficulty: number; | ||||
|     estimatedTime?: number; | ||||
|     available: boolean; | ||||
|     teacherExclusive: boolean; | ||||
|     educationalGoals: EducationalGoal[]; | ||||
|     keywords: string[]; | ||||
|     description: string; | ||||
|     targetAges: number[]; | ||||
|     contentType: string; | ||||
|     contentLocation?: string; | ||||
|     skosConcepts?: string[]; | ||||
|     returnValue?: ReturnValue; | ||||
| } | ||||
|  | @ -0,0 +1,13 @@ | |||
| import {GetEndpoint} from "@/services/api-client/endpoints/get-endpoint.ts"; | ||||
| import {LearningPath, type LearningPathDTO} from "@/services/learning-content/learning-path.ts"; | ||||
| import type {RemoteResource} from "@/services/api-client/remote-resource.ts"; | ||||
| 
 | ||||
| const searchLearningPathsEndpoint = new GetEndpoint<{}, {query: string}, LearningPathDTO[]>( | ||||
|     "/learningObjects/:query" | ||||
| ); | ||||
| 
 | ||||
| export function searchLearningPaths(query: string): RemoteResource<LearningPath[]> { | ||||
|     return searchLearningPathsEndpoint | ||||
|         .get({}, {query: query}) | ||||
|         .map(dtos => dtos.map(dto => LearningPath.fromDTO(dto))); | ||||
| } | ||||
							
								
								
									
										118
									
								
								frontend/src/services/learning-content/learning-path.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								frontend/src/services/learning-content/learning-path.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,118 @@ | |||
| import type {Language} from "@/services/learning-content/language.ts"; | ||||
| import type {RemoteResource} from "@/services/api-client/remote-resource.ts"; | ||||
| import type {LearningObject} from "@/services/learning-content/learning-object.ts"; | ||||
| import {getLearningObjectMetadata} from "@/services/learning-content/learning-object-service.ts"; | ||||
| 
 | ||||
| export interface LearningPathDTO { | ||||
|     language: string; | ||||
|     hruid: string; | ||||
|     title: string; | ||||
|     description: string; | ||||
|     image?: string; // Image might be missing, so it's optional
 | ||||
|     num_nodes: number; | ||||
|     num_nodes_left: number; | ||||
|     nodes: LearningPathNodeDTO[]; | ||||
|     keywords: string; | ||||
|     target_ages: number[]; | ||||
|     min_age: number; | ||||
|     max_age: number; | ||||
|     __order: number; | ||||
| } | ||||
| 
 | ||||
| interface LearningPathNodeDTO { | ||||
|     _id: string; | ||||
|     learningobject_hruid: string; | ||||
|     version: number; | ||||
|     language: Language; | ||||
|     start_node?: boolean; | ||||
|     transitions: LearningPathTransitionDTO[]; | ||||
|     created_at: string; | ||||
|     updatedAt: string; | ||||
|     done?: boolean; // True if a submission exists for this node by the user for whom the learning path is customized.
 | ||||
| } | ||||
| 
 | ||||
| interface LearningPathTransitionDTO { | ||||
|     default: boolean; | ||||
|     _id: string; | ||||
|     next: { | ||||
|         _id: string; | ||||
|         hruid: string; | ||||
|         version: number; | ||||
|         language: string; | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| export class LearningPathNode { | ||||
|     public learningObject: RemoteResource<LearningObject> | ||||
| 
 | ||||
|     constructor( | ||||
|         public readonly learningobjectHruid: string, | ||||
|         public readonly version: number, | ||||
|         public readonly language: Language, | ||||
|         public readonly transitions: {next: LearningPathNode, default: boolean}[], | ||||
|         public readonly createdAt: Date, | ||||
|         public readonly updatedAt: Date | ||||
|     ) { | ||||
|         this.learningObject = getLearningObjectMetadata(learningobjectHruid, language, version); | ||||
|     } | ||||
| 
 | ||||
|     static fromDTOAndOtherNodes(dto: LearningPathNodeDTO, otherNodes: LearningPathNodeDTO[]): LearningPathNode { | ||||
|         return new LearningPathNode( | ||||
|             dto.learningobject_hruid, | ||||
|             dto.version, | ||||
|             dto.language, | ||||
|             dto.transitions.map(transDto => { | ||||
|                 let nextNodeDto = otherNodes.filter(it => | ||||
|                     it.learningobject_hruid === transDto.next.hruid | ||||
|                     && it.language === transDto.next.language | ||||
|                     && it.version === transDto.next.version | ||||
|                 ); | ||||
|                 if (nextNodeDto.length !== 1) { | ||||
|                     throw new Error(`Invalid learning path! There is a transition to node` | ||||
|                         + `${transDto.next.hruid}/${transDto.next.language}/${transDto.next.version}, but there are` | ||||
|                         + `${nextNodeDto.length} such nodes.`); | ||||
|                 } | ||||
|                 return { | ||||
|                     next: LearningPathNode.fromDTOAndOtherNodes(nextNodeDto[0], otherNodes), | ||||
|                     default: transDto.default | ||||
|                 } | ||||
|             }), | ||||
|             new Date(dto.created_at), | ||||
|             new Date(dto.updatedAt), | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class LearningPath { | ||||
|     constructor( | ||||
|         public readonly language: string, | ||||
|         public readonly hruid: string, | ||||
|         public readonly title: string, | ||||
|         public readonly description: string, | ||||
|         public readonly amountOfNodes: number, | ||||
|         public readonly amountOfNodesLeft: number, | ||||
|         public readonly keywords: string[], | ||||
|         public readonly targetAges: {min: number; max: number}, | ||||
|         public readonly startNode: LearningPathNode, | ||||
|         public readonly image?: string // Image might be missing, so it's optional
 | ||||
|     ) { | ||||
|     } | ||||
| 
 | ||||
|     static fromDTO(dto: LearningPathDTO): LearningPath { | ||||
|         let startNodeDto = dto.nodes.filter(it => it.start_node); | ||||
|         if (startNodeDto.length !== 1) { | ||||
|             throw new Error(`Invalid learning path! Expected precisely one start node, but there were ${startNodeDto.length}.`); | ||||
|         } | ||||
|         return new LearningPath( | ||||
|             dto.language, | ||||
|             dto.hruid, | ||||
|             dto.title, | ||||
|             dto.description, | ||||
|             dto.num_nodes, | ||||
|             dto.num_nodes_left, | ||||
|             dto.keywords.split(' '), | ||||
|             {min: dto.min_age, max: dto.max_age}, | ||||
|             LearningPathNode.fromDTOAndOtherNodes(startNodeDto[0], dto.nodes) | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | @ -1,37 +0,0 @@ | |||
| import type {Language} from "@/services/learning-paths/language.ts"; | ||||
| 
 | ||||
| export interface LearningPathIdentifier { | ||||
|     hruid: string; | ||||
|     language: Language; | ||||
| } | ||||
| 
 | ||||
| export interface EducationalGoal { | ||||
|     source: string; | ||||
|     id: string; | ||||
| } | ||||
| 
 | ||||
| export interface ReturnValue { | ||||
|     callback_url: string; | ||||
|     callback_schema: Record<string, any>; | ||||
| } | ||||
| 
 | ||||
| export interface LearningObjectMetadata { | ||||
|     _id: string; | ||||
|     uuid: string; | ||||
|     hruid: string; | ||||
|     version: number; | ||||
|     language: Language; | ||||
|     title: string; | ||||
|     description: string; | ||||
|     difficulty: number; | ||||
|     estimated_time: number; | ||||
|     available: boolean; | ||||
|     teacher_exclusive: boolean; | ||||
|     educational_goals: EducationalGoal[]; | ||||
|     keywords: string[]; | ||||
|     target_ages: number[]; | ||||
|     content_type: string; // Markdown, image, etc.
 | ||||
|     content_location?: string; | ||||
|     skos_concepts?: string[]; | ||||
|     return_value?: ReturnValue; | ||||
| } | ||||
|  | @ -1,40 +0,0 @@ | |||
| import type {Language} from "@/services/learning-paths/language.ts"; | ||||
| 
 | ||||
| export interface LearningPath { | ||||
|     language: string; | ||||
|     hruid: string; | ||||
|     title: string; | ||||
|     description: string; | ||||
|     image?: string; // Image might be missing, so it's optional
 | ||||
|     num_nodes: number; | ||||
|     num_nodes_left: number; | ||||
|     nodes: LearningObjectNode[]; | ||||
|     keywords: string; | ||||
|     target_ages: number[]; | ||||
|     min_age: number; | ||||
|     max_age: number; | ||||
|     __order: number; | ||||
| } | ||||
| 
 | ||||
| export interface LearningObjectNode { | ||||
|     _id: string; | ||||
|     learningobject_hruid: string; | ||||
|     version: number; | ||||
|     language: Language; | ||||
|     start_node?: boolean; | ||||
|     transitions: Transition[]; | ||||
|     created_at: string; | ||||
|     updatedAt: string; | ||||
|     done?: boolean; // True if a submission exists for this node by the user for whom the learning path is customized.
 | ||||
| } | ||||
| 
 | ||||
| export interface Transition { | ||||
|     default: boolean; | ||||
|     _id: string; | ||||
|     next: { | ||||
|         _id: string; | ||||
|         hruid: string; | ||||
|         version: number; | ||||
|         language: string; | ||||
|     }; | ||||
| } | ||||
|  | @ -1,6 +1,6 @@ | |||
| <script setup lang="ts"> | ||||
|     import auth from "@/services/auth/auth-service.ts"; | ||||
|     import apiClient from "@/services/api-client.ts"; | ||||
|     import apiClient from "@/services/api-client/api-client.ts"; | ||||
|     import { ref } from "vue"; | ||||
| 
 | ||||
|     const testResponse = ref(null); | ||||
|  |  | |||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger