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:
parent
766af5444a
commit
1d77da83da
|
|
@ -1,61 +1,67 @@
|
|||
<template>
|
||||
<div>
|
||||
<FeedbackIntro
|
||||
v-if="stepNo === 0"
|
||||
:title="circleStore.circle?.title"
|
||||
:intro="
|
||||
$t('feedback.intro', {
|
||||
<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}`,
|
||||
})
|
||||
"
|
||||
@start="stepNo = 1"
|
||||
></FeedbackIntro>
|
||||
}}
|
||||
</p>
|
||||
<p v-if="stepNo > 0 && stepNo + 1 < numSteps" class="pb-2">
|
||||
{{ stepLabels[stepNo] }}
|
||||
</p>
|
||||
<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"
|
||||
/>
|
||||
<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"
|
||||
:label="$t('feedback.instructorCompetenceLabel')"
|
||||
class="mb-8"
|
||||
:items="RATINGS"
|
||||
/>
|
||||
|
|
@ -63,27 +69,11 @@
|
|||
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"
|
||||
/>
|
||||
<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"
|
||||
|
|
@ -96,16 +86,9 @@
|
|||
: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" />
|
||||
</div>
|
||||
</LearningContentMultiLayout>
|
||||
<!--
|
||||
<pre>
|
||||
satisfaction {{ satisfaction }}
|
||||
goalAttainment {{ goalAttainment }}
|
||||
|
|
@ -120,7 +103,6 @@
|
|||
courseNegativeFeedback {{ courseNegativeFeedback }}
|
||||
mutationResult: {{ mutationResult }}
|
||||
</pre> -->
|
||||
</div>
|
||||
</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 = () => {
|
||||
|
|
|
|||
|
|
@ -20,3 +20,10 @@ export const NavigationProgressPartial: Story = {
|
|||
endBadgeText: "Abgabe",
|
||||
},
|
||||
};
|
||||
|
||||
export const NavigationProgressNoStartEndBadge: Story = {
|
||||
args: {
|
||||
steps: 5,
|
||||
currentStep: 3,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<AssignmentView
|
||||
class="container-medium"
|
||||
:assignment-id="props.value.assignment"
|
||||
:learning-content="props.content"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
<template>
|
||||
<div class="container-medium">
|
||||
<div class="lg:mt-12">
|
||||
<FeedbackForm :page="content" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
@ -14,7 +10,7 @@ interface Value {
|
|||
description: string;
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
value: Value;
|
||||
content: LearningContent;
|
||||
}>();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
<template>
|
||||
<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;
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
<template>
|
||||
<LearningContentSimpleLayout
|
||||
:title="content.title"
|
||||
:subtitle="learningContentTypeData('media_library').title"
|
||||
:learning-content-type="'media_library'"
|
||||
>
|
||||
<div class="container-medium">
|
||||
<div class="lg:mt-8">
|
||||
<h1>{{ content.title }}</h1>
|
||||
|
||||
<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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
<template>
|
||||
<LearningContentSimpleLayout
|
||||
:title="content.title"
|
||||
:subtitle="learningContentTypeData('video').title"
|
||||
:learning-content-type="'video'"
|
||||
>
|
||||
<div class="container-medium">
|
||||
<iframe
|
||||
class="mt-8 aspect-video w-full"
|
||||
|
|
@ -9,10 +14,13 @@
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,22 +51,35 @@ 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">
|
||||
<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 }}
|
||||
</h2>
|
||||
</h3>
|
||||
|
||||
<div
|
||||
class="mt-4 flex flex-col justify-between gap-8 lg:mt-8 lg:flex-row lg:gap-12"
|
||||
|
|
@ -73,7 +94,9 @@ function handleBack() {
|
|||
@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>
|
||||
<span class="text-large font-bold">
|
||||
{{ $t("selfEvaluation.yes") }}.
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex flex-1 items-center border p-4 text-left"
|
||||
|
|
@ -85,7 +108,9 @@ function handleBack() {
|
|||
data-cy="fail"
|
||||
@click="circleStore.markCompletion(currentQuestion, 'fail')"
|
||||
>
|
||||
<it-icon-smiley-thinking class="mr-4 h-16 w-16"></it-icon-smiley-thinking>
|
||||
<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>
|
||||
|
|
@ -100,17 +125,8 @@ function handleBack() {
|
|||
</router-link>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue