Merged develop into feature/VBV-774-course-autogewerbe
This commit is contained in:
commit
908f3cade9
|
|
@ -285,3 +285,6 @@ git-crypt-encrypted-files-check.txt
|
|||
/client/src/gql/dist/minifiedSchema.json
|
||||
|
||||
/sftptest/
|
||||
|
||||
# BabelEdit translation software
|
||||
/client/bable_edit.babel
|
||||
|
|
|
|||
17
README.md
17
README.md
|
|
@ -181,15 +181,23 @@ Deployment happens manually via Bitbucket Pipelines
|
|||
|
||||
Bitbucket Pipelines name: myvbv
|
||||
https://myvbv.control.iterativ.ch/
|
||||
https://myvbv.iterativ.ch/
|
||||
|
||||
https://myvbv.iterativ.ch/v
|
||||
Deployment happens manually via Bitbucket Pipelines
|
||||
|
||||
|
||||
### CapRover feature branch deployment
|
||||
|
||||
You can deploy every feature branch to CapRover directly from Bitbucket Pipelines
|
||||
with a manual step.
|
||||
with a manual step. Either in the "normal" pipeline by running "deploy feature"
|
||||
as last manual step.
|
||||
|
||||
You can also select deploy-step direclty under
|
||||
|
||||
`Branches > Actions > Run pipeline` for a branch
|
||||
and then `custom: deploy-feature-branch -> Run` in the modal.
|
||||
|
||||

|
||||

|
||||
|
||||
When you run caprover_deploy.sh without arguments, it will deploy the current branch
|
||||
|
||||
|
|
@ -199,6 +207,9 @@ When you run caprover_deploy.sh without arguments, it will deploy the current br
|
|||
|
||||
### Cleanup caprover feature branch deployments
|
||||
|
||||
You can delete the CapRover App and DB directly in the CapRover interface
|
||||
or use the `caprover_cleanup.py` script.
|
||||
|
||||
```bash
|
||||
# by default it will delete all vbv-feature-* apps
|
||||
python caprover_cleanup.py
|
||||
|
|
|
|||
|
|
@ -64,6 +64,11 @@ def main(app_name, image_name, environment_file):
|
|||
|
||||
default_allowed_hosts = f"{app_name}.iterativ.ch,{app_name}.control.iterativ.ch"
|
||||
|
||||
app_environment = env.str("IT_APP_ENVIRONMENT", "dev-feature")
|
||||
if app_environment == "local":
|
||||
# local is never the correct value...
|
||||
app_environment = "dev-feature"
|
||||
|
||||
cap.create_and_update_app(
|
||||
app_name=app_name,
|
||||
enable_ssl=True,
|
||||
|
|
@ -72,7 +77,7 @@ def main(app_name, image_name, environment_file):
|
|||
image_name=image_name,
|
||||
container_http_port=7555,
|
||||
environment_variables={
|
||||
"IT_APP_ENVIRONMENT": env.str("IT_APP_ENVIRONMENT", "dev-feature"),
|
||||
"IT_APP_ENVIRONMENT": app_environment,
|
||||
"IT_DEFAULT_ADMIN_PASSWORD": env.str(
|
||||
"IT_DEFAULT_ADMIN_PASSWORD", "ACEEs0DCmNaPxdoNV8vhccuCTRl9b"
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="flex min-h-full flex-col">
|
||||
<MainNavigationBar v-if="!route.meta.hideChrome" class="flex-none" />
|
||||
<HeaderBar v-if="!route.meta.hideChrome" class="flex-none" />
|
||||
<CloseButton v-if="route.meta.showCloseButton" class="flex-none" />
|
||||
<RouterView v-slot="{ Component }" class="flex-auto">
|
||||
<Transition mode="out-in" name="app">
|
||||
|
|
@ -15,13 +15,14 @@
|
|||
import log from "loglevel";
|
||||
|
||||
import AppFooter from "@/components/AppFooter.vue";
|
||||
import MainNavigationBar from "@/components/header/MainNavigationBar.vue";
|
||||
// import MainNavigationBar from "@/components/header/MainNavigationBar.vue";
|
||||
import { graphqlClient } from "@/graphql/client";
|
||||
import eventBus from "@/utils/eventBus";
|
||||
import { provideClient } from "@urql/vue";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import CloseButton from "./components/header/CloseButton.vue";
|
||||
import HeaderBar from "./components/header/HeaderBar.vue";
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,13 @@ const actionButtonProps = computed<{ href: string; text: string; cyKey: string }
|
|||
cyKey: "lm-dashboard-link",
|
||||
};
|
||||
}
|
||||
if (props.courseConfig?.role_key === "MentorUK") {
|
||||
return {
|
||||
href: getLearningPathUrl(props.courseConfig?.course_slug),
|
||||
text: "a.Teilnehmer Vorschau",
|
||||
cyKey: "attendance-dashboard-link",
|
||||
};
|
||||
}
|
||||
return {
|
||||
href: getLearningPathUrl(props.courseConfig?.course_slug),
|
||||
text: "Weiter lernen",
|
||||
|
|
@ -75,7 +82,6 @@ const actionButtonProps = computed<{ href: string; text: string; cyKey: string }
|
|||
|
||||
function hasActionButton(): boolean {
|
||||
return (
|
||||
props.courseConfig?.role_key !== "MentorUK" &&
|
||||
props.courseConfig?.role_key !== "Ausbildungsverantwortlicher" &&
|
||||
props.courseConfig?.role_key !== "Berufsbildner"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ const summary = computed(() => {
|
|||
return mentorAssignmentData.value?.assignments ?? null;
|
||||
});
|
||||
|
||||
const courseSlug = computed(() => mentorAssignmentData.value?.course_slug);
|
||||
|
||||
onMounted(async () => {
|
||||
mentorAssignmentData.value = await fetchMentorCompetenceSummary(
|
||||
props.courseId,
|
||||
|
|
@ -29,7 +31,7 @@ onMounted(async () => {
|
|||
<template>
|
||||
<div v-if="summary" class="w-[325px]">
|
||||
<BaseBox
|
||||
:details-link="`/dashboard/persons-competence?course=${props.courseId}`"
|
||||
:details-link="`/statistic/${props.agentRole}/${courseSlug}/assignment`"
|
||||
data-cy="dashboard.mentor.competenceSummary"
|
||||
>
|
||||
<template #title>{{ $t("Kompetenznachweise") }}</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
<script setup lang="ts">
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useNavigationAttributes } from "@/utils/navigation";
|
||||
import { useRouteLookups } from "@/utils/route";
|
||||
import {
|
||||
getCockpitUrl,
|
||||
getCompetenceNaviUrl,
|
||||
getLearningMentorUrl,
|
||||
getLearningPathUrl,
|
||||
} from "@/utils/utils";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
const { t } = useTranslation();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
|
||||
const { inCockpit, inCompetenceProfile, inLearningMentor, inLearningPath } =
|
||||
useRouteLookups();
|
||||
const {
|
||||
hasCompetenceNaviMenu,
|
||||
hasLearningPathMenu,
|
||||
hasCockpitMenu,
|
||||
hasPreviewMenu,
|
||||
hasLearningMentor,
|
||||
} = useNavigationAttributes();
|
||||
const mentorTabTitle = computed(() =>
|
||||
courseSessionsStore.currentCourseSession?.course.configuration.is_uk
|
||||
? "a.Praxisbildner"
|
||||
: "a.Lernbegleitung"
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div v-if="courseSessionsStore.currentCourseSession" class="hidden space-x-8 lg:flex">
|
||||
<router-link
|
||||
v-if="hasCockpitMenu"
|
||||
data-cy="navigation-cockpit-link"
|
||||
:to="getCockpitUrl(courseSessionsStore.currentCourseSession.course.slug)"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inCockpit() }"
|
||||
>
|
||||
{{ t("cockpit.title") }}
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-if="hasPreviewMenu"
|
||||
data-cy="navigation-preview-link"
|
||||
:to="getLearningPathUrl(courseSessionsStore.currentCourseSession.course.slug)"
|
||||
target="_blank"
|
||||
class="nav-item"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<span>{{ t("a.Vorschau Teilnehmer") }}</span>
|
||||
<it-icon-external-link class="ml-2" />
|
||||
</div>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="hasLearningPathMenu"
|
||||
data-cy="navigation-learning-path-link"
|
||||
:to="getLearningPathUrl(courseSessionsStore.currentCourseSession.course.slug)"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inLearningPath() }"
|
||||
>
|
||||
{{ t("general.learningPath") }}
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-if="hasCompetenceNaviMenu"
|
||||
data-cy="navigation-competence-profile-link"
|
||||
:to="getCompetenceNaviUrl(courseSessionsStore.currentCourseSession.course.slug)"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inCompetenceProfile() }"
|
||||
>
|
||||
{{ t("competences.title") }}
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-if="hasLearningMentor"
|
||||
data-cy="navigation-learning-mentor-link"
|
||||
:to="getLearningMentorUrl(courseSessionsStore.currentCourseSession.course.slug)"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inLearningMentor() }"
|
||||
>
|
||||
{{ t(mentorTabTitle) }}
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import MainNavigationBar from "./MainNavigationBar.vue";
|
||||
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CoursePreviewBar v-if="courseSessionsStore.isCourseSessionPreviewActive" />
|
||||
<template v-else>
|
||||
<MainNavigationBar />
|
||||
</template>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<script setup lang="ts">
|
||||
import { useTranslation } from "i18next-vue";
|
||||
|
||||
const { t } = useTranslation();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="hidden flex-shrink-0 items-center lg:flex">
|
||||
<div class="flex items-center">
|
||||
<router-link to="/" class="flex">
|
||||
<it-icon-vbv class="-ml-3 -mt-6 mr-3 h-8 w-16" />
|
||||
</router-link>
|
||||
<router-link to="/" class="flex">
|
||||
<div class="ml-1 border-l border-white pl-3 pr-10 text-2xl text-white">
|
||||
{{ t("general.title") }}
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
- nav
|
||||
- default
|
||||
- backnav
|
||||
- cockpitnav
|
||||
- course selector
|
||||
- subnav
|
||||
|
||||
- mediathek
|
||||
- calendar
|
||||
- notifications
|
||||
- profile
|
||||
- links
|
||||
|
|
@ -1,56 +1,22 @@
|
|||
<script setup lang="ts">
|
||||
import AccountMenu from "@/components/header/AccountMenu.vue";
|
||||
import CoursePreviewBar from "@/components/header/CoursePreviewBar.vue";
|
||||
import MobileMenu from "@/components/header/MobileMenu.vue";
|
||||
import NotificationPopover from "@/components/notifications/NotificationPopover.vue";
|
||||
import NotificationPopoverContent from "@/components/notifications/NotificationPopoverContent.vue";
|
||||
import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
|
||||
import { getLoginURL } from "@/router/utils";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useNotificationsStore } from "@/stores/notifications";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { useNavigationAttributes } from "@/utils/navigation";
|
||||
import { useRouteLookups } from "@/utils/route";
|
||||
import {
|
||||
getCockpitUrl,
|
||||
getCompetenceNaviUrl,
|
||||
getLearningMentorUrl,
|
||||
getLearningPathUrl,
|
||||
getMediaCenterUrl,
|
||||
} from "@/utils/utils";
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/vue";
|
||||
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import { getMediaCenterUrl } from "@/utils/utils";
|
||||
import log from "loglevel";
|
||||
import { computed, onMounted, reactive } from "vue";
|
||||
import { computed, onMounted } from "vue";
|
||||
import CourseSessionNavigation from "./CourseSessionNavigation.vue";
|
||||
import HomeNavigation from "./HomeNavigation.vue";
|
||||
import MobileMenuButton from "./MobileMenuButton.vue";
|
||||
import NotificationButton from "./NotificationButton.vue";
|
||||
import ProfileMenuButton from "./ProfileMenuButton.vue";
|
||||
|
||||
log.debug("MainNavigationBar created");
|
||||
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind);
|
||||
const userStore = useUserStore();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const notificationsStore = useNotificationsStore();
|
||||
const {
|
||||
inCockpit,
|
||||
inCompetenceProfile,
|
||||
inLearningMentor,
|
||||
inCourse,
|
||||
inLearningPath,
|
||||
inMediaLibrary,
|
||||
inAppointments,
|
||||
} = useRouteLookups();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const state = reactive({
|
||||
showMobileNavigationMenu: false,
|
||||
showMobileProfileMenu: false,
|
||||
});
|
||||
|
||||
function popoverClick(event: Event) {
|
||||
if (breakpoints.smaller("lg").value) {
|
||||
event.preventDefault();
|
||||
state.showMobileProfileMenu = true;
|
||||
}
|
||||
}
|
||||
const { inMediaLibrary, inAppointments } = useRouteLookups();
|
||||
const { hasMediaLibraryMenu, hasAppointmentsMenu, hasSessionTitle } =
|
||||
useNavigationAttributes();
|
||||
|
||||
const selectedCourseSessionTitle = computed(() => {
|
||||
return courseSessionsStore.currentCourseSession?.title;
|
||||
|
|
@ -68,333 +34,59 @@ const appointmentsUrl = computed(() => {
|
|||
onMounted(() => {
|
||||
log.debug("MainNavigationBar mounted");
|
||||
});
|
||||
|
||||
const hasLearningPathMenu = computed(() =>
|
||||
Boolean(
|
||||
courseSessionsStore.currentCourseSession?.actions.includes("learning-path") &&
|
||||
inCourse()
|
||||
)
|
||||
);
|
||||
|
||||
const hasCompetenceNaviMenu = computed(() =>
|
||||
Boolean(
|
||||
courseSessionsStore.currentCourseSession?.actions.includes("competence-navi") &&
|
||||
inCourse()
|
||||
)
|
||||
);
|
||||
|
||||
const hasMediaLibraryMenu = computed(() =>
|
||||
Boolean(
|
||||
courseSessionsStore.currentCourseSession?.actions.includes("media-library") &&
|
||||
inCourse()
|
||||
)
|
||||
);
|
||||
|
||||
const hasCockpitMenu = computed(
|
||||
() =>
|
||||
Boolean(
|
||||
courseSessionsStore.currentCourseSession?.actions.includes("expert-cockpit")
|
||||
) && inCourse()
|
||||
);
|
||||
|
||||
const hasPreviewMenu = computed(
|
||||
() =>
|
||||
Boolean(courseSessionsStore.currentCourseSession?.actions.includes("preview")) &&
|
||||
inCourse()
|
||||
);
|
||||
|
||||
const hasAppointmentsMenu = computed(() =>
|
||||
Boolean(
|
||||
courseSessionsStore.currentCourseSession?.actions.includes("appointments") &&
|
||||
userStore.loggedIn &&
|
||||
inCourse()
|
||||
)
|
||||
);
|
||||
|
||||
const hasNotificationsMenu = computed(() => {
|
||||
return userStore.loggedIn;
|
||||
});
|
||||
|
||||
const hasLearningMentor = computed(() => {
|
||||
if (!inCourse()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!courseSessionsStore.currentCourseSession) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const courseSession = courseSessionsStore.currentCourseSession;
|
||||
return courseSession.actions.includes("learning-mentor");
|
||||
});
|
||||
|
||||
const mentorTabTitle = computed(() =>
|
||||
courseSessionsStore.currentCourseSession?.course.configuration.is_uk
|
||||
? "a.Praxisbildner"
|
||||
: "a.Lernbegleitung"
|
||||
);
|
||||
|
||||
const hasSessionTitle = computed(() => {
|
||||
return courseSessionsStore.currentCourseSession?.title && inCourse();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CoursePreviewBar v-if="courseSessionsStore.isCourseSessionPreviewActive" />
|
||||
<div v-else>
|
||||
<Teleport to="body">
|
||||
<MobileMenu
|
||||
v-if="userStore.loggedIn"
|
||||
:show="state.showMobileNavigationMenu"
|
||||
:course-session="courseSessionsStore.currentCourseSession"
|
||||
:has-media-library-menu="hasMediaLibraryMenu"
|
||||
:has-cockpit-menu="hasCockpitMenu"
|
||||
:has-preview-menu="hasPreviewMenu"
|
||||
:has-learning-path-menu="hasLearningPathMenu"
|
||||
:has-competence-navi-menu="hasCompetenceNaviMenu"
|
||||
:has-learning-mentor="hasLearningMentor"
|
||||
:has-notifications-menu="hasNotificationsMenu"
|
||||
:has-appointments-menu="hasAppointmentsMenu"
|
||||
:media-url="
|
||||
getMediaCenterUrl(courseSessionsStore.currentCourseSession?.course?.slug)
|
||||
"
|
||||
:user="userStore"
|
||||
@closemodal="state.showMobileNavigationMenu = false"
|
||||
@logout="userStore.handleLogout()"
|
||||
/>
|
||||
</Teleport>
|
||||
<Teleport to="body">
|
||||
<ItFullScreenModal
|
||||
v-if="userStore.loggedIn"
|
||||
:show="state.showMobileProfileMenu"
|
||||
@closemodal="state.showMobileProfileMenu = false"
|
||||
>
|
||||
<AccountMenu @close="state.showMobileProfileMenu = false" />
|
||||
</ItFullScreenModal>
|
||||
</Teleport>
|
||||
<Transition name="nav">
|
||||
<nav class="bg-blue-900 text-white">
|
||||
<div class="mx-auto px-4 lg:px-8">
|
||||
<div class="relative flex h-16 justify-between">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center lg:hidden">
|
||||
<!-- Mobile menu button -->
|
||||
<div
|
||||
data-cy="navigation-mobile-menu-button"
|
||||
class="flex"
|
||||
@click="state.showMobileNavigationMenu = true"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="h-8 w-8 text-white hover:text-sky-500 focus:text-sky-500 focus:outline-none"
|
||||
>
|
||||
<it-icon-menu class="h-8 w-8" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="bg-blue-900 text-white">
|
||||
<div class="mx-auto px-4 lg:px-8">
|
||||
<div class="relative flex h-16 justify-between">
|
||||
<MobileMenuButton />
|
||||
<div class="flex flex-1 items-stretch justify-start">
|
||||
<HomeNavigation />
|
||||
<CourseSessionNavigation />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 items-stretch justify-start">
|
||||
<div class="hidden flex-shrink-0 items-center lg:flex">
|
||||
<div class="flex items-center">
|
||||
<router-link to="/" class="flex">
|
||||
<it-icon-vbv class="-ml-3 -mt-6 mr-3 h-8 w-16" />
|
||||
</router-link>
|
||||
<router-link to="/" class="flex">
|
||||
<div
|
||||
class="ml-1 border-l border-white pl-3 pr-10 text-2xl text-white"
|
||||
>
|
||||
{{ t("general.title") }}
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-stretch justify-start space-x-8">
|
||||
<router-link
|
||||
v-if="hasMediaLibraryMenu"
|
||||
:to="
|
||||
getMediaCenterUrl(courseSessionsStore.currentCourseSession?.course.slug)
|
||||
"
|
||||
data-cy="medialibrary-link"
|
||||
class="nav-item-no-mobile"
|
||||
:class="{ 'nav-item--active': inMediaLibrary() }"
|
||||
>
|
||||
<it-icon-media-library class="h-8 w-8" />
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="hasAppointmentsMenu"
|
||||
:to="appointmentsUrl"
|
||||
data-cy="all-duedates-link"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inAppointments() }"
|
||||
>
|
||||
<it-icon-calendar-light class="h-8 w-8" />
|
||||
</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">
|
||||
<router-link
|
||||
v-if="hasCockpitMenu"
|
||||
data-cy="navigation-cockpit-link"
|
||||
:to="
|
||||
getCockpitUrl(
|
||||
courseSessionsStore.currentCourseSession.course.slug
|
||||
)
|
||||
"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inCockpit() }"
|
||||
>
|
||||
{{ t("cockpit.title") }}
|
||||
</router-link>
|
||||
<!-- Notification Bell & Menu -->
|
||||
<NotificationButton />
|
||||
|
||||
<router-link
|
||||
v-if="hasPreviewMenu"
|
||||
data-cy="navigation-preview-link"
|
||||
:to="
|
||||
getLearningPathUrl(
|
||||
courseSessionsStore.currentCourseSession.course.slug
|
||||
)
|
||||
"
|
||||
target="_blank"
|
||||
class="nav-item"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<span>{{ t("a.Vorschau Teilnehmer") }}</span>
|
||||
<it-icon-external-link class="ml-2" />
|
||||
</div>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="hasLearningPathMenu"
|
||||
data-cy="navigation-learning-path-link"
|
||||
:to="
|
||||
getLearningPathUrl(
|
||||
courseSessionsStore.currentCourseSession.course.slug
|
||||
)
|
||||
"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inLearningPath() }"
|
||||
>
|
||||
{{ t("general.learningPath") }}
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-if="hasCompetenceNaviMenu"
|
||||
data-cy="navigation-competence-profile-link"
|
||||
:to="
|
||||
getCompetenceNaviUrl(
|
||||
courseSessionsStore.currentCourseSession.course.slug
|
||||
)
|
||||
"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inCompetenceProfile() }"
|
||||
>
|
||||
{{ t("competences.title") }}
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-if="hasLearningMentor"
|
||||
data-cy="navigation-learning-mentor-link"
|
||||
:to="
|
||||
getLearningMentorUrl(
|
||||
courseSessionsStore.currentCourseSession.course.slug
|
||||
)
|
||||
"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inLearningMentor() }"
|
||||
>
|
||||
{{ t(mentorTabTitle) }}
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="flex items-stretch justify-start space-x-8">
|
||||
<router-link
|
||||
v-if="hasMediaLibraryMenu"
|
||||
:to="
|
||||
getMediaCenterUrl(
|
||||
courseSessionsStore.currentCourseSession?.course.slug
|
||||
)
|
||||
"
|
||||
data-cy="medialibrary-link"
|
||||
class="nav-item-no-mobile"
|
||||
:class="{ 'nav-item--active': inMediaLibrary() }"
|
||||
>
|
||||
<it-icon-media-library class="h-8 w-8" />
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="hasAppointmentsMenu"
|
||||
:to="appointmentsUrl"
|
||||
data-cy="all-duedates-link"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inAppointments() }"
|
||||
>
|
||||
<it-icon-calendar-light class="h-8 w-8" />
|
||||
</router-link>
|
||||
|
||||
<!-- Notification Bell & Menu -->
|
||||
<div v-if="hasNotificationsMenu" class="nav-item leading-none">
|
||||
<NotificationPopover>
|
||||
<template #toggleButtonContent>
|
||||
<div class="relative h-8 w-8">
|
||||
<div>
|
||||
<it-icon-notification class="h-8 w-8" />
|
||||
<div
|
||||
v-if="notificationsStore.hasUnread"
|
||||
aria-label="unread notifications"
|
||||
class="absolute inset-y-0 right-0 h-1.5 w-1.5 rounded-full bg-sky-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #popoverContent>
|
||||
<NotificationPopoverContent />
|
||||
</template>
|
||||
</NotificationPopover>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="hasSessionTitle"
|
||||
class="nav-item hidden items-center lg:inline-flex"
|
||||
>
|
||||
<div class="" data-cy="current-course-session-title">
|
||||
{{ selectedCourseSessionTitle }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-item">
|
||||
<div
|
||||
v-if="userStore.loggedIn"
|
||||
class="flex items-center"
|
||||
data-cy="header-profile"
|
||||
>
|
||||
<Popover class="relative">
|
||||
<PopoverButton @click="popoverClick($event)">
|
||||
<div v-if="userStore.avatar_url">
|
||||
<img
|
||||
class="inline-block h-8 w-8 rounded-full"
|
||||
:src="userStore.avatar_url"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ userStore.getFullName }}
|
||||
</div>
|
||||
</PopoverButton>
|
||||
|
||||
<PopoverPanel
|
||||
v-slot="{ close }"
|
||||
class="absolute -right-2 top-8 z-50 w-[500px] bg-white shadow-lg"
|
||||
>
|
||||
<div class="p-4">
|
||||
<AccountMenu @close="close" />
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
</div>
|
||||
<div v-else>
|
||||
<a class="" :href="getLoginURL({ lang: userStore.language })">
|
||||
Login
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasSessionTitle"
|
||||
class="nav-item hidden items-center lg:inline-flex"
|
||||
>
|
||||
<div class="" data-cy="current-course-session-title">
|
||||
{{ selectedCourseSessionTitle }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-item">
|
||||
<ProfileMenuButton />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.nav-item {
|
||||
@apply inline-flex items-center border-b-4 border-transparent px-1 pt-1 text-white hover:text-sky-500;
|
||||
}
|
||||
|
||||
.nav-item-no-mobile {
|
||||
@apply hidden items-center border-b-4 border-transparent px-1 pt-1 text-white hover:text-sky-500 lg:inline-flex;
|
||||
}
|
||||
|
||||
.nav-item--active {
|
||||
@apply border-sky-500;
|
||||
}
|
||||
</style>
|
||||
<style lang="postcss"></style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
<script setup lang="ts">
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { useNavigationAttributes } from "@/utils/navigation";
|
||||
import { getMediaCenterUrl } from "@/utils/utils";
|
||||
import { ref } from "vue";
|
||||
import MobileMenu from "./MobileMenu.vue";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
|
||||
const {
|
||||
hasCompetenceNaviMenu,
|
||||
hasLearningPathMenu,
|
||||
hasMediaLibraryMenu,
|
||||
hasCockpitMenu,
|
||||
hasPreviewMenu,
|
||||
hasAppointmentsMenu,
|
||||
hasNotificationsMenu,
|
||||
hasLearningMentor,
|
||||
} = useNavigationAttributes();
|
||||
|
||||
const showMenu = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<MobileMenu
|
||||
v-if="userStore.loggedIn"
|
||||
:show="showMenu"
|
||||
:course-session="courseSessionsStore.currentCourseSession"
|
||||
:has-media-library-menu="hasMediaLibraryMenu"
|
||||
:has-cockpit-menu="hasCockpitMenu"
|
||||
:has-preview-menu="hasPreviewMenu"
|
||||
:has-learning-path-menu="hasLearningPathMenu"
|
||||
:has-competence-navi-menu="hasCompetenceNaviMenu"
|
||||
:has-learning-mentor="hasLearningMentor"
|
||||
:has-notifications-menu="hasNotificationsMenu"
|
||||
:has-appointments-menu="hasAppointmentsMenu"
|
||||
:media-url="
|
||||
getMediaCenterUrl(courseSessionsStore.currentCourseSession?.course?.slug)
|
||||
"
|
||||
:user="userStore"
|
||||
@closemodal="showMenu = false"
|
||||
@logout="userStore.handleLogout()"
|
||||
/>
|
||||
</Teleport>
|
||||
<div class="absolute inset-y-0 left-0 flex items-center lg:hidden">
|
||||
<!-- Mobile menu button -->
|
||||
<div data-cy="navigation-mobile-menu-button" class="flex" @click="showMenu = true">
|
||||
<button
|
||||
type="button"
|
||||
class="h-8 w-8 text-white hover:text-sky-500 focus:text-sky-500 focus:outline-none"
|
||||
>
|
||||
<it-icon-menu class="h-8 w-8" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<script setup lang="ts">
|
||||
import NotificationPopover from "@/components/notifications/NotificationPopover.vue";
|
||||
import NotificationPopoverContent from "@/components/notifications/NotificationPopoverContent.vue";
|
||||
import { useNotificationsStore } from "@/stores/notifications";
|
||||
import { useNavigationAttributes } from "@/utils/navigation";
|
||||
|
||||
const notificationsStore = useNotificationsStore();
|
||||
const { hasNotificationsMenu } = useNavigationAttributes();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="hasNotificationsMenu" class="nav-item leading-none">
|
||||
<NotificationPopover>
|
||||
<template #toggleButtonContent>
|
||||
<div class="relative h-8 w-8">
|
||||
<div>
|
||||
<it-icon-notification class="h-8 w-8" />
|
||||
<div
|
||||
v-if="notificationsStore.hasUnread"
|
||||
aria-label="unread notifications"
|
||||
class="absolute inset-y-0 right-0 h-1.5 w-1.5 rounded-full bg-sky-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #popoverContent>
|
||||
<NotificationPopoverContent />
|
||||
</template>
|
||||
</NotificationPopover>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
<script setup lang="ts">
|
||||
import AccountMenu from "@/components/header/AccountMenu.vue";
|
||||
import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
|
||||
import { getLoginURL } from "@/router/utils";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/vue";
|
||||
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
|
||||
import { ref } from "vue";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind);
|
||||
|
||||
const showMenu = ref(false);
|
||||
|
||||
function popoverClick(event: Event) {
|
||||
if (breakpoints.smaller("lg").value) {
|
||||
event.preventDefault();
|
||||
showMenu.value = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<ItFullScreenModal
|
||||
v-if="userStore.loggedIn"
|
||||
:show="showMenu"
|
||||
@closemodal="showMenu = false"
|
||||
>
|
||||
<AccountMenu @close="showMenu = false" />
|
||||
</ItFullScreenModal>
|
||||
</Teleport>
|
||||
<div v-if="userStore.loggedIn" class="flex items-center" data-cy="header-profile">
|
||||
<Popover class="relative">
|
||||
<PopoverButton @click="popoverClick($event)">
|
||||
<div v-if="userStore.avatar_url">
|
||||
<img
|
||||
class="inline-block h-8 w-8 rounded-full"
|
||||
:src="userStore.avatar_url"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ userStore.getFullName }}
|
||||
</div>
|
||||
</PopoverButton>
|
||||
|
||||
<PopoverPanel
|
||||
v-slot="{ close }"
|
||||
class="absolute -right-2 top-8 z-50 w-[500px] bg-white shadow-lg"
|
||||
>
|
||||
<div class="p-4">
|
||||
<AccountMenu @close="close" />
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
</div>
|
||||
<div v-else>
|
||||
<a class="" :href="getLoginURL({ lang: userStore.language })">Login</a>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -264,6 +264,7 @@
|
|||
"a.Teilnehmer": "Teilnehmer",
|
||||
"a.Teilnehmer im": "Teilnehmer im",
|
||||
"a.Teilnehmer nach Zulassungsprofilen im": "Teilnehmer nach Zulassungsprofilen im",
|
||||
"a.Teilnehmer Vorschau": "Teilnehmer Vorschau",
|
||||
"a.Telefonnummer": "Telefonnummer",
|
||||
"a.Telefonnummer hat das falsche Format": "Telefonnummer hat das falsche Format",
|
||||
"a.Termin": "Termin",
|
||||
|
|
|
|||
|
|
@ -264,6 +264,7 @@
|
|||
"a.Teilnehmer": "Participants",
|
||||
"a.Teilnehmer im": "Participants en",
|
||||
"a.Teilnehmer nach Zulassungsprofilen im": "Participants par profil d'admission en",
|
||||
"a.Teilnehmer Vorschau": "Aperçu des participants",
|
||||
"a.Telefonnummer": "Numéro de téléphone",
|
||||
"a.Telefonnummer hat das falsche Format": "Le numéro de téléphone n'est pas au bon format",
|
||||
"a.Termin": "Date",
|
||||
|
|
|
|||
|
|
@ -264,6 +264,7 @@
|
|||
"a.Teilnehmer": "Partecipanti",
|
||||
"a.Teilnehmer im": "I partecipanti al",
|
||||
"a.Teilnehmer nach Zulassungsprofilen im": "Partecipanti per profilo di ammissione nel",
|
||||
"a.Teilnehmer Vorschau": "Anteprima dei partecipanti",
|
||||
"a.Telefonnummer": "Numero di telefono",
|
||||
"a.Telefonnummer hat das falsche Format": "Il numero di telefono ha un formato sbagliato",
|
||||
"a.Termin": "Data",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { computed } from "vue";
|
||||
import { useRouteLookups } from "./route";
|
||||
|
||||
export function useNavigationAttributes() {
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const userStore = useUserStore();
|
||||
const { inCourse } = useRouteLookups();
|
||||
|
||||
const hasCompetenceNaviMenu = computed(() =>
|
||||
Boolean(
|
||||
courseSessionsStore.currentCourseSession?.actions.includes("competence-navi") &&
|
||||
inCourse()
|
||||
)
|
||||
);
|
||||
const hasLearningPathMenu = computed(() =>
|
||||
Boolean(
|
||||
courseSessionsStore.currentCourseSession?.actions.includes("learning-path") &&
|
||||
inCourse()
|
||||
)
|
||||
);
|
||||
const hasMediaLibraryMenu = computed(() =>
|
||||
Boolean(
|
||||
courseSessionsStore.currentCourseSession?.actions.includes("media-library") &&
|
||||
inCourse()
|
||||
)
|
||||
);
|
||||
const hasCockpitMenu = computed(
|
||||
() =>
|
||||
Boolean(
|
||||
courseSessionsStore.currentCourseSession?.actions.includes("expert-cockpit")
|
||||
) && inCourse()
|
||||
);
|
||||
const hasPreviewMenu = computed(
|
||||
() =>
|
||||
Boolean(courseSessionsStore.currentCourseSession?.actions.includes("preview")) &&
|
||||
inCourse()
|
||||
);
|
||||
|
||||
const hasAppointmentsMenu = computed(() =>
|
||||
Boolean(
|
||||
courseSessionsStore.currentCourseSession?.actions.includes("appointments") &&
|
||||
userStore.loggedIn &&
|
||||
inCourse()
|
||||
)
|
||||
);
|
||||
|
||||
const hasNotificationsMenu = computed(() => {
|
||||
return userStore.loggedIn;
|
||||
});
|
||||
const hasLearningMentor = computed(() => {
|
||||
if (!inCourse()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!courseSessionsStore.currentCourseSession) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const courseSession = courseSessionsStore.currentCourseSession;
|
||||
return courseSession.actions.includes("learning-mentor");
|
||||
});
|
||||
const hasSessionTitle = computed(() => {
|
||||
return courseSessionsStore.currentCourseSession?.title && inCourse();
|
||||
});
|
||||
|
||||
return {
|
||||
hasCompetenceNaviMenu,
|
||||
hasLearningPathMenu,
|
||||
hasMediaLibraryMenu,
|
||||
hasCockpitMenu,
|
||||
hasPreviewMenu,
|
||||
hasAppointmentsMenu,
|
||||
hasNotificationsMenu,
|
||||
hasLearningMentor,
|
||||
hasSessionTitle,
|
||||
};
|
||||
}
|
||||
|
|
@ -175,6 +175,18 @@ textarea {
|
|||
.tag-active {
|
||||
@apply rounded-full bg-blue-900 px-4 py-2 font-semibold text-white;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
@apply inline-flex items-center border-b-4 border-transparent px-1 pt-1 text-white hover:text-sky-500;
|
||||
}
|
||||
|
||||
.nav-item-no-mobile {
|
||||
@apply hidden items-center border-b-4 border-transparent px-1 pt-1 text-white hover:text-sky-500 lg:inline-flex;
|
||||
}
|
||||
|
||||
.nav-item--active {
|
||||
@apply border-sky-500;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 491 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 381 KiB |
|
|
@ -639,7 +639,7 @@ OAUTH_SIGNIN_REDIRECT_URI = env(
|
|||
"OAUTH_SIGNIN_REDIRECT_URI", default="http://localhost:8000/sso/callback"
|
||||
)
|
||||
|
||||
OAUTH_LOGOUT_REDIRECT_URI = env("OAUTH_LOGOUT_REDIRECT_URI", default="")
|
||||
OAUTH_LOGOUT_REDIRECT_URI = env("OAUTH_LOGOUT_REDIRECT_URI", default="/")
|
||||
|
||||
OAUTH_SIGNIN_URL = env("OAUTH_SIGNIN_URL", default="")
|
||||
OAUTH_SIGNIN_REALM = env("OAUTH_SIGNIN_REALM", default="vbv")
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ class GetDashboardConfig(TestCase):
|
|||
role="MentorUK",
|
||||
is_uk=True,
|
||||
is_vv=False,
|
||||
has_preview=True,
|
||||
has_preview=False,
|
||||
widgets=["MentorPersonWidget", "MentorCompetenceWidget"],
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -264,7 +264,6 @@ def has_preview(role_key: RoleKeyType) -> bool:
|
|||
role_key
|
||||
in [
|
||||
RoleKeyType.MENTOR_VV,
|
||||
RoleKeyType.MENTOR_UK,
|
||||
RoleKeyType.BERUFSBILDNER,
|
||||
RoleKeyType.TRAINING_RESPONSIBLE,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -394,7 +394,7 @@ def course_session_permissions(user: User, course_session_id: int) -> list[str]:
|
|||
"expert-cockpit": is_expert,
|
||||
"learning-path": is_member,
|
||||
"competence-navi": is_member,
|
||||
"complete-learning-content": is_expert or is_member,
|
||||
"complete-learning-content": is_expert or is_member or is_berufsbildner,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -39,12 +39,18 @@ class ActionTestCase(TestCase):
|
|||
role=CourseSessionUser.Role.EXPERT,
|
||||
)
|
||||
|
||||
bb = create_user("bb")
|
||||
AgentParticipantRelation.objects.create(
|
||||
agent=bb, participant=csu, role="BERUFSBILDNER"
|
||||
)
|
||||
|
||||
# WHEN
|
||||
mentor_actions = course_session_permissions(lm, self.course_session.id)
|
||||
participant_actions = course_session_permissions(
|
||||
participant, self.course_session.id
|
||||
)
|
||||
trainer_actions = course_session_permissions(trainer, self.course_session.id)
|
||||
bb_actions = course_session_permissions(bb, self.course_session.id)
|
||||
|
||||
# THEN
|
||||
self.assertEqual(
|
||||
|
|
@ -81,3 +87,11 @@ class ActionTestCase(TestCase):
|
|||
"complete-learning-content",
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
bb_actions,
|
||||
[
|
||||
"preview",
|
||||
"media-library",
|
||||
"complete-learning-content",
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -143,7 +143,8 @@ def logout(request):
|
|||
id_token = user_data.get("id_token", "")
|
||||
if not id_token:
|
||||
logger.debug("SSO Logout", extra={"mode": "id_token_not_set"})
|
||||
return redirect(f"{redirect_uri}&client_id=iterativ")
|
||||
url_param_symbol = "&" if "?" in redirect_uri else "?"
|
||||
return redirect(f"{redirect_uri}{url_param_symbol}client_id=iterativ")
|
||||
|
||||
# Handle scenarios when SSO-related data is present or redirect_uri is not set
|
||||
if not redirect_uri:
|
||||
|
|
|
|||
Loading…
Reference in New Issue