Merged in feature/VBV-310-VBV-324-document-improvements (pull request #66)
Feature/VBV-310 VBV 324 document improvements Approved-by: Elia Bieri
This commit is contained in:
commit
a11c0d481f
|
|
@ -0,0 +1,35 @@
|
||||||
|
<template>
|
||||||
|
<Dialog :open="true" class="relative z-50" @close="close">
|
||||||
|
<div class="fixed inset-0 bg-black/30" aria-hidden="true"></div>
|
||||||
|
<div class="fixed inset-0 flex items-center justify-center p-4">
|
||||||
|
<DialogPanel class="w-full max-w-2xl bg-white pt-8">
|
||||||
|
<div class="px-8 pb-4">
|
||||||
|
<DialogTitle class="relative mb-8 flex flex-row">{{ title }}</DialogTitle>
|
||||||
|
<p v-html="content" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row-reverse gap-x-4 border-t border-t-gray-500 p-4">
|
||||||
|
<button class="btn-primary" @click="confirm">Löschen</button>
|
||||||
|
<button class="btn-secondary" @click="close">Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</DialogPanel>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Dialog, DialogPanel, DialogTitle } from "@headlessui/vue";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
isOpen: boolean;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(["confirm", "close"]);
|
||||||
|
const close = () => {
|
||||||
|
emit("close");
|
||||||
|
};
|
||||||
|
const confirm = () => {
|
||||||
|
emit("confirm");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Dialog, DialogDescription, DialogPanel, DialogTitle } from "@headlessui/vue";
|
import { Dialog, DialogPanel, DialogTitle } from "@headlessui/vue";
|
||||||
|
|
||||||
interface Props {
|
export interface Props {
|
||||||
modelValue: boolean;
|
modelValue: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
withDefaults(defineProps<Props>(), {
|
||||||
modelValue: false,
|
modelValue: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -20,20 +20,22 @@ function setIsOpen(value: boolean) {
|
||||||
<Dialog :open="modelValue" class="relative z-50" @close="setIsOpen">
|
<Dialog :open="modelValue" class="relative z-50" @close="setIsOpen">
|
||||||
<div class="fixed inset-0 bg-black/30" aria-hidden="true"></div>
|
<div class="fixed inset-0 bg-black/30" aria-hidden="true"></div>
|
||||||
<div class="fixed inset-0 flex items-center justify-center p-4">
|
<div class="fixed inset-0 flex items-center justify-center p-4">
|
||||||
<DialogPanel class="w-full max-w-2xl bg-white px-8 pb-4 pt-8">
|
<DialogPanel class="w-full max-w-2xl bg-white pt-8">
|
||||||
<DialogTitle class="relative mb-8 flex flex-row">
|
<div class="px-8 pb-4">
|
||||||
<slot name="title"></slot>
|
<DialogTitle class="relative mb-8 flex flex-row">
|
||||||
<button
|
<slot name="title"></slot>
|
||||||
type="button"
|
<button
|
||||||
class="absolute right-4 h-4 w-4 cursor-pointer"
|
type="button"
|
||||||
@click="setIsOpen(false)"
|
class="absolute right-4 h-4 w-4 cursor-pointer"
|
||||||
>
|
@click="setIsOpen(false)"
|
||||||
<it-icon-close></it-icon-close>
|
>
|
||||||
</button>
|
<it-icon-close></it-icon-close>
|
||||||
</DialogTitle>
|
</button>
|
||||||
<DialogDescription></DialogDescription>
|
</DialogTitle>
|
||||||
|
|
||||||
<slot name="body"></slot>
|
<slot name="body"></slot>
|
||||||
|
</div>
|
||||||
|
<slot name="footer" />
|
||||||
</DialogPanel>
|
</DialogPanel>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@
|
||||||
"chooseLearningSequence": "Bitte wähle eine Lernsequenz aus",
|
"chooseLearningSequence": "Bitte wähle eine Lernsequenz aus",
|
||||||
"chooseName": "Bitte wähle einen Namen",
|
"chooseName": "Bitte wähle einen Namen",
|
||||||
"chooseSequence": "Wähle eine Lernsequenz aus",
|
"chooseSequence": "Wähle eine Lernsequenz aus",
|
||||||
|
"deleteModalTitle": "Unterlage löschen",
|
||||||
|
"deleteModalWarning": "Willst du die Unterlage <strong>\"{title}\"</strong> löschen?<br> Diese Aktion ist nicht umkehrbar.",
|
||||||
"expertDescription": "Stelle deinen Lernenden zusätzliche Inhalte zur Verfügung.",
|
"expertDescription": "Stelle deinen Lernenden zusätzliche Inhalte zur Verfügung.",
|
||||||
"fileLabel": "Datei",
|
"fileLabel": "Datei",
|
||||||
"maxFileSize": "Maximale Dateigrösse: 20 MB",
|
"maxFileSize": "Maximale Dateigrösse: 20 MB",
|
||||||
|
|
@ -38,6 +40,10 @@
|
||||||
"modalNameInformation": "Max. 70 Zeichen",
|
"modalNameInformation": "Max. 70 Zeichen",
|
||||||
"selectFile": "Bitte wähle eine Datei aus",
|
"selectFile": "Bitte wähle eine Datei aus",
|
||||||
"title": "Unterlagen",
|
"title": "Unterlagen",
|
||||||
|
"trainerDescription": "Finde auf Teams zusätzliche Inhalte für deinen Unterricht.",
|
||||||
|
"trainerLinkSrc": "https://teams.microsoft.com",
|
||||||
|
"trainerLinkText": "Inhalte auf Teams anschauen",
|
||||||
|
"trainerTitle": "Begleitung für Trainer",
|
||||||
"uploadErrorMessage": "Beim Hochladen ist ein Fehler aufgetreten. Bitte versuche es erneut.",
|
"uploadErrorMessage": "Beim Hochladen ist ein Fehler aufgetreten. Bitte versuche es erneut.",
|
||||||
"userDescription": "Hier findest du die Unterlagen, die dir die Fachexpertin zur Verfügung gestellt hat."
|
"userDescription": "Hier findest du die Unterlagen, die dir die Fachexpertin zur Verfügung gestellt hat."
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,29 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ItModal from "@/components/ui/ItModal.vue";
|
import log from "loglevel";
|
||||||
import * as log from "loglevel";
|
import { computed, onMounted } from "vue";
|
||||||
import { computed, onMounted, ref, watch } from "vue";
|
|
||||||
import CircleDiagram from "./CircleDiagram.vue";
|
import CircleDiagram from "./CircleDiagram.vue";
|
||||||
import CircleOverview from "./CircleOverview.vue";
|
import CircleOverview from "./CircleOverview.vue";
|
||||||
import DocumentUploadForm from "./DocumentUploadForm.vue";
|
import DocumentSection from "./DocumentSection.vue";
|
||||||
import LearningSequence from "./LearningSequence.vue";
|
import LearningSequence from "./LearningSequence.vue";
|
||||||
|
|
||||||
import { uploadCircleDocument } from "@/services/files";
|
|
||||||
import { useAppStore } from "@/stores/app";
|
import { useAppStore } from "@/stores/app";
|
||||||
import { useCircleStore } from "@/stores/circle";
|
import { useCircleStore } from "@/stores/circle";
|
||||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||||
import type { CourseSessionUser, DocumentUploadData } from "@/types";
|
import type { CourseSessionUser } from "@/types";
|
||||||
import { humanizeDuration } from "@/utils/humanizeDuration";
|
import { humanizeDuration } from "@/utils/humanizeDuration";
|
||||||
import sumBy from "lodash/sumBy";
|
import sumBy from "lodash/sumBy";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
|
|
||||||
const route = useRoute();
|
export interface Props {
|
||||||
const courseSessionsStore = useCourseSessionsStore();
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
courseSlug: string;
|
courseSlug: string;
|
||||||
circleSlug: string;
|
circleSlug: string;
|
||||||
profileUser?: CourseSessionUser;
|
profileUser?: CourseSessionUser;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const courseSessionsStore = useCourseSessionsStore();
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
readonly: false,
|
readonly: false,
|
||||||
profileUser: undefined,
|
profileUser: undefined,
|
||||||
|
|
@ -33,10 +31,6 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
||||||
log.debug("CirclePage created", props.readonly, props.profileUser);
|
log.debug("CirclePage created", props.readonly, props.profileUser);
|
||||||
|
|
||||||
const showUploadModal = ref(false);
|
|
||||||
const showUploadErrorMessage = ref(false);
|
|
||||||
const isUploading = ref(false);
|
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
appStore.showMainNavigationBar = true;
|
appStore.showMainNavigationBar = true;
|
||||||
|
|
||||||
|
|
@ -51,15 +45,6 @@ const duration = computed(() => {
|
||||||
return "";
|
return "";
|
||||||
});
|
});
|
||||||
|
|
||||||
const dropdownLearningSequences = computed(() =>
|
|
||||||
circleStore.circle?.learningSequences.map((sequence) => ({
|
|
||||||
id: sequence.id,
|
|
||||||
name: sequence.title,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(showUploadModal, (_v) => (showUploadErrorMessage.value = false));
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
log.debug(
|
log.debug(
|
||||||
"CirclePage mounted",
|
"CirclePage mounted",
|
||||||
|
|
@ -107,28 +92,6 @@ onMounted(async () => {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function uploadDocument(data: DocumentUploadData) {
|
|
||||||
isUploading.value = true;
|
|
||||||
showUploadErrorMessage.value = false;
|
|
||||||
try {
|
|
||||||
if (!courseSessionsStore.currentCourseSession) {
|
|
||||||
throw new Error("No course session found");
|
|
||||||
}
|
|
||||||
const newDocument = await uploadCircleDocument(
|
|
||||||
data,
|
|
||||||
courseSessionsStore.currentCourseSession.id
|
|
||||||
);
|
|
||||||
const courseSessionStore = useCourseSessionsStore();
|
|
||||||
courseSessionStore.addDocument(newDocument);
|
|
||||||
showUploadModal.value = false;
|
|
||||||
isUploading.value = false;
|
|
||||||
} catch (error) {
|
|
||||||
log.error(error);
|
|
||||||
showUploadErrorMessage.value = true;
|
|
||||||
isUploading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -213,60 +176,7 @@ async function uploadDocument(data: DocumentUploadData) {
|
||||||
{{ $t("circlePage.learnMore") }}
|
{{ $t("circlePage.learnMore") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!props.readonly" class="mt-8 block border p-6">
|
<DocumentSection v-if="!readonly" />
|
||||||
<h3 class="text-blue-dark">
|
|
||||||
{{ $t("circlePage.documents.title") }}
|
|
||||||
</h3>
|
|
||||||
<div v-if="!courseSessionsStore.canUploadCircleDocuments">
|
|
||||||
<div class="mt-4 leading-relaxed">
|
|
||||||
{{ $t("circlePage.documents.userDescription") }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ol
|
|
||||||
v-if="
|
|
||||||
courseSessionsStore &&
|
|
||||||
courseSessionsStore.circleDocuments &&
|
|
||||||
courseSessionsStore.circleDocuments.length > 0
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
v-for="learningSequence of courseSessionsStore.circleDocuments"
|
|
||||||
:key="learningSequence.id"
|
|
||||||
>
|
|
||||||
<h4 class="text-bold mt-4">{{ learningSequence.title }}</h4>
|
|
||||||
<ul>
|
|
||||||
<li
|
|
||||||
v-for="document of learningSequence.documents"
|
|
||||||
:key="document.url"
|
|
||||||
>
|
|
||||||
<a :href="document.url" download>
|
|
||||||
<span>{{ document.name }}</span>
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
v-if="courseSessionsStore.canUploadCircleDocuments"
|
|
||||||
type="button"
|
|
||||||
class="relative top-[1px] ml-2 inline-block h-3 w-3 cursor-pointer leading-6"
|
|
||||||
@click="courseSessionsStore.removeDocument(document.id)"
|
|
||||||
>
|
|
||||||
<it-icon-close class="h-3 w-3"></it-icon-close>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
<div v-if="courseSessionsStore.canUploadCircleDocuments">
|
|
||||||
<div class="mt-4 leading-relaxed">
|
|
||||||
{{ $t("circlePage.documents.expertDescription") }}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="btn-primary mt-4 text-xl"
|
|
||||||
@click="showUploadModal = true"
|
|
||||||
>
|
|
||||||
{{ $t("circlePage.documents.action") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="!props.readonly" class="expert mt-8 border p-6">
|
<div v-if="!props.readonly" class="expert mt-8 border p-6">
|
||||||
<h3 class="text-blue-dark">{{ $t("circlePage.gotQuestions") }}</h3>
|
<h3 class="text-blue-dark">{{ $t("circlePage.gotQuestions") }}</h3>
|
||||||
<div class="mt-4 leading-relaxed">
|
<div class="mt-4 leading-relaxed">
|
||||||
|
|
@ -312,17 +222,6 @@ async function uploadDocument(data: DocumentUploadData) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ItModal v-model="showUploadModal">
|
|
||||||
<template #title>{{ $t("circlePage.documents.action") }}</template>
|
|
||||||
<template #body>
|
|
||||||
<DocumentUploadForm
|
|
||||||
:learning-sequences="dropdownLearningSequences"
|
|
||||||
:show-upload-error-message="showUploadErrorMessage"
|
|
||||||
:is-uploading="isUploading"
|
|
||||||
@form-submit="uploadDocument"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</ItModal>
|
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<template>
|
||||||
|
<li
|
||||||
|
:key="doc.url"
|
||||||
|
class="grid grid-cols-document-list-item border-b border-b-gray-500 py-3 grid-areas-document-list-item"
|
||||||
|
>
|
||||||
|
<h3 class="grid-in-title">
|
||||||
|
{{ doc.name }}
|
||||||
|
</h3>
|
||||||
|
<h4 class="grid-in-subtitle">{{ subtitle }}</h4>
|
||||||
|
<div class="flex items-center justify-end gap-x-4 grid-in-icons">
|
||||||
|
<a v-if="canDelete" class="flex cursor-pointer" @click="emit('delete')">
|
||||||
|
<it-icon-delete class="h-8 w-8" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a download :href="doc.url" class="flex">
|
||||||
|
<it-icon-download class="h-8 w-8" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
doc: {
|
||||||
|
url: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
canDelete: boolean;
|
||||||
|
subtitle: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(["delete"]);
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
<template>
|
||||||
|
<div class="mt-8 block border p-6">
|
||||||
|
<h3 class="text-blue-dark">
|
||||||
|
{{ $t("circlePage.documents.title") }}
|
||||||
|
</h3>
|
||||||
|
<div>
|
||||||
|
<div class="mt-4 leading-relaxed">
|
||||||
|
<template v-if="!courseSessionsStore.canUploadCircleDocuments">
|
||||||
|
{{ $t("circlePage.documents.userDescription") }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ $t("circlePage.documents.expertDescription") }}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
v-if="courseSessionsStore.circleDocuments.length"
|
||||||
|
class="mt-8 border-t border-t-gray-500"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="learningSequence of courseSessionsStore.circleDocuments"
|
||||||
|
:key="learningSequence.id"
|
||||||
|
>
|
||||||
|
<DocumentListItem
|
||||||
|
v-for="doc of learningSequence.documents"
|
||||||
|
:key="doc.url"
|
||||||
|
:subtitle="learningSequence.title"
|
||||||
|
:can-delete="courseSessionsStore.canUploadCircleDocuments"
|
||||||
|
:doc="doc"
|
||||||
|
@delete="deleteDocument(doc)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
<div v-if="courseSessionsStore.canUploadCircleDocuments">
|
||||||
|
<button class="btn-primary mt-8 text-xl" @click="showUploadModal = true">
|
||||||
|
{{ $t("circlePage.documents.action") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ItModal v-model="showUploadModal">
|
||||||
|
<template #title>{{ $t("circlePage.documents.action") }}</template>
|
||||||
|
<template #body>
|
||||||
|
<DocumentUploadForm
|
||||||
|
:learning-sequences="dropdownLearningSequences"
|
||||||
|
:show-upload-error-message="showUploadErrorMessage"
|
||||||
|
:is-uploading="isUploading"
|
||||||
|
@form-submit="uploadDocument"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ItModal>
|
||||||
|
<div
|
||||||
|
v-if="courseSessionsStore.canUploadCircleDocuments"
|
||||||
|
class="mt-8 flex flex-col gap-y-4 border p-6"
|
||||||
|
>
|
||||||
|
<h3 class="text-blue-dark">
|
||||||
|
{{ $t("circlePage.documents.trainerTitle") }}
|
||||||
|
</h3>
|
||||||
|
<div class="leading-relaxed">
|
||||||
|
{{ $t("circlePage.documents.trainerDescription") }}
|
||||||
|
</div>
|
||||||
|
<a target="_blank" class="link" :href="$t('circlePage.documents.trainerLinkSrc')">
|
||||||
|
{{ $t("circlePage.documents.trainerLinkText") }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ItModal from "@/components/ui/ItModal.vue";
|
||||||
|
import { uploadCircleDocument } from "@/services/files";
|
||||||
|
import { useCircleStore } from "@/stores/circle";
|
||||||
|
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||||
|
import type { CircleDocument, DocumentUploadData } from "@/types";
|
||||||
|
import dialog from "@/utils/confirm-dialog";
|
||||||
|
import log from "loglevel";
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import DocumentListItem from "./DocumentListItem.vue";
|
||||||
|
import DocumentUploadForm from "./DocumentUploadForm.vue";
|
||||||
|
|
||||||
|
const courseSessionsStore = useCourseSessionsStore();
|
||||||
|
const circleStore = useCircleStore();
|
||||||
|
const showUploadModal = ref(false);
|
||||||
|
const showUploadErrorMessage = ref(false);
|
||||||
|
const isUploading = ref(false);
|
||||||
|
const dropdownLearningSequences = computed(() =>
|
||||||
|
circleStore.circle?.learningSequences.map((sequence) => ({
|
||||||
|
id: sequence.id,
|
||||||
|
name: sequence.title,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
// confirm dialog
|
||||||
|
const { t } = useI18n();
|
||||||
|
const deleteDocument = async (doc: CircleDocument) => {
|
||||||
|
const options = {
|
||||||
|
title: t("circlePage.documents.deleteModalTitle"),
|
||||||
|
content: t("circlePage.documents.deleteModalWarning", { title: doc.name }),
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await dialog.confirm(options);
|
||||||
|
courseSessionsStore.removeDocument(doc.id);
|
||||||
|
} catch (e) {
|
||||||
|
log.debug("rejected");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
watch(showUploadModal, () => (showUploadErrorMessage.value = false));
|
||||||
|
async function uploadDocument(data: DocumentUploadData) {
|
||||||
|
isUploading.value = true;
|
||||||
|
showUploadErrorMessage.value = false;
|
||||||
|
try {
|
||||||
|
if (!courseSessionsStore.currentCourseSession) {
|
||||||
|
throw new Error("No course session found");
|
||||||
|
}
|
||||||
|
const newDocument = await uploadCircleDocument(
|
||||||
|
data,
|
||||||
|
courseSessionsStore.currentCourseSession.id
|
||||||
|
);
|
||||||
|
const courseSessionStore = useCourseSessionsStore();
|
||||||
|
courseSessionStore.addDocument(newDocument);
|
||||||
|
showUploadModal.value = false;
|
||||||
|
isUploading.value = false;
|
||||||
|
} catch (error) {
|
||||||
|
log.error(error);
|
||||||
|
showUploadErrorMessage.value = true;
|
||||||
|
isUploading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -4,7 +4,7 @@ import type { DocumentUploadData, DropdownSelectable } from "@/types";
|
||||||
import { reactive } from "vue";
|
import { reactive } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
interface Props {
|
export interface Props {
|
||||||
learningSequences: DropdownSelectable[];
|
learningSequences: DropdownSelectable[];
|
||||||
showUploadErrorMessage: boolean;
|
showUploadErrorMessage: boolean;
|
||||||
isUploading: boolean;
|
isUploading: boolean;
|
||||||
|
|
|
||||||
|
|
@ -182,21 +182,23 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
||||||
const circleDocuments = computed(() => {
|
const circleDocuments = computed(() => {
|
||||||
const circleStore = useCircleStore();
|
const circleStore = useCircleStore();
|
||||||
|
|
||||||
return circleStore.circle?.learningSequences
|
return (
|
||||||
.map((ls) => ({ id: ls.id, title: ls.title, documents: [] }))
|
circleStore.circle?.learningSequences
|
||||||
.map((ls: { id: number; title: string; documents: CircleDocument[] }) => {
|
.map((ls) => ({ id: ls.id, title: ls.title, documents: [] }))
|
||||||
if (currentCourseSession.value === undefined) {
|
.map((ls: { id: number; title: string; documents: CircleDocument[] }) => {
|
||||||
return ls;
|
if (currentCourseSession.value === undefined) {
|
||||||
}
|
return ls;
|
||||||
|
|
||||||
for (const document of currentCourseSession.value.documents) {
|
|
||||||
if (document.learning_sequence === ls.id) {
|
|
||||||
ls.documents.push(document);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return ls;
|
for (const document of currentCourseSession.value.documents) {
|
||||||
})
|
if (document.learning_sequence === ls.id) {
|
||||||
.filter((ls) => ls.documents.length > 0);
|
ls.documents.push(document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ls;
|
||||||
|
})
|
||||||
|
.filter((ls) => ls.documents.length > 0) || []
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
function addDocument(document: CircleDocument) {
|
function addDocument(document: CircleDocument) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
import ConfirmDialog from "@/components/ui/ConfirmDialog.vue";
|
||||||
|
import { createApp } from "vue";
|
||||||
|
|
||||||
|
interface ConfirmDialogOptions {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
type ResolveReject = (v?: unknown) => void;
|
||||||
|
|
||||||
|
// inspired by https://stackoverflow.com/a/69773076/6071058
|
||||||
|
/*
|
||||||
|
* We need a separate service for the ConfirmDialog, because we don't want the
|
||||||
|
* boilerplate of including the component and the handling of the promises inside
|
||||||
|
* of every component where we need a confirm dialog.
|
||||||
|
*
|
||||||
|
* With this service, one can simply import the dialog and use it, e.g.
|
||||||
|
*
|
||||||
|
import dialog from "@/utils/confirm-dialog";
|
||||||
|
const someMethodToConfirm = async () => {
|
||||||
|
const options = {
|
||||||
|
title: 'Dialog Title',
|
||||||
|
content: 'Do you really wanna?'
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await dialog.confirm(options);
|
||||||
|
doSomethingWhenConfirmed();
|
||||||
|
} catch (e) {
|
||||||
|
log.debug("rejected");
|
||||||
|
doSomethingWhenRejected()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
confirm(options: ConfirmDialogOptions) {
|
||||||
|
const mountEl = document.createElement("div");
|
||||||
|
document.body.appendChild(mountEl);
|
||||||
|
|
||||||
|
let _resolve: ResolveReject, _reject: ResolveReject;
|
||||||
|
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
_resolve = resolve;
|
||||||
|
_reject = reject;
|
||||||
|
});
|
||||||
|
|
||||||
|
const cleanUp = () => {
|
||||||
|
mountEl?.parentNode?.removeChild(mountEl);
|
||||||
|
dialog.unmount();
|
||||||
|
};
|
||||||
|
|
||||||
|
const dialog = createApp(ConfirmDialog, {
|
||||||
|
isOpen: true,
|
||||||
|
title: options.title,
|
||||||
|
content: options.content,
|
||||||
|
onClose() {
|
||||||
|
cleanUp();
|
||||||
|
_reject();
|
||||||
|
},
|
||||||
|
onConfirm() {
|
||||||
|
cleanUp();
|
||||||
|
_resolve();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dialog.mount(mountEl);
|
||||||
|
return promise;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -51,11 +51,13 @@ module.exports = {
|
||||||
],
|
],
|
||||||
"rating-scale-slim": ["bar bar bar", "fst mid fth"],
|
"rating-scale-slim": ["bar bar bar", "fst mid fth"],
|
||||||
"icon-card": ["icon title", "icon value"],
|
"icon-card": ["icon title", "icon value"],
|
||||||
|
"document-list-item": ["title icons", "subtitle icons"],
|
||||||
},
|
},
|
||||||
gridTemplateColumns: {
|
gridTemplateColumns: {
|
||||||
"horizontal-bar-chart": "50px 1fr 300px 4fr 300px 1fr",
|
"horizontal-bar-chart": "50px 1fr 300px 4fr 300px 1fr",
|
||||||
"horizontal-bar-chart-slim": "50px 1fr 78px 4fr 78px 1fr",
|
"horizontal-bar-chart-slim": "50px 1fr 78px 4fr 78px 1fr",
|
||||||
"icon-card": "60px auto",
|
"icon-card": "60px auto",
|
||||||
|
"document-list-item": "1fr 100px",
|
||||||
},
|
},
|
||||||
gridTemplateRows: {
|
gridTemplateRows: {
|
||||||
"horizontal-bar-chart": "200px 40px",
|
"horizontal-bar-chart": "200px 40px",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Outline" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
|
||||||
|
<path d="m25.63,7.06h-6.08v-1.05c0-1.18-.96-2.14-2.14-2.14h-4.83c-1.18,0-2.14.96-2.14,2.14v1.05h-6.08v1.5h2.61l.82,15.54c.06,1.14,1,2.03,2.14,2.03h10.14c1.14,0,2.08-.89,2.14-2.03l.82-15.54h2.61v-1.5Zm-13.69-1.05c0-.35.29-.64.64-.64h4.83c.35,0,.64.29.64.64v1.05h-6.11v-1.05Zm8.76,18.02c-.02.34-.3.61-.64.61h-10.14c-.34,0-.62-.27-.64-.61l-.81-15.46h13.04l-.81,15.46Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 489 B |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M15,22.31c.19,0,.38-.07,.53-.22l7.45-7.45-1.06-1.06-6.17,6.17V3.83h-1.5v15.92l-6.17-6.17-1.06,1.06,7.45,7.45c.15,.15,.34,.22,.53,.22Z"/><rect x="4.94" y="24.84" width="20.11" height="1.5"/></svg>
|
||||||
|
After Width: | Height: | Size: 309 B |
Loading…
Reference in New Issue