Merged in feature/new-lc-navigation (pull request #60)

Implement new learning content navigation/layout

* Fix first part of cypress tests

* Add event bus type to fix typecheck

* Rework SelfEvaluation to support new layout

* Fix layout

* Hide lang switcher icon in lc footer

Closes https://iterativ.atlassian.net/browse/VBV-319

* Fix cypress tests

* Unregister event bus handler

* Hide ItNavigationProgress on self evaluations with
only a single step

* Last fixes

* Merged develop into feature/new-lc-navigation
This commit is contained in:
Elia Bieri 2023-04-25 10:06:24 +00:00
parent 766af5444a
commit 1d77da83da
24 changed files with 499 additions and 384 deletions

View File

@ -1,126 +1,108 @@
<template>
<div>
<FeedbackIntro
v-if="stepNo === 0"
:title="circleStore.circle?.title"
:intro="
$t('feedback.intro', {
name: `${courseSessionsStore.circleExperts[0].first_name} ${courseSessionsStore.circleExperts[0].last_name}`,
})
"
@start="stepNo = 1"
></FeedbackIntro>
<ItRadioGroup
v-if="stepNo === 1"
v-model="wouldRecommend"
:label="$t('feedback.recommendLabel')"
class="mb-8"
:items="YES_NO"
/>
<ItRadioGroup
v-if="stepNo === 2"
v-model="satisfaction"
:label="$t('feedback.satisfactionLabel')"
class="mb-8"
:items="RATINGS"
/>
<ItRadioGroup
v-if="stepNo === 3"
v-model="goalAttainment"
:label="$t('feedback.goalAttainmentLabel')"
class="mb-8"
:items="RATINGS"
/>
<ItRadioGroup
v-if="stepNo === 4"
v-model="proficiency"
:label="$t('feedback.proficiencyLabel')"
class="mb-8"
:items="PERCENTAGES"
/>
<ItRadioGroup
v-if="stepNo === 5"
v-model="receivedMaterials"
:label="$t('feedback.receivedMaterialsLabel')"
class="mb-8"
:items="YES_NO"
/>
<ItRadioGroup
v-if="stepNo === 5 && receivedMaterials"
v-model="materialsRating"
:label="$t('feedback.materialsRatingLabel')"
class="mb-8"
:items="RATINGS"
/>
<ItRadioGroup
v-if="stepNo === 6"
v-model="instructorCompetence"
:label="$t('feedback.instructorCompetenceLabel')"
class="mb-8"
:items="RATINGS"
/>
<ItRadioGroup
v-if="stepNo === 7"
v-model="instructorRespect"
class="mb-8"
:label="$t('feedback.instructorRespectLabel')"
:items="RATINGS"
/>
<ItTextarea
v-if="stepNo === 8"
v-model="instructorOpenFeedback"
:label="$t('feedback.instructorOpenFeedbackLabel')"
class="mb-8"
/>
<ItTextarea
v-if="stepNo === 9"
v-model="courseNegativeFeedback"
:label="$t('feedback.courseNegativeFeedbackLabel')"
class="mb-8"
/>
<ItTextarea
v-if="stepNo === 10"
v-model="coursePositiveFeedback"
:label="$t('feedback.coursePositiveFeedbackLabel')"
class="mb-8"
/>
<FeedbackCompletition
v-if="stepNo === 11"
:avatar-url="courseSessionsStore.circleExperts[0].avatar_url"
:title="
$t('feedback.completionTitle', {
name: `${courseSessionsStore.circleExperts[0].first_name} ${courseSessionsStore.circleExperts[0].last_name}`,
})
"
:description="$t('feedback.completionDescription')"
:feedback-sent="mutationResult != null"
@send-feedback="sendFeedback"
/>
<LearningContentNavigation
:show-back-button="stepNo > 0"
:show-next-button="stepNo > 0 && stepNo < MAX_STEPS - 1"
:question-index="stepNo"
:max-question-index="MAX_STEPS"
@back="previousStep"
@continue="nextStep"
></LearningContentNavigation>
<!-- <hr class="mb-10 mt-10" />
<pre>
satisfaction {{ satisfaction }}
goalAttainment {{ goalAttainment }}
proficiency {{ proficiency }}
receivedMaterials {{ receivedMaterials }}
materialsRating {{ materialsRating }}
instructorCompetence {{ instructorCompetence }}
instructorRespect {{ instructorRespect }}
instructorOpenFeedback {{ instructorOpenFeedback }}
wouldRecommend {{ wouldRecommend }}
coursePositiveFeedback {{ coursePositiveFeedback }}
courseNegativeFeedback {{ courseNegativeFeedback }}
mutationResult: {{ mutationResult }}
</pre> -->
</div>
<LearningContentMultiLayout
:title="title"
subtitle="Feedback"
:learning-content-type="'feedback'"
:show-start-button="stepNo === 0"
:show-next-button="stepNo > 0 && stepNo + 1 < numSteps"
:show-previous-button="stepNo > 0"
:show-exit-button="stepNo + 1 === numSteps"
:current-step="stepNo"
:steps-count="numSteps"
:start-badge-text="$t('feedback.introduction')"
:end-badge-text="$t('feedback.submission')"
@previous="previousStep()"
@next="nextStep()"
>
<div class="container-medium">
<p v-if="stepNo === 0" class="mt-10">
{{
$t("feedback.intro", {
name: `${courseSessionsStore.circleExperts[0].first_name} ${courseSessionsStore.circleExperts[0].last_name}`,
})
}}
</p>
<p v-if="stepNo > 0 && stepNo + 1 < numSteps" class="pb-2">
{{ stepLabels[stepNo] }}
</p>
<ItRadioGroup
v-if="stepNo === 1"
v-model="wouldRecommend"
class="mb-8"
:items="YES_NO"
/>
<ItRadioGroup
v-if="stepNo === 2"
v-model="satisfaction"
class="mb-8"
:items="RATINGS"
/>
<ItRadioGroup
v-if="stepNo === 3"
v-model="goalAttainment"
class="mb-8"
:items="RATINGS"
/>
<ItRadioGroup
v-if="stepNo === 4"
v-model="proficiency"
class="mb-8"
:items="PERCENTAGES"
/>
<ItRadioGroup
v-if="stepNo === 5"
v-model="receivedMaterials"
class="mb-8"
:items="YES_NO"
/>
<div v-if="stepNo === 5 && receivedMaterials">
<p class="pb-2">{{ t("feedback.materialsRatingLabel") }}</p>
<ItRadioGroup v-model="materialsRating" class="mb-8" :items="RATINGS" />
</div>
<ItRadioGroup
v-if="stepNo === 6"
v-model="instructorCompetence"
class="mb-8"
:items="RATINGS"
/>
<ItRadioGroup
v-if="stepNo === 7"
v-model="instructorRespect"
class="mb-8"
:items="RATINGS"
/>
<ItTextarea v-if="stepNo === 8" v-model="instructorOpenFeedback" class="mb-8" />
<ItTextarea v-if="stepNo === 9" v-model="courseNegativeFeedback" class="mb-8" />
<ItTextarea v-if="stepNo === 10" v-model="coursePositiveFeedback" class="mb-8" />
<FeedbackCompletition
v-if="stepNo === 11"
:avatar-url="courseSessionsStore.circleExperts[0].avatar_url"
:title="
$t('feedback.completionTitle', {
name: `${courseSessionsStore.circleExperts[0].first_name} ${courseSessionsStore.circleExperts[0].last_name}`,
})
"
:description="$t('feedback.completionDescription')"
:feedback-sent="mutationResult != null"
@send-feedback="sendFeedback"
/>
</div>
</LearningContentMultiLayout>
<!--
<pre>
satisfaction {{ satisfaction }}
goalAttainment {{ goalAttainment }}
proficiency {{ proficiency }}
receivedMaterials {{ receivedMaterials }}
materialsRating {{ materialsRating }}
instructorCompetence {{ instructorCompetence }}
instructorRespect {{ instructorRespect }}
instructorOpenFeedback {{ instructorOpenFeedback }}
wouldRecommend {{ wouldRecommend }}
coursePositiveFeedback {{ coursePositiveFeedback }}
courseNegativeFeedback {{ courseNegativeFeedback }}
mutationResult: {{ mutationResult }}
</pre> -->
</template>
<script setup lang="ts">
@ -128,31 +110,52 @@ import ItRadioGroup from "@/components/ui/ItRadioGroup.vue";
import ItTextarea from "@/components/ui/ItTextarea.vue";
import { graphql } from "@/gql/";
import type { SendFeedbackInput } from "@/gql/graphql";
import LearningContentNavigation from "@/pages/learningPath/learningContentPage/LearningContentNavigation.vue";
import FeedbackCompletition from "@/pages/learningPath/learningContentPage/feedback/FeedbackCompletition.vue";
import FeedbackIntro from "@/pages/learningPath/learningContentPage/feedback/FeedbackIntro.vue";
import {
PERCENTAGES,
RATINGS,
YES_NO,
} from "@/pages/learningPath/learningContentPage/feedback/feedback.constants";
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
import { useCircleStore } from "@/stores/circle";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import type { LearningContent } from "@/types";
import { useMutation } from "@urql/vue";
import log from "loglevel";
import { onMounted, reactive, ref } from "vue";
import { computed, onMounted, reactive, ref } from "vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ page: LearningContent }>();
const courseSessionsStore = useCourseSessionsStore();
const circleStore = useCircleStore();
const { t } = useI18n();
onMounted(async () => {
log.debug("Feedback mounted");
});
const stepNo = ref(0);
const MAX_STEPS = 12;
const title = computed(
() => `«${circleStore.circle?.title}»: ${t("feedback.areYouSatisfied")}`
);
const stepLabels = [
t("feedback.introduction"),
t("feedback.recommendLabel"),
t("feedback.satisfactionLabel"),
t("feedback.goalAttainmentLabel"),
t("feedback.proficiencyLabel"),
t("feedback.receivedMaterialsLabel"),
t("feedback.instructorCompetenceLabel"),
t("feedback.instructorRespectLabel"),
t("feedback.instructorOpenFeedbackLabel"),
t("feedback.courseNegativeFeedbackLabel"),
t("feedback.coursePositiveFeedbackLabel"),
t("feedback.submission"),
];
const numSteps = stepLabels.length;
const sendFeedbackMutation = graphql(`
mutation SendFeedbackMutation($input: SendFeedbackInput!) {
@ -189,9 +192,10 @@ const previousStep = () => {
}
};
const nextStep = () => {
if (stepNo.value < MAX_STEPS - 1) {
if (stepNo.value < numSteps) {
stepNo.value += 1;
}
log.info(`next step ${stepNo.value} of ${numSteps}`);
};
const sendFeedback = () => {

View File

@ -20,3 +20,10 @@ export const NavigationProgressPartial: Story = {
endBadgeText: "Abgabe",
},
};
export const NavigationProgressNoStartEndBadge: Story = {
args: {
steps: 5,
currentStep: 3,
},
};

View File

@ -2,16 +2,28 @@
import { computed } from "vue";
export interface Props {
// Number of steps including the start and end badge
steps: number;
// Current step, starting at 0 for the start badge
currentStep: number;
startBadgeText: string;
endBadgeText: string;
startBadgeText?: string;
endBadgeText?: string;
}
const props = defineProps<Props>();
const props = withDefaults(defineProps<Props>(), {
startBadgeText: undefined,
endBadgeText: undefined,
});
const hasStartBadge = computed(() => typeof props.startBadgeText !== "undefined");
const hasEndBadge = computed(() => typeof props.endBadgeText !== "undefined");
const numNumberSteps = computed(
() => props.steps - Number(hasStartBadge.value) - Number(hasEndBadge.value)
);
function getPillClasses(step: number) {
if (step == props.currentStep) {
if (step + Number(hasStartBadge.value) == props.currentStep) {
return "bg-sky-500 text-bold";
} else if (step < props.currentStep) {
return "bg-green-500";
@ -20,16 +32,16 @@ function getPillClasses(step: number) {
}
const startBadgeClasses = computed(() => {
if (0 == props.currentStep) {
if (0 === props.currentStep) {
return "bg-sky-500 text-bold";
}
return "bg-green-500";
});
const endBadgeClasses = computed(() => {
if (props.steps + 1 == props.currentStep) {
if (props.steps === props.currentStep + 1) {
return "bg-sky-500 text-bold";
} else if (props.steps + 2 == props.currentStep) {
} else if (props.steps === props.currentStep) {
return "bg-green-500 text-bold";
}
return "border";
@ -39,23 +51,28 @@ const endBadgeClasses = computed(() => {
<template>
<div class="flex flex-row text-sm">
<div
class="inline-flex h-7 items-center justify-center rounded-3xl px-3"
v-if="props.startBadgeText"
class="inline-flex h-7 items-center justify-center whitespace-nowrap rounded-3xl px-3 text-sm"
:class="startBadgeClasses"
>
{{ props.startBadgeText }}
</div>
<div v-for="step in props.steps" :key="step" class="flex flex-row">
<hr class="w-16 self-center border border-[1px] border-gray-400" />
<div v-for="(_, step) in numNumberSteps" :key="step" class="flex flex-row">
<hr
v-if="hasStartBadge || step !== 0"
class="w-8 self-center border border-gray-400"
/>
<div
class="inline-flex h-7 w-7 items-center justify-center rounded-full px-3 py-1"
class="inline-flex h-7 w-7 items-center justify-center rounded-full px-3 py-1 text-sm"
:class="getPillClasses(step)"
>
{{ step }}
{{ step + 1 }}
</div>
</div>
<hr class="w-16 self-center border border-gray-400" />
<hr v-if="hasEndBadge" class="w-8 self-center border border-gray-400" />
<div
class="inline-flex h-7 items-center justify-center rounded-3xl px-3"
v-if="endBadgeText"
class="inline-flex h-7 items-center justify-center whitespace-nowrap rounded-3xl px-3 text-sm"
:class="endBadgeClasses"
>
{{ props.endBadgeText }}

View File

@ -33,10 +33,14 @@ import type { RadioItem } from "@/pages/learningPath/learningContentPage/feedbac
import { RadioGroup, RadioGroupLabel, RadioGroupOption } from "@headlessui/vue";
defineProps<{
interface Props {
modelValue: any;
items: RadioItem<any>[];
label: string;
}>();
label?: string;
}
withDefaults(defineProps<Props>(), {
label: undefined,
});
defineEmits(["update:modelValue"]);
</script>

View File

@ -1,6 +1,6 @@
<template>
<div>
<h2 class="heading-1 mb-8 block">{{ label }}</h2>
<h2 v-if="label" class="heading-1 mb-8 block">{{ label }}</h2>
<textarea
:value="modelValue"
class="h-40 w-full border-gray-500"
@ -10,10 +10,14 @@
</template>
<script setup lang="ts">
defineProps<{
interface Props {
modelValue: string;
label: string;
}>();
label?: string;
}
withDefaults(defineProps<Props>(), {
label: undefined,
});
const emit = defineEmits(["update:modelValue"]);
const onInput = (event: Event) => {

View File

@ -70,6 +70,7 @@
"instructorOpenFeedbackLabel": "Was ich dem Kursleiter sonst noch sagen wollte:",
"instructorRespectLabel": "Fragen und Anregungen der Kursteilnehmenden wurden ernst genommen und aufgegriffen.",
"intro": "{name}, dein/e Trainer/-in, bittet dich, ihm/ihr Feedback zu geben. Das ist freiwillig, würde aber ihm/ihr helfen, deine Lernerlebniss zu verbessern.",
"introduction": "Einleitung",
"materialsRatingLabel": "Falls ja: Wie beurteilen Sie die Vorbereitungsunterlagen (z.B. eLearning)?",
"noFeedbacks": "Es wurden noch keine Feedbacks abgegeben",
"proficiencyLabel": "Wie beurteilen Sie Ihre Sicherheit bezüglichen den Themen nach dem Kurs?",
@ -80,6 +81,7 @@
"sendFeedback": "Feedback abschicken",
"sentByUsers": "Von {count} Teilnehmern ausgefüllt",
"showDetails": "Details anzeigen",
"submission": "Abgabe",
"unhappy": "Unzufrieden",
"veryHappy": "Sehr zufrieden",
"veryUnhappy": "Sehr unzufrieden"

View File

@ -33,12 +33,7 @@ function close() {
<template>
<div v-if="singleCriteria" class="absolute bottom-0 top-0 w-full bg-white">
<LearningContentContainer
:title="''"
:next-button-text="$t('general.save')"
@exit="router.back()"
@next="router.back()"
>
<LearningContentContainer @exit="router.back()">
<div v-if="singleCriteria" class="container-medium">
<div class="mt-4 border p-6 lg:mt-8 lg:p-12">
<h2 class="heading-2">

View File

@ -4,10 +4,11 @@ import { useCircleStore } from "@/stores/circle";
import type { LearningContent, LearningContentType } from "@/types";
import log from "loglevel";
import type { Component } from "vue";
import { computed } from "vue";
import { computed, onUnmounted } from "vue";
import AssignmentBlock from "@/pages/learningPath/learningContentPage/blocks/AssignmentBlock.vue";
import AttendanceDayBlock from "@/pages/learningPath/learningContentPage/blocks/AttendanceDayBlock.vue";
import eventBus from "@/utils/eventBus";
import DescriptionBlock from "./blocks/DescriptionBlock.vue";
import DescriptionTextBlock from "./blocks/DescriptionTextBlock.vue";
import FeedbackBlock from "./blocks/FeedbackBlock.vue";
@ -57,20 +58,21 @@ const component = computed(() => {
return DEFAULT_BLOCK;
});
const showNavigationBorder = computed(() => {
return block.value?.type !== "feedback";
function handleFinishedLearningContent() {
circleStore.continueFromLearningContent(props.learningContent);
}
eventBus.on("finishedLearningContent", handleFinishedLearningContent);
onUnmounted(() => {
eventBus.off("finishedLearningContent", handleFinishedLearningContent);
});
</script>
<template>
<LearningContentContainer
v-if="block"
:title="learningContent.title"
:next-button-text="$t('learningContent.completeAndContinue')"
:learning-content-block="learningContent.contents[0]"
:show-border="showNavigationBorder"
@exit="circleStore.closeLearningContent(props.learningContent)"
@next="circleStore.continueFromLearningContent(props.learningContent)"
>
<div>
<component

View File

@ -1,35 +1,18 @@
<script setup lang="ts">
import LearningContentBadge from "@/pages/learningPath/LearningContentTypeBadge.vue";
import type { LearningContentBlock } from "@/types";
import * as log from "loglevel";
log.debug("LearningContentContainer.vue setup");
interface Props {
title: string;
nextButtonText: string;
learningContentBlock: LearningContentBlock | null;
showBorder: boolean;
}
const _props = withDefaults(defineProps<Props>(), {
title: "",
nextButtonText: "",
learningContentBlock: null,
showBorder: true,
});
defineEmits(["next", "exit"]);
defineEmits(["exit"]);
</script>
<template>
<div>
<div class="h-full"></div>
<!-- just here to not make the footer jump during the transition -->
<div class="absolute bottom-0 top-0 w-full bg-white">
<div class="h-content overflow-y-scroll">
<div class="h-content overflow-y-auto">
<header
class="relative flex h-12 w-full items-center justify-between bg-white px-4 py-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"
>
<button
type="button"
@ -42,30 +25,6 @@ defineEmits(["next", "exit"]);
</header>
<slot></slot>
</div>
<nav
class="nav flex items-center justify-between bg-white px-4"
:class="{ 'border-t': showBorder }"
>
<div class="flex justify-between">
<LearningContentBadge
v-if="learningContentBlock && learningContentBlock.type"
:learning-content-type="learningContentBlock.type"
class="mr-2 hidden lg:flex"
/>
<h1 class="hidden text-base font-normal lg:inline-block" data-cy="ln-title">
{{ title }}
</h1>
</div>
<button
type="button"
class="btn-blue btn-large-icon z-10"
data-cy="complete-and-continue"
@click="$emit('next')"
>
{{ nextButtonText }}
<it-icon-check class="ml-2"></it-icon-check>
</button>
</nav>
</div>
</div>
</template>

View File

@ -1,61 +0,0 @@
<script setup lang="ts">
interface Props {
showBackButton: boolean;
showNextButton: boolean;
questionIndex: number;
maxQuestionIndex: number;
}
const _props = withDefaults(defineProps<Props>(), {
showBackButton: true,
showNextButton: true,
questionIndex: 0,
maxQuestionIndex: 0,
});
defineEmits(["back", "continue"]);
</script>
<template>
<div>
<nav class="mb-4 mt-16 flex">
<button
v-if="showBackButton"
class="btn-secondary mr-2 flex items-center"
data-cy="previous-step"
@click="$emit('back')"
>
<it-icon-arrow-left class="mr-2 h-6 w-6"></it-icon-arrow-left>
{{ $t("general.backCapitalized") }}
</button>
<button
v-if="showNextButton"
class="btn-secondary flex items-center"
data-cy="next-step"
@click="$emit('continue')"
>
{{ $t("general.next") }}
<it-icon-arrow-right class="ml-2 h-6 w-6"></it-icon-arrow-right>
</button>
</nav>
<div
role="progressbar"
:aria-valuenow="questionIndex + 1"
:aria-valuemax="maxQuestionIndex"
:aria-valuemin="1"
class="absolute bottom-[86px] left-0 right-0 inline-flex h-1 gap-1 lg:left-4 lg:right-4"
>
<span
v-for="i in maxQuestionIndex"
:key="i"
class="w-full"
:class="{
'bg-sky-500': i <= questionIndex + 1,
'bg-gray-400': i > questionIndex + 1,
}"
></span>
</div>
</div>
</template>
<style scoped></style>

View File

@ -1,5 +1,6 @@
<template>
<AssignmentView
class="container-medium"
:assignment-id="props.value.assignment"
:learning-content="props.content"
/>

View File

@ -1,9 +1,5 @@
<template>
<div class="container-medium">
<div class="lg:mt-12">
<FeedbackForm :page="content" />
</div>
</div>
<FeedbackForm :page="content" />
</template>
<script setup lang="ts">
@ -14,7 +10,7 @@ interface Value {
description: string;
}
defineProps<{
const props = defineProps<{
value: Value;
content: LearningContent;
}>();

View File

@ -1,11 +1,18 @@
<template>
<div class="h-screen">
<iframe width="100%" height="100%" scrolling="no" :src="value.url" />
</div>
<LearningContentSimpleLayout
:subtitle="learningContentTypeData('resource').title"
:learning-content-type="'resource'"
>
<div class="h-screen">
<iframe width="100%" height="100%" scrolling="no" :src="value.url" />
</div>
</LearningContentSimpleLayout>
</template>
<script setup lang="ts">
import LearningContentSimpleLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentSimpleLayout.vue";
import type { LearningContent } from "@/types";
import { learningContentTypeData } from "@/utils/typeMaps";
interface Value {
url: string;

View File

@ -1,18 +1,22 @@
<template>
<div class="container-medium">
<div class="lg:mt-8">
<h1>{{ content.title }}</h1>
<LearningContentSimpleLayout
:title="content.title"
:subtitle="learningContentTypeData('media_library').title"
:learning-content-type="'media_library'"
>
<div class="container-medium">
<p class="text-large my-4 lg:my-8">{{ value.description }}</p>
<router-link :to="`${value.url}?back=${route.path}`" class="button btn-primary">
Mediathek öffnen
</router-link>
</div>
</div>
</LearningContentSimpleLayout>
</template>
<script setup lang="ts">
import LearningContentSimpleLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentSimpleLayout.vue";
import type { LearningContent } from "@/types";
import { learningContentTypeData } from "@/utils/typeMaps";
import { useRoute } from "vue-router";
const route = useRoute();

View File

@ -1,14 +1,15 @@
<template>
<div class="container-medium">
<div class="lg:mt-8">
<p class="text-large my-4">{{ value.description }}</p>
<h1>{{ content.title }}</h1>
</div>
</div>
<LearningContentSimpleLayout
:title="content.title"
:subtitle="learningContentTypeData('placeholder').title"
:learning-content-type="'placeholder'"
></LearningContentSimpleLayout>
</template>
<script setup lang="ts">
import LearningContentSimpleLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentSimpleLayout.vue";
import type { LearningContent } from "@/types";
import { learningContentTypeData } from "@/utils/typeMaps";
interface Value {
description: string;

View File

@ -1,18 +1,26 @@
<template>
<div class="container-medium">
<iframe
class="mt-8 aspect-video w-full"
:src="value.url"
:title="content.title"
frameborder="0"
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
</div>
<LearningContentSimpleLayout
:title="content.title"
:subtitle="learningContentTypeData('video').title"
:learning-content-type="'video'"
>
<div class="container-medium">
<iframe
class="mt-8 aspect-video w-full"
:src="value.url"
:title="content.title"
frameborder="0"
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
</div>
</LearningContentSimpleLayout>
</template>
<script setup lang="ts">
import LearningContentSimpleLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentSimpleLayout.vue";
import type { LearningContent } from "@/types";
import { learningContentTypeData } from "@/utils/typeMaps";
interface Value {
url: string;

View File

@ -1,23 +0,0 @@
<template>
<div>
<h1 class="mb-8">«{{ title }}»: {{ $t("feedback.areYouSatisfied") }}</h1>
<p class="mb-8">{{ intro }}</p>
<button class="btn-primary" @click="$emit('start')">
{{ $t("general.start") }}
</button>
</div>
</template>
<script setup lang="ts">
interface Props {
title: string;
intro: string;
}
const _props = withDefaults(defineProps<Props>(), {
title: "",
intro: "",
});
defineEmits(["start"]);
</script>

View File

@ -0,0 +1,66 @@
<script setup lang="ts">
import eventBus from "@/utils/eventBus";
interface Props {
showStartButton?: boolean;
showPreviousButton?: boolean;
showNextButton?: boolean;
showExitButton?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
showStartButton: false,
showPreviousButton: false,
showNextButton: false,
showExitButton: true,
});
defineEmits(["start", "previous", "next"]);
</script>
<template>
<nav class="nav absolute bottom-0 w-full border-t px-6 py-4">
<div class="relative z-10 flex flex-row place-content-end bg-white">
<button
v-if="props.showPreviousButton"
class="btn-secondary mr-2 flex items-center"
data-cy="previous-step"
@click="$emit('previous')"
>
<it-icon-arrow-left class="mr-2 h-6 w-6"></it-icon-arrow-left>
{{ $t("general.backCapitalized") }}
</button>
<button
v-if="props.showNextButton"
class="btn-blue z-10 flex items-center"
data-cy="next-step"
@click="$emit('next')"
>
{{ $t("general.next") }}
<it-icon-arrow-right class="ml-2 h-6 w-6"></it-icon-arrow-right>
</button>
<button
v-if="props.showStartButton"
type="button"
class="btn-blue z-10 flex items-center"
data-cy="start"
@click="$emit('start')"
>
{{ $t("general.start") }}
<it-icon-arrow-right class="ml-2 h-6 w-6"></it-icon-arrow-right>
</button>
<button
v-if="props.showExitButton"
type="button"
class="btn-blue z-10 flex items-center"
data-cy="complete-and-continue"
@click="eventBus.emit('finishedLearningContent', true)"
>
{{ "Als erledigt markieren" }}
<it-icon-check class="ml-2"></it-icon-check>
</button>
</div>
</nav>
</template>
<style scoped></style>

View File

@ -0,0 +1,68 @@
<script setup lang="ts">
// Layout for learning contents with multiple steps
import ItNavigationProgress from "@/components/ui/ItNavigationProgress.vue";
import LearningContentFooter from "@/pages/learningPath/learningContentPage/layouts/LearningContentFooter.vue";
import type { LearningContentType } from "@/types";
import { learningContentTypeData } from "@/utils/typeMaps";
interface Props {
title: string | undefined;
subtitle: string;
learningContentType: LearningContentType;
showStartButton: boolean;
showPreviousButton: boolean;
showNextButton: boolean;
showExitButton: boolean;
currentStep: number;
stepsCount: number;
startBadgeText?: string;
endBadgeText?: string;
}
const props = withDefaults(defineProps<Props>(), {
title: undefined,
startBadgeText: undefined,
endBadgeText: undefined,
});
const emit = defineEmits(["previous", "next", "exit"]);
</script>
<template>
<div class="container-medium">
<div
v-if="props.learningContentType !== 'placeholder'"
class="flex h-min w-min items-center gap-2 rounded-full pb-10"
>
<component
:is="learningContentTypeData(props.learningContentType).icon"
class="h-6 w-6 text-gray-900"
></component>
<p class="whitespace-nowrap text-gray-900">
{{ props.subtitle }}
</p>
</div>
<h2 v-if="props.title" class="pb-6 text-3xl" data-cy="ln-title">
{{ props.title }}
</h2>
<ItNavigationProgress
v-if="props.stepsCount > 1"
:current-step="props.currentStep"
:start-badge-text="props.startBadgeText"
:steps="stepsCount"
:end-badge-text="props.endBadgeText"
class="overflow-hidden pb-12"
></ItNavigationProgress>
<slot></slot>
</div>
<LearningContentFooter
:show-start-button="props.showStartButton"
:show-next-button="props.showNextButton"
:show-previous-button="props.showPreviousButton"
:show-exit-button="props.showExitButton"
@previous="emit('previous')"
@next="emit('next')"
@start="emit('next')"
></LearningContentFooter>
</template>

View File

@ -0,0 +1,37 @@
<script setup lang="ts">
// Basic layout for a learning content that only has a single step
import LearningContentFooter from "@/pages/learningPath/learningContentPage/layouts/LearningContentFooter.vue";
import type { LearningContentType } from "@/types";
import { learningContentTypeData } from "@/utils/typeMaps";
interface Props {
title: string | undefined;
subtitle: string;
learningContentType: LearningContentType;
}
const props = withDefaults(defineProps<Props>(), {
title: undefined,
});
</script>
<template>
<div class="container-medium">
<div
v-if="props.learningContentType !== 'placeholder'"
class="flex h-min w-full items-center gap-2 pb-8"
>
<component
:is="learningContentTypeData(props.learningContentType).icon"
class="h-6 w-6 text-gray-900"
></component>
<p class="whitespace-nowrap text-gray-900">
{{ props.subtitle }}
</p>
</div>
<h2 v-if="props.title" data-cy="ln-title">{{ props.title }}</h2>
</div>
<slot></slot>
<LearningContentFooter></LearningContentFooter>
</template>

View File

@ -5,9 +5,10 @@ import * as log from "loglevel";
import { COMPLETION_FAILURE, COMPLETION_SUCCESS } from "@/constants";
import LearningContentContainer from "@/pages/learningPath/learningContentPage/LearningContentContainer.vue";
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import eventBus from "@/utils/eventBus";
import { computed, reactive } from "vue";
import LearningContentNavigation from "../learningContentPage/LearningContentNavigation.vue";
log.debug("LearningContent.vue setup");
@ -24,7 +25,14 @@ const props = defineProps<{
const questions = computed(() => props.learningUnit?.children);
const currentQuestion = computed(() => questions.value[state.questionIndex]);
const showBackButton = computed(() => state.questionIndex != 0);
const showPreviousButton = computed(() => state.questionIndex != 0);
const showNextButton = computed(
() => state.questionIndex + 1 < questions.value?.length && questions.value?.length > 1
);
const showExitButton = computed(
() =>
questions.value?.length === 1 || questions.value?.length === state.questionIndex + 1
);
function handleContinue() {
log.debug("handleContinue");
@ -43,74 +51,82 @@ function handleBack() {
state.questionIndex -= 1;
}
}
eventBus.on("finishedLearningContent", () => {
circleStore.closeSelfEvaluation(props.learningUnit);
});
</script>
<template>
<div v-if="learningUnit">
<LearningContentContainer
:title="$t('selfEvaluation.title', { title: learningUnit.title })"
:next-button-text="$t('learningContent.completeAndContinue')"
:show-border="false"
@exit="circleStore.closeSelfEvaluation(props.learningUnit)"
@next="circleStore.closeSelfEvaluation(props.learningUnit)"
>
<div class="container-medium h-full">
<div class="mt-8 lg:mt-40">
<h2 class="heading-2">
{{ currentQuestion.competence_id }} {{ currentQuestion.title }}
</h2>
<LearningContentMultiLayout
:current-step="state.questionIndex"
:subtitle="$t('selfEvaluation.title')"
:title="$t('selfEvaluation.title', { title: learningUnit.title })"
learning-content-type="learningmodule"
:steps-count="questions.length"
:show-next-button="showNextButton"
:show-exit-button="showExitButton"
:show-start-button="false"
:show-previous-button="showPreviousButton"
@previous="handleBack()"
@next="handleContinue()"
>
<div class="h-full">
<div class="mt-8">
<h3 class="heading-3">
{{ currentQuestion.competence_id }} {{ currentQuestion.title }}
</h3>
<div
class="mt-4 flex flex-col justify-between gap-8 lg:mt-8 lg:flex-row lg:gap-12"
>
<button
class="inline-flex flex-1 items-center border p-4 text-left"
:class="{
'border-green-500': currentQuestion.completion_status === 'success',
'border-2': currentQuestion.completion_status === 'success',
}"
data-cy="success"
@click="circleStore.markCompletion(currentQuestion, COMPLETION_SUCCESS)"
<div
class="mt-4 flex flex-col justify-between gap-8 lg:mt-8 lg:flex-row lg:gap-12"
>
<it-icon-smiley-happy class="mr-4 h-16 w-16"></it-icon-smiley-happy>
<span class="text-large font-bold">{{ $t("selfEvaluation.yes") }}.</span>
</button>
<button
class="inline-flex flex-1 items-center border p-4 text-left"
:class="{
'border-orange-500':
currentQuestion.completion_status === COMPLETION_FAILURE,
'border-2': currentQuestion.completion_status === COMPLETION_FAILURE,
}"
data-cy="fail"
@click="circleStore.markCompletion(currentQuestion, 'fail')"
>
<it-icon-smiley-thinking class="mr-4 h-16 w-16"></it-icon-smiley-thinking>
<span class="text-xl font-bold">{{ $t("selfEvaluation.no") }}.</span>
</button>
</div>
<button
class="inline-flex flex-1 items-center border p-4 text-left"
:class="{
'border-green-500': currentQuestion.completion_status === 'success',
'border-2': currentQuestion.completion_status === 'success',
}"
data-cy="success"
@click="circleStore.markCompletion(currentQuestion, COMPLETION_SUCCESS)"
>
<it-icon-smiley-happy class="mr-4 h-16 w-16"></it-icon-smiley-happy>
<span class="text-large font-bold">
{{ $t("selfEvaluation.yes") }}.
</span>
</button>
<button
class="inline-flex flex-1 items-center border p-4 text-left"
:class="{
'border-orange-500':
currentQuestion.completion_status === COMPLETION_FAILURE,
'border-2': currentQuestion.completion_status === COMPLETION_FAILURE,
}"
data-cy="fail"
@click="circleStore.markCompletion(currentQuestion, 'fail')"
>
<it-icon-smiley-thinking
class="mr-4 h-16 w-16"
></it-icon-smiley-thinking>
<span class="text-xl font-bold">{{ $t("selfEvaluation.no") }}.</span>
</button>
</div>
<div class="mt-6 lg:mt-12">
{{ $t("selfEvaluation.progressText") }}
<router-link
:to="courseSession.currentCourseSession?.competence_url || '/'"
class="text-primary-500 underline"
>
{{ $t("selfEvaluation.progressLink") }}
</router-link>
<div class="mt-6 lg:mt-12">
{{ $t("selfEvaluation.progressText") }}
<router-link
:to="courseSession.currentCourseSession?.competence_url || '/'"
class="text-primary-500 underline"
>
{{ $t("selfEvaluation.progressLink") }}
</router-link>
</div>
</div>
</div>
<LearningContentNavigation
:show-next-button="
questions.length > 1 && state.questionIndex + 1 < questions.length
"
:show-back-button="showBackButton"
:question-index="state.questionIndex"
:max-question-index="questions.length"
@back="handleBack"
@continue="handleContinue"
></LearningContentNavigation>
</div>
</LearningContentMultiLayout>
</LearningContentContainer>
</div>
</template>

View File

@ -3,6 +3,7 @@ import mitt from "mitt";
export type MittEvents = {
// FIXME: clean up with VBV-305
switchedCourseSession: number;
finishedLearningContent: boolean;
};
const eventBus = mitt<MittEvents>();

View File

@ -38,15 +38,15 @@ describe("circle page", () => {
"contain",
"Verschaffe dir einen Überblick"
);
cy.get('[data-cy="complete-and-continue"]').click();
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
cy.get('[data-cy="ls-continue-button"]').click();
cy.get('[data-cy="ln-title"]').should("contain", "Mediathek Fahrzeug");
cy.get('[data-cy="complete-and-continue"]').click();
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
cy.get('[data-cy="ls-continue-button"]').click();
cy.get('[data-cy="ln-title"]').should("contain", "Vorbereitungsauftrag");
cy.get('[data-cy="complete-and-continue"]').click();
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
cy.get(
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick-checkbox"] > .cy-checkbox-checked'
@ -68,7 +68,7 @@ describe("circle page", () => {
"contain",
"Verschaffe dir einen Überblick"
);
cy.get('[data-cy="complete-and-continue"]').click();
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
cy.get('[data-cy="ls-continue-button"]').should("contain", "Weiter geht's");
cy.get('[data-cy="ls-continue-button"]').click();

View File

@ -105,9 +105,9 @@ Cypress.Commands.add('makeSelfEvaluation', (answers) => {
cy.get('[data-cy="fail"]').click();
}
if (i < answers.length - 1) {
cy.get('[data-cy="next-step"]').click();
cy.get('[data-cy="next-step"]').click({ force: true });
} else {
cy.get('[data-cy="complete-and-continue"]').click();
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
}
}
});