feat(frontend): basisimplementatie leerobject upload-UI
This commit is contained in:
		
							parent
							
								
									6600441b08
								
							
						
					
					
						commit
						be1091544c
					
				
					 11 changed files with 311 additions and 40 deletions
				
			
		|  | @ -2,7 +2,6 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
| import { LearningObject } from '../../entities/content/learning-object.entity.js'; | import { LearningObject } from '../../entities/content/learning-object.entity.js'; | ||||||
| import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; | import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; | ||||||
| import { Language } from '@dwengo-1/common/util/language'; | import { Language } from '@dwengo-1/common/util/language'; | ||||||
| import { Teacher } from '../../entities/users/teacher.entity.js'; |  | ||||||
| 
 | 
 | ||||||
| export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> { | export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> { | ||||||
|     public async findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> { |     public async findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> { | ||||||
|  | @ -35,7 +34,11 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj | ||||||
| 
 | 
 | ||||||
|     public async findAllByAdmin(adminUsername: string): Promise<LearningObject[]> { |     public async findAllByAdmin(adminUsername: string): Promise<LearningObject[]> { | ||||||
|         return this.find( |         return this.find( | ||||||
|             { admins: { $contains: adminUsername } }, |             { | ||||||
|  |                 admins: { | ||||||
|  |                     username: adminUsername | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|             { populate: ['admins'] } // Make sure to load admin relations
 |             { populate: ['admins'] } // Make sure to load admin relations
 | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -9,7 +9,6 @@ import { | ||||||
| } from '@dwengo-1/common/interfaces/learning-content'; | } from '@dwengo-1/common/interfaces/learning-content'; | ||||||
| import {getLearningObjectRepository, getTeacherRepository} from "../../data/repositories"; | import {getLearningObjectRepository, getTeacherRepository} from "../../data/repositories"; | ||||||
| import {processLearningObjectZip} from "./learning-object-zip-processing-service"; | import {processLearningObjectZip} from "./learning-object-zip-processing-service"; | ||||||
| import {BadRequestException} from "../../exceptions/bad-request-exception"; |  | ||||||
| import {LearningObject} from "../../entities/content/learning-object.entity"; | import {LearningObject} from "../../entities/content/learning-object.entity"; | ||||||
| 
 | 
 | ||||||
| function getProvider(id: LearningObjectIdentifierDTO): LearningObjectProvider { | function getProvider(id: LearningObjectIdentifierDTO): LearningObjectProvider { | ||||||
|  | @ -67,7 +66,6 @@ const learningObjectService = { | ||||||
|         const learningObjectRepository = getLearningObjectRepository(); |         const learningObjectRepository = getLearningObjectRepository(); | ||||||
|         const learningObject = await processLearningObjectZip(learningObjectPath); |         const learningObject = await processLearningObjectZip(learningObjectPath); | ||||||
| 
 | 
 | ||||||
|         console.log(learningObject); |  | ||||||
|         if (!learningObject.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { |         if (!learningObject.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { | ||||||
|             learningObject.hruid = getEnvVar(envVars.UserContentPrefix) + learningObject.hruid; |             learningObject.hruid = getEnvVar(envVars.UserContentPrefix) + learningObject.hruid; | ||||||
|         } |         } | ||||||
|  | @ -75,10 +73,10 @@ const learningObjectService = { | ||||||
|         // 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( | ||||||
|             admins.map(it => teacherRepo.findByUsername(it)) |             admins.map(async it => teacherRepo.findByUsername(it)) | ||||||
|         ); |         ); | ||||||
|         adminTeachers.forEach(it => { |         adminTeachers.forEach(it => { | ||||||
|             if (it != null) { |             if (it !== null) { | ||||||
|                 learningObject.admins.add(it); |                 learningObject.admins.add(it); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ import {LearningObject} from "../../entities/content/learning-object.entity"; | ||||||
| import {getAttachmentRepository, getLearningObjectRepository} from "../../data/repositories"; | import {getAttachmentRepository, getLearningObjectRepository} from "../../data/repositories"; | ||||||
| import {BadRequestException} from "../../exceptions/bad-request-exception"; | import {BadRequestException} from "../../exceptions/bad-request-exception"; | ||||||
| import {LearningObjectMetadata} from "@dwengo-1/common/dist/interfaces/learning-content"; | import {LearningObjectMetadata} from "@dwengo-1/common/dist/interfaces/learning-content"; | ||||||
|  | import { ReturnValue } from '../../entities/content/return-value.entity'; | ||||||
| 
 | 
 | ||||||
| const METADATA_PATH_REGEX = /.*[/^]metadata\.json$/; | const METADATA_PATH_REGEX = /.*[/^]metadata\.json$/; | ||||||
| const CONTENT_PATH_REGEX = /.*[/^]content\.[a-zA-Z]*$/; | const CONTENT_PATH_REGEX = /.*[/^]content\.[a-zA-Z]*$/; | ||||||
|  | @ -13,33 +14,38 @@ const CONTENT_PATH_REGEX = /.*[/^]content\.[a-zA-Z]*$/; | ||||||
|  * @param filePath Path of the zip file to process. |  * @param filePath Path of the zip file to process. | ||||||
|  */ |  */ | ||||||
| export async function processLearningObjectZip(filePath: string): Promise<LearningObject> { | export async function processLearningObjectZip(filePath: string): Promise<LearningObject> { | ||||||
|     const learningObjectRepo = getLearningObjectRepository(); |     let zip: unzipper.CentralDirectory; | ||||||
|     const attachmentRepo = getAttachmentRepository(); |     try { | ||||||
|  |         zip = await unzipper.Open.file(filePath); | ||||||
|  |     } catch(_: unknown) { | ||||||
|  |         throw new BadRequestException("invalid_zip"); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     const zip = await unzipper.Open.file(filePath); |  | ||||||
| 
 | 
 | ||||||
|     let metadata: LearningObjectMetadata | undefined = undefined; |     let metadata: LearningObjectMetadata | undefined = undefined; | ||||||
|     const attachments: {name: string, content: Buffer}[] = []; |     const attachments: {name: string, content: Buffer}[] = []; | ||||||
|     let content: Buffer | undefined = undefined; |     let content: Buffer | undefined = undefined; | ||||||
| 
 | 
 | ||||||
|     if (zip.files.length == 0) { |     if (zip.files.length === 0) { | ||||||
|         throw new BadRequestException("empty_zip") |         throw new BadRequestException("empty_zip") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     for (const file of zip.files) { |     await Promise.all( | ||||||
|         if (file.type !== "Directory") { |         zip.files.map(async file => { | ||||||
|             if (METADATA_PATH_REGEX.test(file.path)) { |             if (file.type !== "Directory") { | ||||||
|                 metadata = await processMetadataJson(file); |                 if (METADATA_PATH_REGEX.test(file.path)) { | ||||||
|             } else if (CONTENT_PATH_REGEX.test(file.path)) { |                     metadata = await processMetadataJson(file); | ||||||
|                 content = await processFile(file); |                 } else if (CONTENT_PATH_REGEX.test(file.path)) { | ||||||
|             } else { |                     content = await processFile(file); | ||||||
|                 attachments.push({ |                 } else { | ||||||
|                     name: file.path, |                     attachments.push({ | ||||||
|                     content: await processFile(file) |                         name: file.path, | ||||||
|                 }); |                         content: await processFile(file) | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|         } |         }) | ||||||
|     } |     ); | ||||||
| 
 | 
 | ||||||
|     if (!metadata) { |     if (!metadata) { | ||||||
|         throw new BadRequestException("missing_metadata"); |         throw new BadRequestException("missing_metadata"); | ||||||
|  | @ -49,20 +55,30 @@ export async function processLearningObjectZip(filePath: string): Promise<Learni | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |     const learningObject = createLearningObject(metadata, content, attachments); | ||||||
|  | 
 | ||||||
|  |     return learningObject; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function createLearningObject( | ||||||
|  |     metadata: LearningObjectMetadata, content: Buffer, attachments: { name: string; content: Buffer; }[] | ||||||
|  | ): LearningObject { | ||||||
|  |     const learningObjectRepo = getLearningObjectRepository(); | ||||||
|  |     const attachmentRepo = getAttachmentRepository(); | ||||||
|     const learningObject = learningObjectRepo.create({ |     const learningObject = learningObjectRepo.create({ | ||||||
|         admins: [], |         admins: [], | ||||||
|         available: metadata.available ?? true, |         available: metadata.available ?? true, | ||||||
|         content: content, |         content: content, | ||||||
|         contentType: metadata.content_type, |         contentType: metadata.content_type, | ||||||
|         copyright: metadata.copyright, |         copyright: metadata.copyright ?? "", | ||||||
|         description: metadata.description, |         description: metadata.description ?? "", | ||||||
|         educationalGoals: metadata.educational_goals, |         educationalGoals: metadata.educational_goals ?? [], | ||||||
|         hruid: metadata.hruid, |         hruid: metadata.hruid, | ||||||
|         keywords: metadata.keywords, |         keywords: metadata.keywords, | ||||||
|         language: metadata.language, |         language: metadata.language, | ||||||
|         license: "", |         license: metadata.license ?? "", | ||||||
|         returnValue: metadata.return_value, |         returnValue: metadata.return_value ?? new ReturnValue(), | ||||||
|         skosConcepts: metadata.skos_concepts, |         skosConcepts: metadata.skos_concepts ?? [], | ||||||
|         teacherExclusive: metadata.teacher_exclusive, |         teacherExclusive: metadata.teacher_exclusive, | ||||||
|         title: metadata.title, |         title: metadata.title, | ||||||
|         version: metadata.version |         version: metadata.version | ||||||
|  | @ -72,9 +88,8 @@ export async function processLearningObjectZip(filePath: string): Promise<Learni | ||||||
|         content: it.content, |         content: it.content, | ||||||
|         mimeType: mime.lookup(it.name) || "text/plain", |         mimeType: mime.lookup(it.name) || "text/plain", | ||||||
|         learningObject |         learningObject | ||||||
|     })) |     })); | ||||||
|     attachmentEntities.forEach(it => learningObject.attachments.add(it)); |     attachmentEntities.forEach(it => { learningObject.attachments.add(it); }); | ||||||
| 
 |  | ||||||
|     return learningObject; |     return learningObject; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -37,6 +37,28 @@ export abstract class BaseController { | ||||||
|         return response.data; |         return response.data; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Sends a POST-request with a form-data body with the given file. | ||||||
|  |      * | ||||||
|  |      * @param path Relative path in the api to send the request to. | ||||||
|  |      * @param formFieldName The name of the form field in which the file should be. | ||||||
|  |      * @param file The file to upload. | ||||||
|  |      * @param queryParams The query parameters. | ||||||
|  |      * @returns The response the POST request generated. | ||||||
|  |      */ | ||||||
|  |     protected async postFile<T>(path: string, formFieldName: string, file: File, queryParams?: QueryParams): Promise<T> { | ||||||
|  |         const formData = new FormData(); | ||||||
|  |         formData.append(formFieldName, file); | ||||||
|  |         const response = await apiClient.post<T>(this.absolutePathFor(path), formData, { | ||||||
|  |             params: queryParams, | ||||||
|  |             headers: { | ||||||
|  |                 'Content-Type': 'multipart/form-data' | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |         BaseController.assertSuccessResponse(response) | ||||||
|  |         return response.data; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     protected async delete<T>(path: string, queryParams?: QueryParams): Promise<T> { |     protected async delete<T>(path: string, queryParams?: QueryParams): Promise<T> { | ||||||
|         const response = await apiClient.delete<T>(this.absolutePathFor(path), { params: queryParams }); |         const response = await apiClient.delete<T>(this.absolutePathFor(path), { params: queryParams }); | ||||||
|         BaseController.assertSuccessResponse(response); |         BaseController.assertSuccessResponse(response); | ||||||
|  |  | ||||||
|  | @ -14,4 +14,12 @@ export class LearningObjectController extends BaseController { | ||||||
|     async getHTML(hruid: string, language: Language, version: number): Promise<Document> { |     async getHTML(hruid: string, language: Language, version: number): Promise<Document> { | ||||||
|         return this.get<Document>(`/${hruid}/html`, { language, version }, "document"); |         return this.get<Document>(`/${hruid}/html`, { language, version }, "document"); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     async getAllAdministratedBy(admin: string): Promise<LearningObject[]> { | ||||||
|  |         return this.get<LearningObject[]>("/", { admin }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async upload(learningObjectZip: File): Promise<LearningObject> { | ||||||
|  |         return this.postFile<LearningObject>("/", "learningObject", learningObjectZip); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,9 +1,10 @@ | ||||||
| import { type MaybeRefOrGetter, toValue } from "vue"; | import { type MaybeRefOrGetter, toValue } from "vue"; | ||||||
| import type { Language } from "@/data-objects/language.ts"; | import type { Language } from "@/data-objects/language.ts"; | ||||||
| import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; | import { useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; | ||||||
| import { getLearningObjectController } from "@/controllers/controllers.ts"; | import { getLearningObjectController } from "@/controllers/controllers.ts"; | ||||||
| import type { LearningObject } from "@/data-objects/learning-objects/learning-object.ts"; | import type { LearningObject } from "@/data-objects/learning-objects/learning-object.ts"; | ||||||
| import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; | import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; | ||||||
|  | import type { AxiosError } from "axios"; | ||||||
| 
 | 
 | ||||||
| export const LEARNING_OBJECT_KEY = "learningObject"; | export const LEARNING_OBJECT_KEY = "learningObject"; | ||||||
| const learningObjectController = getLearningObjectController(); | const learningObjectController = getLearningObjectController(); | ||||||
|  | @ -24,15 +25,15 @@ export function useLearningObjectMetadataQuery( | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function useLearningObjectHTMLQuery( | export function useLearningObjectHTMLQuery( | ||||||
|     hruid: MaybeRefOrGetter<string>, |     hruid: MaybeRefOrGetter<string | undefined>, | ||||||
|     language: MaybeRefOrGetter<Language>, |     language: MaybeRefOrGetter<Language | undefined>, | ||||||
|     version: MaybeRefOrGetter<number>, |     version: MaybeRefOrGetter<number | undefined>, | ||||||
| ): UseQueryReturnType<Document, Error> { | ): UseQueryReturnType<Document, Error> { | ||||||
|     return useQuery({ |     return useQuery({ | ||||||
|         queryKey: [LEARNING_OBJECT_KEY, "html", hruid, language, version], |         queryKey: [LEARNING_OBJECT_KEY, "html", hruid, language, version], | ||||||
|         queryFn: async () => { |         queryFn: async () => { | ||||||
|             const [hruidVal, languageVal, versionVal] = [toValue(hruid), toValue(language), toValue(version)]; |             const [hruidVal, languageVal, versionVal] = [toValue(hruid), toValue(language), toValue(version)]; | ||||||
|             return learningObjectController.getHTML(hruidVal, languageVal, versionVal); |             return learningObjectController.getHTML(hruidVal!, languageVal!, versionVal!); | ||||||
|         }, |         }, | ||||||
|         enabled: () => Boolean(toValue(hruid)) && Boolean(toValue(language)) && Boolean(toValue(version)), |         enabled: () => Boolean(toValue(hruid)) && Boolean(toValue(language)) && Boolean(toValue(version)), | ||||||
|     }); |     }); | ||||||
|  | @ -55,3 +56,25 @@ export function useLearningObjectListForPathQuery( | ||||||
|         enabled: () => Boolean(toValue(learningPath)), |         enabled: () => Boolean(toValue(learningPath)), | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export function useLearningObjectListForAdminQuery( | ||||||
|  |     admin: MaybeRefOrGetter<string | undefined> | ||||||
|  | ): UseQueryReturnType<LearningObject[], Error> { | ||||||
|  |     return useQuery({ | ||||||
|  |         queryKey: [LEARNING_OBJECT_KEY, "forAdmin", admin], | ||||||
|  |         queryFn: async () => { | ||||||
|  |             const adminVal = toValue(admin); | ||||||
|  |             return await learningObjectController.getAllAdministratedBy(adminVal!); | ||||||
|  |         }, | ||||||
|  |         enabled: () => toValue(admin) !== undefined | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useUploadLearningObjectMutation(): UseMutationReturnType<LearningObject, AxiosError, {learningObjectZip: File}, unknown> { | ||||||
|  |     const queryClient = useQueryClient(); | ||||||
|  | 
 | ||||||
|  |     return useMutation({ | ||||||
|  |         mutationFn: async ({ learningObjectZip }) => await learningObjectController.upload(learningObjectZip), | ||||||
|  |         onSuccess: async () => { await queryClient.invalidateQueries({queryKey: [LEARNING_OBJECT_KEY, "forAdmin"]}); } | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -14,6 +14,7 @@ import UserHomePage from "@/views/homepage/UserHomePage.vue"; | ||||||
| import SingleTheme from "@/views/SingleTheme.vue"; | import SingleTheme from "@/views/SingleTheme.vue"; | ||||||
| import LearningObjectView from "@/views/learning-paths/learning-object/LearningObjectView.vue"; | import LearningObjectView from "@/views/learning-paths/learning-object/LearningObjectView.vue"; | ||||||
| import authService from "@/services/auth/auth-service"; | import authService from "@/services/auth/auth-service"; | ||||||
|  | import OwnLearningContentPage from "@/views/own-learning-content/OwnLearningContentPage.vue"; | ||||||
| 
 | 
 | ||||||
| const router = createRouter({ | const router = createRouter({ | ||||||
|     history: createWebHistory(import.meta.env.BASE_URL), |     history: createWebHistory(import.meta.env.BASE_URL), | ||||||
|  | @ -114,6 +115,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", | ||||||
|  |  | ||||||
|  | @ -0,0 +1,76 @@ | ||||||
|  | <script setup lang="ts"> | ||||||
|  |     import { useUploadLearningObjectMutation } from '@/queries/learning-objects'; | ||||||
|  |     import { ref, watch, type Ref } from 'vue'; | ||||||
|  |     import { useI18n } from 'vue-i18n'; | ||||||
|  |     import { VFileUpload } from 'vuetify/labs/VFileUpload'; | ||||||
|  | 
 | ||||||
|  |     const { t } = useI18n(); | ||||||
|  | 
 | ||||||
|  |     const dialogOpen = ref(false); | ||||||
|  | 
 | ||||||
|  |     interface ContainsErrorString { | ||||||
|  |         error: string; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const fileToUpload: Ref<File | undefined> = ref(undefined); | ||||||
|  | 
 | ||||||
|  |     const { isPending, error, isError, isSuccess, mutate } = useUploadLearningObjectMutation(); | ||||||
|  | 
 | ||||||
|  |     watch(isSuccess, (newIsSuccess) => { | ||||||
|  |         if (newIsSuccess) { | ||||||
|  |             dialogOpen.value = false; | ||||||
|  |             fileToUpload.value = undefined; | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     function uploadFile() { | ||||||
|  |         if (fileToUpload.value) { | ||||||
|  |             mutate({learningObjectZip: fileToUpload.value}); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | </script> | ||||||
|  | <template> | ||||||
|  |     <v-dialog max-width="500" v-model="dialogOpen"> | ||||||
|  |         <template v-slot:activator="{ props: activatorProps }"> | ||||||
|  |             <v-fab icon="mdi mdi-plus" v-bind="activatorProps"></v-fab> | ||||||
|  |         </template> | ||||||
|  | 
 | ||||||
|  |         <template v-slot:default="{ isActive }"> | ||||||
|  |             <v-card :title="t('learning_object_upload_title')"> | ||||||
|  |                 <v-card-text> | ||||||
|  |                     <v-file-upload | ||||||
|  |                         :browse-text="t('upload_browse')" | ||||||
|  |                         :divider-text="t('upload_divider')" | ||||||
|  |                         icon="mdi-upload" | ||||||
|  |                         :title="t('upload_drag_and_drop')" | ||||||
|  |                         v-model="fileToUpload" | ||||||
|  |                         :disabled="isPending" | ||||||
|  |                     ></v-file-upload> | ||||||
|  |                     <v-alert | ||||||
|  |                         v-if="error" | ||||||
|  |                         icon="mdi mdi-alert-circle" | ||||||
|  |                         type="error" | ||||||
|  |                         :title="t('upload_failed')" | ||||||
|  |                         :text="t((error.response?.data as ContainsErrorString).error ?? error.message)" | ||||||
|  |                     ></v-alert> | ||||||
|  |                 </v-card-text> | ||||||
|  | 
 | ||||||
|  |                 <v-card-actions> | ||||||
|  |                     <v-spacer></v-spacer> | ||||||
|  |                     <v-btn | ||||||
|  |                         :text="t('cancel')" | ||||||
|  |                         @click="isActive.value = false" | ||||||
|  |                     ></v-btn> | ||||||
|  |                     <v-btn | ||||||
|  |                         :text="t('upload')" | ||||||
|  |                         @click="uploadFile()" | ||||||
|  |                         :loading="isPending" | ||||||
|  |                         :disabled="!fileToUpload" | ||||||
|  |                     ></v-btn> | ||||||
|  |                 </v-card-actions> | ||||||
|  |             </v-card> | ||||||
|  |         </template> | ||||||
|  |     </v-dialog> | ||||||
|  | </template> | ||||||
|  | <style scoped> | ||||||
|  | </style> | ||||||
|  | @ -1,11 +1,52 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|  |     import {useLearningObjectListForAdminQuery} from "@/queries/learning-objects.ts"; | ||||||
|  |     import OwnLearningObjectsView from "@/views/own-learning-content/OwnLearningObjectsView.vue" | ||||||
|  |     import OwnLearningPathsView from "@/views/own-learning-content/OwnLearningPathsView.vue" | ||||||
|  |     import authService from "@/services/auth/auth-service.ts"; | ||||||
|  |     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||||
|  |     import type { LearningObject } from "@/data-objects/learning-objects/learning-object"; | ||||||
|  |     import { ref, type Ref } from "vue"; | ||||||
|  | import { useI18n } from "vue-i18n"; | ||||||
| 
 | 
 | ||||||
|  |     const { t } = useI18n(); | ||||||
|  | 
 | ||||||
|  |     const learningObjectsQuery = | ||||||
|  |         useLearningObjectListForAdminQuery(authService.authState.user?.profile.preferred_username); | ||||||
|  | 
 | ||||||
|  |     type Tab = "learningObjects" | "learningPaths"; | ||||||
|  |     const tab: Ref<Tab> = ref("learningObjects"); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|  |     <div class="tab-pane-container"> | ||||||
|  |         <v-tabs v-model="tab"> | ||||||
|  |         <v-tab value="learningObjects">{{ t('learningObjects') }}</v-tab> | ||||||
|  |         <v-tab value="learningPaths">{{ t('learningPaths') }}</v-tab> | ||||||
|  |         </v-tabs> | ||||||
| 
 | 
 | ||||||
|  |         <v-tabs-window v-model="tab" class="main-content"> | ||||||
|  |             <v-tabs-window-item value="learningObjects" class="main-content"> | ||||||
|  |                 <using-query-result | ||||||
|  |                     :query-result="learningObjectsQuery" | ||||||
|  |                     v-slot="response: { data: LearningObject[] }" | ||||||
|  |                 > | ||||||
|  |                     <own-learning-objects-view :learningObjects="response.data"></own-learning-objects-view> | ||||||
|  |                 </using-query-result> | ||||||
|  |             </v-tabs-window-item> | ||||||
|  |             <v-tabs-window-item value="learningPaths"> | ||||||
|  |                 <own-learning-paths-view/> | ||||||
|  |             </v-tabs-window-item> | ||||||
|  |         </v-tabs-window> | ||||||
|  |     </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
| 
 |     .tab-pane-container { | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: column; | ||||||
|  |         height: 100%; | ||||||
|  |     } | ||||||
|  |     .main-content { | ||||||
|  |         flex: 1; | ||||||
|  |     } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -1,11 +1,80 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|  |     import type { LearningObject } from '@/data-objects/learning-objects/learning-object'; | ||||||
|  |     import LearningObjectUploadButton from '@/views/own-learning-content/LearningObjectUploadButton.vue' | ||||||
|  |     import LearningObjectContentView from '../learning-paths/learning-object/content/LearningObjectContentView.vue'; | ||||||
|  |     import { computed, ref, type Ref } from 'vue'; | ||||||
|  |     import { useI18n } from 'vue-i18n'; | ||||||
|  | import { useLearningObjectHTMLQuery } from '@/queries/learning-objects'; | ||||||
|  | import UsingQueryResult from '@/components/UsingQueryResult.vue'; | ||||||
| 
 | 
 | ||||||
|  |     const { t } = useI18n(); | ||||||
|  |     const props = defineProps<{ | ||||||
|  |         learningObjects: LearningObject[] | ||||||
|  |     }>(); | ||||||
|  | 
 | ||||||
|  |     const tableHeaders = [ | ||||||
|  |         { title: t("hruid"), width: "250px", key: "key" }, | ||||||
|  |         { title: t("language"), width: "50px", key: "language" }, | ||||||
|  |         { title: t("version"), width: "50px", key: "version" }, | ||||||
|  |         { title: t("title"), key: "title" } | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     const selectedLearningObjects: Ref<LearningObject[]> = ref([]) | ||||||
|  | 
 | ||||||
|  |     const selectedLearningObject = computed(() => | ||||||
|  |         selectedLearningObjects.value ? selectedLearningObjects.value[0] : undefined | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     const learningObjectQueryResult = useLearningObjectHTMLQuery( | ||||||
|  |         () => selectedLearningObject.value?.key, | ||||||
|  |         () => selectedLearningObject.value?.language, | ||||||
|  |         () => selectedLearningObject.value?.version | ||||||
|  |     ); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
| 
 |     <div class="root"> | ||||||
|  |         <v-data-table | ||||||
|  |             class="table" | ||||||
|  |             v-model="selectedLearningObjects" | ||||||
|  |             :items="props.learningObjects" | ||||||
|  |             :headers="tableHeaders" | ||||||
|  |             select-strategy="single" | ||||||
|  |             show-select | ||||||
|  |             return-object | ||||||
|  |         /> | ||||||
|  |         <v-card | ||||||
|  |             class="preview" | ||||||
|  |             v-if="selectedLearningObjects.length > 0" | ||||||
|  |             :title="t('preview_for') + selectedLearningObjects[0].title" | ||||||
|  |         > | ||||||
|  |             <template v-slot:text> | ||||||
|  |                 <using-query-result :query-result="learningObjectQueryResult" v-slot="response: { data: Document }"> | ||||||
|  |                     <learning-object-content-view :learning-object-content="response.data"></learning-object-content-view> | ||||||
|  |                 </using-query-result> | ||||||
|  |             </template> | ||||||
|  |         </v-card> | ||||||
|  |     </div> | ||||||
|  |     <div class="fab"> | ||||||
|  |         <learning-object-upload-button/> | ||||||
|  |     </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
| 
 |     .fab { | ||||||
|  |         position: absolute; | ||||||
|  |         right: 20px; | ||||||
|  |         bottom: 20px; | ||||||
|  |     } | ||||||
|  |     .root { | ||||||
|  |         display: flex; | ||||||
|  |         gap: 20px; | ||||||
|  |         padding: 20px; | ||||||
|  |     } | ||||||
|  |     .preview { | ||||||
|  |         flex: 1; | ||||||
|  |     } | ||||||
|  |     .table { | ||||||
|  |         flex: 1; | ||||||
|  |     } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -0,0 +1,9 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |     <p>Own learning paths</p> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  | </style> | ||||||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger