Merged in feature/VBV-515-preview (pull request #205)

WIP Feature/VBV-515 preview

Approved-by: Daniel Egger
This commit is contained in:
Livio Bieri 2023-09-21 08:06:30 +00:00 committed by Daniel Egger
commit 18a6eecb49
9 changed files with 197 additions and 83 deletions

View File

@ -1,11 +1,26 @@
<script lang="ts" setup> <script lang="ts" setup>
import { formatDueDate } from "@/components/dueDates/dueDatesUtils"; 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<{ const props = defineProps<{
dueDate: DueDate; dueDate: DueDate;
singleLine?: boolean; 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> </script>
<template> <template>
@ -14,10 +29,13 @@ const props = defineProps<{
:class="{ 'flex-col': props.singleLine, 'items-center': !props.singleLine }" :class="{ 'flex-col': props.singleLine, 'items-center': !props.singleLine }"
> >
<div class="space-y-1"> <div class="space-y-1">
<div> <div class="text-bold">
<a class="text-bold underline" :href="props.dueDate.url"> <a v-if="disableLink" class="underline" :href="props.dueDate.url">
{{ props.dueDate.title }} {{ props.dueDate.title }}
</a> </a>
<template v-else>
{{ props.dueDate.title }}
</template>
</div> </div>
<div class="text-small text-gray-900"> <div class="text-small text-gray-900">
<div v-if="props.dueDate.date_type_translation_key"> <div v-if="props.dueDate.date_type_translation_key">

View File

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

View File

@ -14,6 +14,8 @@ import { Popover, PopoverButton, PopoverPanel } from "@headlessui/vue";
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"; import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
import { computed, onMounted, reactive } from "vue"; import { computed, onMounted, reactive } from "vue";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import CoursePreviewBar from "@/components/header/CoursePreviewBar.vue";
import { getCompetenceBaseUrl } from "@/utils/utils";
log.debug("MainNavigationBar created"); log.debug("MainNavigationBar created");
@ -47,7 +49,8 @@ onMounted(() => {
</script> </script>
<template> <template>
<div> <CoursePreviewBar v-if="courseSessionsStore.hasCourseSessionPreview" />
<div v-else>
<Teleport to="body"> <Teleport to="body">
<MobileMenu <MobileMenu
v-if="userStore.loggedIn" v-if="userStore.loggedIn"
@ -100,43 +103,51 @@ onMounted(() => {
</div> </div>
</div> </div>
<div class="hidden space-x-8 lg:flex"> <!-- Satisfy the type checker; these menu items are
<!-- Navigation Links Desktop --> only relevant if there is a current course session -->
<router-link <template v-if="courseSessionsStore.currentCourseSession">
v-if=" <div class="hidden space-x-8 lg:flex">
inCourse() && <template v-if="courseSessionsStore.currentCourseSessionHasCockpit">
courseSessionsStore.currentCourseSession && <router-link
courseSessionsStore.currentCourseSessionHasCockpit :to="`${courseSessionsStore.currentCourseSession.course_url}/cockpit`"
" class="nav-item"
:to="`${courseSessionsStore.currentCourseSession.course_url}/cockpit`" :class="{ 'nav-item--active': inCockpit() }"
class="nav-item" >
:class="{ 'nav-item--active': inCockpit() }" {{ t("cockpit.title") }}
> </router-link>
{{ t("cockpit.title") }}
</router-link>
<router-link <router-link
v-if="inCourse() && courseSessionsStore.currentCourseSession" :to="courseSessionsStore.currentCourseSession.learning_path_url"
:to="courseSessionsStore.currentCourseSession.learning_path_url" target="_blank"
class="nav-item" class="nav-item"
:class="{ 'nav-item--active': inLearningPath() }" >
> <div class="flex items-center">
{{ t("general.learningPath") }} <span>{{ t("a.VorschauTeilnehmer") }}</span>
</router-link> <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 <router-link
v-if="inCourse() && courseSessionsStore.currentCourseSession" :to="
:to="`${courseSessionsStore.currentCourseSession.competence_url.replace( getCompetenceBaseUrl(courseSessionsStore.currentCourseSession)
// TODO: remove the `competence_url` with url to Navi... "
'/competences', class="nav-item"
'' :class="{ 'nav-item--active': inCompetenceProfile() }"
)}`" >
class="nav-item" {{ t("competences.title") }}
:class="{ 'nav-item--active': inCompetenceProfile() }" </router-link>
> </template>
{{ t("competences.title") }} </div>
</router-link> </template>
</div>
</div> </div>
<div class="flex items-stretch justify-start space-x-8"> <div class="flex items-stretch justify-start space-x-8">
@ -227,15 +238,4 @@ onMounted(() => {
.nav-item--active { .nav-item--active {
@apply border-sky-500; @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> </style>

View File

@ -1,9 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue"; import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import type { UserState } from "@/stores/user"; import type { UserState } from "@/stores/user";
import type { CourseSession } from "@/types"; import type { CourseSession } from "@/types";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { useCourseSessionsStore } from "@/stores/courseSessions"; import { getCompetenceBaseUrl } from "@/utils/utils";
const router = useRouter(); const router = useRouter();
@ -53,36 +54,32 @@ const courseSessionsStore = useCourseSessionsStore();
</div> </div>
<div> <div>
<div v-if="courseSession" class="mt-6 border-b"> <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"> <ul class="mt-6">
<li <template v-if="courseSessionsStore.currentCourseSessionHasCockpit">
v-if="courseSessionsStore.currentCourseSessionHasCockpit" <li class="mb-6">
class="mb-6" <button @click="clickLink(`${courseSession.course_url}/cockpit`)">
> {{ $t("cockpit.title") }}
<button @click="clickLink(`${courseSession?.course_url}/cockpit`)"> </button>
{{ $t("cockpit.title") }} </li>
</button> <li class="mb-6">
</li> <button @click="clickLink(courseSession.learning_path_url)">
<li class="mb-6"> {{ $t("a.VorschauTeilnehmer") }}
<button @click="clickLink(courseSession?.learning_path_url)"> </button>
{{ $t("general.learningPath") }} </li>
</button> </template>
</li> <template v-else>
<li class="mb-6"> <li class="mb-6">
<button <button @click="clickLink(courseSession.learning_path_url)">
@click=" {{ $t("general.learningPath") }}
clickLink( </button>
courseSession?.competence_url.replace( </li>
// TODO: remove the `competence_url` with url to Navi... <li class="mb-6">
'/competences', <button @click="clickLink(getCompetenceBaseUrl(courseSession))">
'' {{ $t("competences.title") }}
) </button>
) </li>
" </template>
>
{{ $t("competences.title") }}
</button>
</li>
<li class="mb-6"> <li class="mb-6">
<button <button
data-cy="medialibrary-link" data-cy="medialibrary-link"

View File

@ -29,8 +29,5 @@ const circleDates = computed(() => {
<DueDateSingle :due-date="dueDate" :single-line="true"></DueDateSingle> <DueDateSingle :due-date="dueDate" :single-line="true"></DueDateSingle>
</div> </div>
<div v-if="circleDates.length === 0">{{ $t("dueDates.noDueDatesAvailable") }}</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> </div>
</template> </template>

View File

@ -1,5 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import * as log from "loglevel"; 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"); log.debug("LearningContentContainer.vue setup");
@ -9,6 +13,7 @@ defineEmits(["exit"]);
<template> <template>
<div> <div>
<div class="absolute bottom-0 top-0 w-full bg-white"> <div class="absolute bottom-0 top-0 w-full bg-white">
<CoursePreviewBar v-if="courseSessionsStore.hasCourseSessionPreview" />
<div class="h-content overflow-y-auto"> <div class="h-content overflow-y-auto">
<header <header
class="relative flex h-12 w-full items-center justify-between bg-white px-4 lg:h-16 lg:px-8" class="relative flex h-12 w-full items-center justify-between bg-white px-4 lg:h-16 lg:px-8"

View File

@ -10,6 +10,7 @@ import type {
ExpertSessionUser, ExpertSessionUser,
} from "@/types"; } from "@/types";
import eventBus from "@/utils/eventBus"; import eventBus from "@/utils/eventBus";
import { useRouteLookups } from "@/utils/route";
import { useLocalStorage } from "@vueuse/core"; import { useLocalStorage } from "@vueuse/core";
import dayjs from "dayjs"; import dayjs from "dayjs";
import uniqBy from "lodash/uniqBy"; import uniqBy from "lodash/uniqBy";
@ -24,6 +25,7 @@ const SELECTED_COURSE_SESSIONS_KEY = "selectedCourseSessionMap";
export const useCourseSessionsStore = defineStore("courseSessions", () => { export const useCourseSessionsStore = defineStore("courseSessions", () => {
const loaded = ref(false); const loaded = ref(false);
const allCourseSessions = ref<CourseSession[]>([]); const allCourseSessions = ref<CourseSession[]>([]);
const { inCompetenceProfile, inLearningPath } = useRouteLookups();
async function loadCourseSessionsData(reload = false) { async function loadCourseSessionsData(reload = false) {
log.debug("loadCourseSessionsData called"); log.debug("loadCourseSessionsData called");
@ -134,6 +136,12 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
return false; return false;
}); });
const hasCourseSessionPreview = computed(() => {
const isCourseExpert =
currentCourseSession.value && currentCourseSessionHasCockpit.value;
return Boolean(isCourseExpert && (inLearningPath() || inCompetenceProfile()));
});
const circleExperts = computed(() => { const circleExperts = computed(() => {
const circleStore = useCircleStore(); const circleStore = useCircleStore();
const circleTranslationKey = circleStore.circle?.translation_key; const circleTranslationKey = circleStore.circle?.translation_key;
@ -262,6 +270,7 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
courseSessionForCourse, courseSessionForCourse,
switchCourseSession, switchCourseSession,
hasCockpit, hasCockpit,
hasCourseSessionPreview,
currentCourseSessionHasCockpit, currentCourseSessionHasCockpit,
canUploadCircleDocuments, canUploadCircleDocuments,
circleDocuments, circleDocuments,

View File

@ -1,3 +1,13 @@
import type { CourseSession } from "@/types";
export function assertUnreachable(msg: string): never { export function assertUnreachable(msg: string): never {
throw new Error("Didn't expect to get here, " + msg); 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",
""
);
}

23
cypress/e2e/preview.cy.js Normal file
View File

@ -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");
});
});