Merged in feature/VBV-515-preview (pull request #205)
WIP Feature/VBV-515 preview Approved-by: Daniel Egger
This commit is contained in:
commit
18a6eecb49
|
|
@ -1,11 +1,26 @@
|
|||
<script lang="ts" setup>
|
||||
import { formatDueDate } from "@/components/dueDates/dueDatesUtils";
|
||||
import type { DueDate } from "@/types";
|
||||
import type { CourseSession, DueDate } from "@/types";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
|
||||
const props = defineProps<{
|
||||
dueDate: DueDate;
|
||||
singleLine?: boolean;
|
||||
}>();
|
||||
|
||||
/* FIXME @livioso 19.09.23: This is a temporary workaround to have a ship-able / deployable
|
||||
version of the preview feature (VBV-516). The plan is to tackle the role-based
|
||||
due dates calendar next (VBV-524) which will touch all usage of this component.
|
||||
For now, just disable links for trainer / expert -> to reduce level of confusion ;)
|
||||
*/
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const courseSession = courseSessionsStore.allCourseSessions.find(
|
||||
(cs: CourseSession) => cs.id === props.dueDate.course_session
|
||||
);
|
||||
|
||||
const disableLink = courseSession
|
||||
? !courseSessionsStore.hasCockpit(courseSession)
|
||||
: false;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -14,10 +29,13 @@ const props = defineProps<{
|
|||
:class="{ 'flex-col': props.singleLine, 'items-center': !props.singleLine }"
|
||||
>
|
||||
<div class="space-y-1">
|
||||
<div>
|
||||
<a class="text-bold underline" :href="props.dueDate.url">
|
||||
<div class="text-bold">
|
||||
<a v-if="disableLink" class="underline" :href="props.dueDate.url">
|
||||
{{ props.dueDate.title }}
|
||||
</a>
|
||||
<template v-else>
|
||||
{{ props.dueDate.title }}
|
||||
</template>
|
||||
</div>
|
||||
<div class="text-small text-gray-900">
|
||||
<div v-if="props.dueDate.date_type_translation_key">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
<script setup lang="ts">
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import { useRouteLookups } from "@/utils/route";
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import { getCompetenceBaseUrl } from "@/utils/utils";
|
||||
|
||||
const { inCompetenceProfile, inLearningPath } = useRouteLookups();
|
||||
const courseSession = useCurrentCourseSession();
|
||||
|
||||
const { t } = useTranslation();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-cy="course-preview-bar">
|
||||
<nav class="bg-yellow-500">
|
||||
<div class="mx-auto px-4 lg:px-8">
|
||||
<div
|
||||
class="relative flex h-16 w-full flex-col items-center justify-end space-x-8 lg:flex-row lg:items-stretch lg:justify-center"
|
||||
>
|
||||
<span class="flex items-center px-1 pt-1 font-bold text-black">
|
||||
{{ t("a.VorschauTeilnehmer") }} ({{ courseSession.title }})
|
||||
</span>
|
||||
|
||||
<div class="flex space-x-8">
|
||||
<router-link
|
||||
:to="courseSession.learning_path_url"
|
||||
class="preview-nav-item"
|
||||
:class="{ 'preview-nav-item--active': inLearningPath() }"
|
||||
>
|
||||
{{ t("general.learningPath") }}
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
:to="getCompetenceBaseUrl(courseSession)"
|
||||
class="preview-nav-item"
|
||||
:class="{ 'preview-nav-item--active': inCompetenceProfile() }"
|
||||
>
|
||||
{{ t("competences.title") }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.preview-nav-item {
|
||||
@apply inline-flex items-center border-b-4 border-transparent px-1 pt-1 text-black hover:text-gray-800;
|
||||
}
|
||||
|
||||
.preview-nav-item--active {
|
||||
@apply border-black;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -14,6 +14,8 @@ import { Popover, PopoverButton, PopoverPanel } from "@headlessui/vue";
|
|||
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
|
||||
import { computed, onMounted, reactive } from "vue";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import CoursePreviewBar from "@/components/header/CoursePreviewBar.vue";
|
||||
import { getCompetenceBaseUrl } from "@/utils/utils";
|
||||
|
||||
log.debug("MainNavigationBar created");
|
||||
|
||||
|
|
@ -47,7 +49,8 @@ onMounted(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CoursePreviewBar v-if="courseSessionsStore.hasCourseSessionPreview" />
|
||||
<div v-else>
|
||||
<Teleport to="body">
|
||||
<MobileMenu
|
||||
v-if="userStore.loggedIn"
|
||||
|
|
@ -100,43 +103,51 @@ onMounted(() => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden space-x-8 lg:flex">
|
||||
<!-- Navigation Links Desktop -->
|
||||
<router-link
|
||||
v-if="
|
||||
inCourse() &&
|
||||
courseSessionsStore.currentCourseSession &&
|
||||
courseSessionsStore.currentCourseSessionHasCockpit
|
||||
"
|
||||
:to="`${courseSessionsStore.currentCourseSession.course_url}/cockpit`"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inCockpit() }"
|
||||
>
|
||||
{{ t("cockpit.title") }}
|
||||
</router-link>
|
||||
<!-- Satisfy the type checker; these menu items are
|
||||
only relevant if there is a current course session -->
|
||||
<template v-if="courseSessionsStore.currentCourseSession">
|
||||
<div class="hidden space-x-8 lg:flex">
|
||||
<template v-if="courseSessionsStore.currentCourseSessionHasCockpit">
|
||||
<router-link
|
||||
:to="`${courseSessionsStore.currentCourseSession.course_url}/cockpit`"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inCockpit() }"
|
||||
>
|
||||
{{ t("cockpit.title") }}
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-if="inCourse() && courseSessionsStore.currentCourseSession"
|
||||
:to="courseSessionsStore.currentCourseSession.learning_path_url"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inLearningPath() }"
|
||||
>
|
||||
{{ t("general.learningPath") }}
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="courseSessionsStore.currentCourseSession.learning_path_url"
|
||||
target="_blank"
|
||||
class="nav-item"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<span>{{ t("a.VorschauTeilnehmer") }}</span>
|
||||
<it-icon-external-link class="ml-2" />
|
||||
</div>
|
||||
</router-link>
|
||||
</template>
|
||||
<template v-else>
|
||||
<router-link
|
||||
:to="courseSessionsStore.currentCourseSession.learning_path_url"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inLearningPath() }"
|
||||
>
|
||||
{{ t("general.learningPath") }}
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-if="inCourse() && courseSessionsStore.currentCourseSession"
|
||||
:to="`${courseSessionsStore.currentCourseSession.competence_url.replace(
|
||||
// TODO: remove the `competence_url` with url to Navi...
|
||||
'/competences',
|
||||
''
|
||||
)}`"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inCompetenceProfile() }"
|
||||
>
|
||||
{{ t("competences.title") }}
|
||||
</router-link>
|
||||
</div>
|
||||
<router-link
|
||||
:to="
|
||||
getCompetenceBaseUrl(courseSessionsStore.currentCourseSession)
|
||||
"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inCompetenceProfile() }"
|
||||
>
|
||||
{{ t("competences.title") }}
|
||||
</router-link>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="flex items-stretch justify-start space-x-8">
|
||||
|
|
@ -227,15 +238,4 @@ onMounted(() => {
|
|||
.nav-item--active {
|
||||
@apply border-sky-500;
|
||||
}
|
||||
|
||||
.nav-enter-active,
|
||||
.nav-leave-active {
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-enter-from,
|
||||
.nav-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-80px);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import type { UserState } from "@/stores/user";
|
||||
import type { CourseSession } from "@/types";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { getCompetenceBaseUrl } from "@/utils/utils";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
@ -53,36 +54,32 @@ const courseSessionsStore = useCourseSessionsStore();
|
|||
</div>
|
||||
<div>
|
||||
<div v-if="courseSession" class="mt-6 border-b">
|
||||
<h4 class="text-sm text-gray-900">{{ courseSession?.course.title }}</h4>
|
||||
<h4 class="text-sm text-gray-900">{{ courseSession.course.title }}</h4>
|
||||
<ul class="mt-6">
|
||||
<li
|
||||
v-if="courseSessionsStore.currentCourseSessionHasCockpit"
|
||||
class="mb-6"
|
||||
>
|
||||
<button @click="clickLink(`${courseSession?.course_url}/cockpit`)">
|
||||
{{ $t("cockpit.title") }}
|
||||
</button>
|
||||
</li>
|
||||
<li class="mb-6">
|
||||
<button @click="clickLink(courseSession?.learning_path_url)">
|
||||
{{ $t("general.learningPath") }}
|
||||
</button>
|
||||
</li>
|
||||
<li class="mb-6">
|
||||
<button
|
||||
@click="
|
||||
clickLink(
|
||||
courseSession?.competence_url.replace(
|
||||
// TODO: remove the `competence_url` with url to Navi...
|
||||
'/competences',
|
||||
''
|
||||
)
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ $t("competences.title") }}
|
||||
</button>
|
||||
</li>
|
||||
<template v-if="courseSessionsStore.currentCourseSessionHasCockpit">
|
||||
<li class="mb-6">
|
||||
<button @click="clickLink(`${courseSession.course_url}/cockpit`)">
|
||||
{{ $t("cockpit.title") }}
|
||||
</button>
|
||||
</li>
|
||||
<li class="mb-6">
|
||||
<button @click="clickLink(courseSession.learning_path_url)">
|
||||
{{ $t("a.VorschauTeilnehmer") }}
|
||||
</button>
|
||||
</li>
|
||||
</template>
|
||||
<template v-else>
|
||||
<li class="mb-6">
|
||||
<button @click="clickLink(courseSession.learning_path_url)">
|
||||
{{ $t("general.learningPath") }}
|
||||
</button>
|
||||
</li>
|
||||
<li class="mb-6">
|
||||
<button @click="clickLink(getCompetenceBaseUrl(courseSession))">
|
||||
{{ $t("competences.title") }}
|
||||
</button>
|
||||
</li>
|
||||
</template>
|
||||
<li class="mb-6">
|
||||
<button
|
||||
data-cy="medialibrary-link"
|
||||
|
|
|
|||
|
|
@ -29,8 +29,5 @@ const circleDates = computed(() => {
|
|||
<DueDateSingle :due-date="dueDate" :single-line="true"></DueDateSingle>
|
||||
</div>
|
||||
<div v-if="circleDates.length === 0">{{ $t("dueDates.noDueDatesAvailable") }}</div>
|
||||
<a class="border-t border-gray-500 pt-8 underline" href="">
|
||||
{{ $t("dueDates.showAllDueDates") }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from "loglevel";
|
||||
import CoursePreviewBar from "@/components/header/CoursePreviewBar.vue";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
|
||||
log.debug("LearningContentContainer.vue setup");
|
||||
|
||||
|
|
@ -9,6 +13,7 @@ defineEmits(["exit"]);
|
|||
<template>
|
||||
<div>
|
||||
<div class="absolute bottom-0 top-0 w-full bg-white">
|
||||
<CoursePreviewBar v-if="courseSessionsStore.hasCourseSessionPreview" />
|
||||
<div class="h-content overflow-y-auto">
|
||||
<header
|
||||
class="relative flex h-12 w-full items-center justify-between bg-white px-4 lg:h-16 lg:px-8"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import type {
|
|||
ExpertSessionUser,
|
||||
} from "@/types";
|
||||
import eventBus from "@/utils/eventBus";
|
||||
import { useRouteLookups } from "@/utils/route";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import dayjs from "dayjs";
|
||||
import uniqBy from "lodash/uniqBy";
|
||||
|
|
@ -24,6 +25,7 @@ const SELECTED_COURSE_SESSIONS_KEY = "selectedCourseSessionMap";
|
|||
export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
||||
const loaded = ref(false);
|
||||
const allCourseSessions = ref<CourseSession[]>([]);
|
||||
const { inCompetenceProfile, inLearningPath } = useRouteLookups();
|
||||
|
||||
async function loadCourseSessionsData(reload = false) {
|
||||
log.debug("loadCourseSessionsData called");
|
||||
|
|
@ -134,6 +136,12 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
|||
return false;
|
||||
});
|
||||
|
||||
const hasCourseSessionPreview = computed(() => {
|
||||
const isCourseExpert =
|
||||
currentCourseSession.value && currentCourseSessionHasCockpit.value;
|
||||
return Boolean(isCourseExpert && (inLearningPath() || inCompetenceProfile()));
|
||||
});
|
||||
|
||||
const circleExperts = computed(() => {
|
||||
const circleStore = useCircleStore();
|
||||
const circleTranslationKey = circleStore.circle?.translation_key;
|
||||
|
|
@ -262,6 +270,7 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
|||
courseSessionForCourse,
|
||||
switchCourseSession,
|
||||
hasCockpit,
|
||||
hasCourseSessionPreview,
|
||||
currentCourseSessionHasCockpit,
|
||||
canUploadCircleDocuments,
|
||||
circleDocuments,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,13 @@
|
|||
import type { CourseSession } from "@/types";
|
||||
|
||||
export function assertUnreachable(msg: string): never {
|
||||
throw new Error("Didn't expect to get here, " + msg);
|
||||
}
|
||||
|
||||
export function getCompetenceBaseUrl(courseSession: CourseSession): string {
|
||||
return courseSession.competence_url.replace(
|
||||
// TODO: remove the `competence_url` with url to Navi...
|
||||
"/competences",
|
||||
""
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import {login} from "./helpers";
|
||||
|
||||
describe("preview.cy.js", () => {
|
||||
beforeEach(() => {
|
||||
cy.manageCommand("cypress_reset");
|
||||
});
|
||||
|
||||
it("Student does not see preview", () => {
|
||||
login("test-student1@example.com", "test");
|
||||
cy.visit("/course/test-lehrgang/learn/fahrzeug");
|
||||
cy.get('[data-cy="course-preview-bar"]').should("not.exist");
|
||||
});
|
||||
|
||||
it("Trainer sees preview exclusively on course", () => {
|
||||
login("test-trainer1@example.com", "test");
|
||||
|
||||
cy.visit("/");
|
||||
cy.get('[data-cy="course-preview-bar"]').should("not.exist");
|
||||
|
||||
cy.visit("/course/test-lehrgang/learn/fahrzeug");
|
||||
cy.get('[data-cy="course-preview-bar"]').should("exist");
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue