feat(frontend): basisimplementatie leerobject upload-UI

This commit is contained in:
Gerald Schmittinger 2025-05-12 00:47:37 +02:00
parent 6600441b08
commit be1091544c
11 changed files with 311 additions and 40 deletions

View file

@ -37,6 +37,28 @@ export abstract class BaseController {
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> {
const response = await apiClient.delete<T>(this.absolutePathFor(path), { params: queryParams });
BaseController.assertSuccessResponse(response);

View file

@ -14,4 +14,12 @@ export class LearningObjectController extends BaseController {
async getHTML(hruid: string, language: Language, version: number): Promise<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);
}
}

View file

@ -1,9 +1,10 @@
import { type MaybeRefOrGetter, toValue } from "vue";
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 type { LearningObject } from "@/data-objects/learning-objects/learning-object.ts";
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
import type { AxiosError } from "axios";
export const LEARNING_OBJECT_KEY = "learningObject";
const learningObjectController = getLearningObjectController();
@ -24,15 +25,15 @@ export function useLearningObjectMetadataQuery(
}
export function useLearningObjectHTMLQuery(
hruid: MaybeRefOrGetter<string>,
language: MaybeRefOrGetter<Language>,
version: MaybeRefOrGetter<number>,
hruid: MaybeRefOrGetter<string | undefined>,
language: MaybeRefOrGetter<Language | undefined>,
version: MaybeRefOrGetter<number | undefined>,
): UseQueryReturnType<Document, Error> {
return useQuery({
queryKey: [LEARNING_OBJECT_KEY, "html", hruid, language, version],
queryFn: async () => {
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)),
});
@ -55,3 +56,25 @@ export function useLearningObjectListForPathQuery(
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"]}); }
});
}

View file

@ -14,6 +14,7 @@ import UserHomePage from "@/views/homepage/UserHomePage.vue";
import SingleTheme from "@/views/SingleTheme.vue";
import LearningObjectView from "@/views/learning-paths/learning-object/LearningObjectView.vue";
import authService from "@/services/auth/auth-service";
import OwnLearningContentPage from "@/views/own-learning-content/OwnLearningContentPage.vue";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@ -114,6 +115,12 @@ const router = createRouter({
component: LearningPathSearchPage,
meta: { requiresAuth: true },
},
{
path: "my",
name: "OwnLearningContentPage",
component: OwnLearningContentPage,
meta: { requiresAuth: true }
},
{
path: ":hruid/:language/:learningObjectHruid",
name: "LearningPath",

View file

@ -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>

View file

@ -1,11 +1,52 @@
<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>
<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>
<style scoped>
.tab-pane-container {
display: flex;
flex-direction: column;
height: 100%;
}
.main-content {
flex: 1;
}
</style>

View file

@ -1,11 +1,80 @@
<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>
<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>
<style scoped>
.fab {
position: absolute;
right: 20px;
bottom: 20px;
}
.root {
display: flex;
gap: 20px;
padding: 20px;
}
.preview {
flex: 1;
}
.table {
flex: 1;
}
</style>

View file

@ -0,0 +1,9 @@
<script lang="ts">
</script>
<template>
<p>Own learning paths</p>
</template>
<style>
</style>