Merge branch 'develop' into feature/VBV-234-bugfix-create-new-page-in-wagtail
This commit is contained in:
commit
cb37c55732
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { defineConfig } from "cypress";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
component: {
|
||||||
|
devServer: {
|
||||||
|
framework: "vue",
|
||||||
|
bundler: "vite",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"baseUrl": "http://localhost:5050"
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"body": "Fixtures are a great way to mock data for responses to routes",
|
||||||
|
"email": "hello@cypress.io",
|
||||||
|
"name": "Using fixtures to represent data"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
// ***********************************************
|
||||||
|
// This example commands.ts shows you how to
|
||||||
|
// create various custom commands and overwrite
|
||||||
|
// existing commands.
|
||||||
|
//
|
||||||
|
// For more comprehensive examples of custom
|
||||||
|
// commands please read more here:
|
||||||
|
// https://on.cypress.io/custom-commands
|
||||||
|
// ***********************************************
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a parent command --
|
||||||
|
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a child command --
|
||||||
|
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a dual command --
|
||||||
|
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This will overwrite an existing command --
|
||||||
|
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||||
|
//
|
||||||
|
// declare global {
|
||||||
|
// namespace Cypress {
|
||||||
|
// interface Chainable {
|
||||||
|
// login(email: string, password: string): Chainable<void>
|
||||||
|
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||||
|
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||||
|
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||||
|
<title>Components App</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div data-cy-root></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
// ***********************************************************
|
||||||
|
// This example support/component.ts is processed and
|
||||||
|
// loaded automatically before your test files.
|
||||||
|
//
|
||||||
|
// This is a great place to put global configuration and
|
||||||
|
// behavior that modifies Cypress.
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off
|
||||||
|
// automatically serving support files with the
|
||||||
|
// 'supportFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/configuration
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// Import commands.js using ES2015 syntax:
|
||||||
|
import "../../tailwind.css";
|
||||||
|
import "./commands";
|
||||||
|
|
||||||
|
// Alternatively you can use CommonJS syntax:
|
||||||
|
// require('./commands')
|
||||||
|
|
||||||
|
import { setupI18n } from "@/i18n.ts";
|
||||||
|
import router from "@/router";
|
||||||
|
import { mount } from "cypress/vue";
|
||||||
|
import { createPinia } from "pinia";
|
||||||
|
|
||||||
|
// Augment the Cypress namespace to include type definitions for
|
||||||
|
// your custom command.
|
||||||
|
// Alternatively, can be defined in cypress/support/component.d.ts
|
||||||
|
// with a <reference path="./component" /> at the top of your spec.
|
||||||
|
declare global {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
|
namespace Cypress {
|
||||||
|
interface Chainable {
|
||||||
|
mount: typeof mount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cypress.Commands.add("mount", (component, options = {}) => {
|
||||||
|
options.global = options.global || {};
|
||||||
|
options.global.plugins = options.global.plugins || [];
|
||||||
|
options.global.plugins.push(setupI18n());
|
||||||
|
options.global.plugins.push(createPinia());
|
||||||
|
|
||||||
|
if (!options.router) {
|
||||||
|
options.router = router;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mount(component, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Example use:
|
||||||
|
// cy.mount(MyComponent)
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["es5", "dom"],
|
||||||
|
"target": "es5",
|
||||||
|
"types": ["cypress", "node"]
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts"]
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -15,7 +15,8 @@
|
||||||
"prettier:check": "prettier . --check",
|
"prettier:check": "prettier . --check",
|
||||||
"tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch",
|
"tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch",
|
||||||
"storybook": "storybook dev -p 6006",
|
"storybook": "storybook dev -p 6006",
|
||||||
"build-storybook": "rm -rf ../server/vbv_lernwelt/static/storybook/* && storybook build -o ../server/vbv_lernwelt/static/storybook"
|
"build-storybook": "rm -rf ../server/vbv_lernwelt/static/storybook/* && storybook build -o ../server/vbv_lernwelt/static/storybook",
|
||||||
|
"cypress:open": "cypress open"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/tailwindcss": "^0.1.2",
|
"@headlessui/tailwindcss": "^0.1.2",
|
||||||
|
|
@ -24,6 +25,7 @@
|
||||||
"@sentry/vue": "^7.20.0",
|
"@sentry/vue": "^7.20.0",
|
||||||
"@urql/vue": "^1.0.2",
|
"@urql/vue": "^1.0.2",
|
||||||
"@vueuse/core": "^9.13.0",
|
"@vueuse/core": "^9.13.0",
|
||||||
|
"cypress": "^12.9.0",
|
||||||
"d3": "^7.6.1",
|
"d3": "^7.6.1",
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
"graphql": "^16.6.0",
|
"graphql": "^16.6.0",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<template>
|
||||||
|
<AccountMenuContent
|
||||||
|
:course-sessions="courseSessionsStore.allCurrentCourseSessions"
|
||||||
|
:selected-course-session="courseSessionsStore.currentCourseSession?.id"
|
||||||
|
:user="userStore"
|
||||||
|
@logout="logout"
|
||||||
|
@select-course-session="selectCourseSession"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import AccountMenuContent from "@/components/header/AccountMenuContent.vue";
|
||||||
|
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||||
|
import { useUserStore } from "@/stores/user";
|
||||||
|
import type { CourseSession } from "@/types";
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
userStore.handleLogout();
|
||||||
|
};
|
||||||
|
const selectCourseSession = (courseSession: CourseSession) => {
|
||||||
|
courseSessionsStore.switchCourseSession(courseSession);
|
||||||
|
};
|
||||||
|
|
||||||
|
const courseSessionsStore = useCourseSessionsStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import avatar from "../../../.storybook/uk1.patrizia.huggel.jpg";
|
||||||
|
import AccountMenuContent from "./AccountMenuContent.vue";
|
||||||
|
|
||||||
|
const bern = {
|
||||||
|
id: 1,
|
||||||
|
title: "Bern 2023 a",
|
||||||
|
};
|
||||||
|
let selectedSession = bern;
|
||||||
|
const courseSessions = [
|
||||||
|
bern,
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "Zürich 2023 a",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const user = {
|
||||||
|
first_name: "Vreni",
|
||||||
|
last_name: "Schmid",
|
||||||
|
email: "vreni.schmid@example.com",
|
||||||
|
avatar_url: avatar,
|
||||||
|
loggedIn: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectCourseSession = (session) => {
|
||||||
|
selectedSession = session;
|
||||||
|
console.log("session");
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("<AccountMenuContent />", () => {
|
||||||
|
it("renders", () => {
|
||||||
|
// see: https://on.cypress.io/mounting-vue
|
||||||
|
cy.mount(AccountMenuContent, {
|
||||||
|
props: {
|
||||||
|
user,
|
||||||
|
allCourseSessions: courseSessions,
|
||||||
|
selectedCourseSession: selectedSession,
|
||||||
|
onSelectCourseSession: selectCourseSession,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -21,25 +21,37 @@ const meta: Meta<typeof AccountMenuContent> = {
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof AccountMenuContent>;
|
type Story = StoryObj<typeof AccountMenuContent>;
|
||||||
|
|
||||||
export const DefaultStory: Story = {
|
const courseSessions = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "Bern 2023 a",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "Zürich 2023 a",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const loggedInUser = {
|
||||||
|
first_name: "Vreni",
|
||||||
|
last_name: "Schmid",
|
||||||
|
email: "vreni.schmid@example.com",
|
||||||
|
avatar_url: avatar,
|
||||||
|
loggedIn: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LoggedInUser: Story = {
|
||||||
args: {
|
args: {
|
||||||
show: true,
|
allCourseSessions: courseSessions,
|
||||||
courseSessions: [
|
user: loggedInUser,
|
||||||
{
|
},
|
||||||
id: 1,
|
};
|
||||||
title: "Bern 2023 a",
|
|
||||||
},
|
export const NoUser: Story = {
|
||||||
{
|
args: {
|
||||||
id: 2,
|
allCourseSessions: courseSessions,
|
||||||
title: "Zürich 2023 a",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
user: {
|
user: {
|
||||||
first_name: "Vreni",
|
loggedIn: false,
|
||||||
last_name: "Schmid",
|
|
||||||
email: "vreni.schmid@example.com",
|
|
||||||
avatar_url: avatar,
|
|
||||||
loggedIn: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,31 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import CourseSessionsMenu from "@/components/header/CourseSessionsMenu.vue";
|
||||||
import type { UserState } from "@/stores/user";
|
import type { UserState } from "@/stores/user";
|
||||||
import type { CourseSession } from "@/types";
|
import type { CourseSession } from "@/types";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
courseSessions: CourseSession[];
|
courseSessions: CourseSession[];
|
||||||
user: UserState | undefined;
|
user: UserState;
|
||||||
|
selectedCourseSession?: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emits = defineEmits(["selectCourseSession", "logout"]);
|
const emit = defineEmits(["selectCourseSession", "logout"]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="text-black">
|
<div class="text-black">
|
||||||
<div v-if="user?.loggedIn" class="border-b py-4">
|
<div class="border-b py-4">
|
||||||
<div class="flex justify-start">
|
<div class="flex justify-start">
|
||||||
<div v-if="user?.avatar_url">
|
<div v-if="user.avatar_url">
|
||||||
<img
|
<img
|
||||||
class="inline-block h-20 w-20 rounded-full"
|
class="inline-block h-20 w-20 rounded-full"
|
||||||
:src="user?.avatar_url"
|
:src="user.avatar_url"
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-6">
|
<div class="ml-6">
|
||||||
<h3>{{ user?.first_name }} {{ user?.last_name }}</h3>
|
<h3>{{ user.first_name }} {{ user.last_name }}</h3>
|
||||||
<div class="text-sm text-gray-800">{{ user?.email }}</div>
|
<div class="text-sm text-gray-800">{{ user.email }}</div>
|
||||||
<div class="text-sm text-gray-800">
|
<div class="text-sm text-gray-800">
|
||||||
<router-link class="link" to="/profile">Profil anzeigen</router-link>
|
<router-link class="link" to="/profile">Profil anzeigen</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -32,17 +34,14 @@ const emits = defineEmits(["selectCourseSession", "logout"]);
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="props.courseSessions.length" class="border-b py-4">
|
<div v-if="props.courseSessions.length" class="border-b py-4">
|
||||||
<div v-for="cs in props.courseSessions" :key="cs.id">
|
<CourseSessionsMenu
|
||||||
<button @click="$emit('selectCourseSession', cs)">{{ cs.title }}</button>
|
:items="courseSessions"
|
||||||
</div>
|
:selected="selectedCourseSession"
|
||||||
|
@select="emit('selectCourseSession', $event)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button type="button" class="mt-6 flex items-center" @click="emit('logout')">
|
||||||
v-if="user?.loggedIn"
|
|
||||||
type="button"
|
|
||||||
class="mt-6 flex items-center"
|
|
||||||
@click="$emit('logout')"
|
|
||||||
>
|
|
||||||
<it-icon-logout class="inline-block" />
|
<it-icon-logout class="inline-block" />
|
||||||
<span class="ml-1">{{ $t("mainNavigation.logout") }}</span>
|
<span class="ml-1">{{ $t("mainNavigation.logout") }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||||
|
import type { Item } from "./CourseSessionsMenu.vue";
|
||||||
|
import CourseSessionsMenu from "./CourseSessionsMenu.vue";
|
||||||
|
|
||||||
|
const meta: Meta<typeof CourseSessionsMenu> = {
|
||||||
|
title: "VBV/CourseSessionsMenu",
|
||||||
|
component: CourseSessionsMenu,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof CourseSessionsMenu>;
|
||||||
|
|
||||||
|
const items: Item[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
title: "Bern 2021 a",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
title: "Bern 2021 b",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
title: "Bern 2022 a",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
title: "Bern 2022 b",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const CourseSessionsMenuStory: Story = {
|
||||||
|
args: {
|
||||||
|
items,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<template>
|
||||||
|
<fieldset>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-for="item in props.items"
|
||||||
|
:key="item.id"
|
||||||
|
class="mb-4 flex items-center last:mb-0"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:id="String(item.id)"
|
||||||
|
name="coursesessions"
|
||||||
|
type="radio"
|
||||||
|
:value="item.id"
|
||||||
|
:checked="selected === item.id"
|
||||||
|
class="focus:ring-indigo-900 h-4 w-4 border-gray-300 text-blue-900"
|
||||||
|
@change="emit('select', item)"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
:for="String(item.id)"
|
||||||
|
class="ml-3 block text-sm font-medium leading-6 text-gray-900"
|
||||||
|
>
|
||||||
|
{{ item.title }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { CourseSession } from "@/types";
|
||||||
|
|
||||||
|
export interface Item {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
items: CourseSession[];
|
||||||
|
selected?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits(["select"]);
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import router from "@/router";
|
||||||
|
import { createPinia } from "pinia";
|
||||||
|
import { markRaw } from "vue";
|
||||||
|
import MainNavigationBar from "./MainNavigationBar.vue";
|
||||||
|
|
||||||
|
describe("<MainNavigationBar />", () => {
|
||||||
|
it("renders", async () => {
|
||||||
|
const pinia = createPinia();
|
||||||
|
pinia.use(({ store }) => {
|
||||||
|
store.router = markRaw(router);
|
||||||
|
});
|
||||||
|
router.push("/");
|
||||||
|
await router.isReady();
|
||||||
|
// see: https://on.cypress.io/mounting-vue
|
||||||
|
cy.mount(MainNavigationBar, {
|
||||||
|
router: router,
|
||||||
|
global: {
|
||||||
|
plugins: [pinia],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
|
|
||||||
import AccountMenuContent from "@/components/header/AccountMenuContent.vue";
|
import AccountMenu from "@/components/header/AccountMenu.vue";
|
||||||
import MobileMenu from "@/components/header/MobileMenu.vue";
|
import MobileMenu from "@/components/header/MobileMenu.vue";
|
||||||
import NotificationPopover from "@/components/notifications/NotificationPopover.vue";
|
import NotificationPopover from "@/components/notifications/NotificationPopover.vue";
|
||||||
import NotificationPopoverContent from "@/components/notifications/NotificationPopoverContent.vue";
|
import NotificationPopoverContent from "@/components/notifications/NotificationPopoverContent.vue";
|
||||||
|
|
@ -9,20 +9,19 @@ import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
|
||||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||||
import { useNotificationsStore } from "@/stores/notifications";
|
import { useNotificationsStore } from "@/stores/notifications";
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
import type { CourseSession } from "@/types";
|
import { useRouteLookups } from "@/utils/route";
|
||||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/vue";
|
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/vue";
|
||||||
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
|
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
|
||||||
import { computed, onMounted, reactive } from "vue";
|
import { computed, onMounted, reactive } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useRoute } from "vue-router";
|
|
||||||
|
|
||||||
log.debug("MainNavigationBar created");
|
log.debug("MainNavigationBar created");
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const breakpoints = useBreakpoints(breakpointsTailwind);
|
const breakpoints = useBreakpoints(breakpointsTailwind);
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const courseSessionsStore = useCourseSessionsStore();
|
const courseSessionsStore = useCourseSessionsStore();
|
||||||
const notificationsStore = useNotificationsStore();
|
const notificationsStore = useNotificationsStore();
|
||||||
|
const { inCockpit, inCompetenceProfile, inCourse, inLearningPath } = useRouteLookups();
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
|
|
@ -30,37 +29,6 @@ const state = reactive({
|
||||||
showMobileProfileMenu: false,
|
showMobileProfileMenu: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
function inCourse() {
|
|
||||||
return route.path.startsWith("/course/");
|
|
||||||
}
|
|
||||||
|
|
||||||
function inCockpit() {
|
|
||||||
const regex = new RegExp("/course/[^/]+/cockpit");
|
|
||||||
return regex.test(route.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
function inLearningPath() {
|
|
||||||
const regex = new RegExp("/course/[^/]+/learn");
|
|
||||||
return regex.test(route.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
function inCompetenceProfile() {
|
|
||||||
const regex = new RegExp("/course/[^/]+/competence");
|
|
||||||
return regex.test(route.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
function inMediaLibrary() {
|
|
||||||
return route.path.startsWith("/media/");
|
|
||||||
}
|
|
||||||
|
|
||||||
function logout() {
|
|
||||||
userStore.handleLogout();
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectCourseSession(courseSession: CourseSession) {
|
|
||||||
courseSessionsStore.switchCourseSession(courseSession);
|
|
||||||
}
|
|
||||||
|
|
||||||
function popoverClick(event: Event) {
|
function popoverClick(event: Event) {
|
||||||
if (breakpoints.smaller("lg").value) {
|
if (breakpoints.smaller("lg").value) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -96,11 +64,7 @@ onMounted(() => {
|
||||||
:show="state.showMobileProfileMenu"
|
:show="state.showMobileProfileMenu"
|
||||||
@closemodal="state.showMobileProfileMenu = false"
|
@closemodal="state.showMobileProfileMenu = false"
|
||||||
>
|
>
|
||||||
<AccountMenuContent
|
<AccountMenu />
|
||||||
:course-sessions="courseSessionsStore.currentCourseSessions"
|
|
||||||
:user="userStore"
|
|
||||||
@logout="logout"
|
|
||||||
/>
|
|
||||||
</ItFullScreenModal>
|
</ItFullScreenModal>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
<Transition name="nav">
|
<Transition name="nav">
|
||||||
|
|
@ -129,13 +93,14 @@ onMounted(() => {
|
||||||
<div
|
<div
|
||||||
class="ml-1 border-l border-white pl-3 pr-10 text-2xl text-white"
|
class="ml-1 border-l border-white pl-3 pr-10 text-2xl text-white"
|
||||||
>
|
>
|
||||||
{{ $t("general.title") }}
|
{{ t("general.title") }}
|
||||||
</div>
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hidden space-x-8 lg:flex">
|
<div class="hidden space-x-8 lg:flex">
|
||||||
|
<!-- Navigation Links Desktop -->
|
||||||
<router-link
|
<router-link
|
||||||
v-if="
|
v-if="
|
||||||
inCourse() &&
|
inCourse() &&
|
||||||
|
|
@ -146,7 +111,7 @@ onMounted(() => {
|
||||||
class="nav-item"
|
class="nav-item"
|
||||||
:class="{ 'nav-item--active': inCockpit() }"
|
:class="{ 'nav-item--active': inCockpit() }"
|
||||||
>
|
>
|
||||||
{{ $t("cockpit.title") }}
|
{{ t("cockpit.title") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<router-link
|
<router-link
|
||||||
|
|
@ -155,7 +120,7 @@ onMounted(() => {
|
||||||
class="nav-item"
|
class="nav-item"
|
||||||
:class="{ 'nav-item--active': inLearningPath() }"
|
:class="{ 'nav-item--active': inLearningPath() }"
|
||||||
>
|
>
|
||||||
{{ $t("general.learningPath") }}
|
{{ t("general.learningPath") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<router-link
|
<router-link
|
||||||
|
|
@ -164,12 +129,13 @@ onMounted(() => {
|
||||||
class="nav-item"
|
class="nav-item"
|
||||||
:class="{ 'nav-item--active': inCompetenceProfile() }"
|
:class="{ 'nav-item--active': inCompetenceProfile() }"
|
||||||
>
|
>
|
||||||
{{ $t("competences.title") }}
|
{{ t("competences.title") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-stretch justify-start space-x-8">
|
<div class="flex items-stretch justify-start space-x-8">
|
||||||
|
<!-- Notification Bell & Menu -->
|
||||||
<div v-if="userStore.loggedIn" class="nav-item">
|
<div v-if="userStore.loggedIn" class="nav-item">
|
||||||
<NotificationPopover>
|
<NotificationPopover>
|
||||||
<template #toggleButtonContent>
|
<template #toggleButtonContent>
|
||||||
|
|
@ -217,12 +183,7 @@ onMounted(() => {
|
||||||
class="absolute -right-2 top-8 z-50 w-[500px] bg-white shadow-lg"
|
class="absolute -right-2 top-8 z-50 w-[500px] bg-white shadow-lg"
|
||||||
>
|
>
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<AccountMenuContent
|
<AccountMenu />
|
||||||
:course-sessions="courseSessionsStore.currentCourseSessions"
|
|
||||||
:user="userStore"
|
|
||||||
@logout="logout"
|
|
||||||
@select-course-session="selectCourseSession"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</PopoverPanel>
|
</PopoverPanel>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import type { CheckboxItem } from "@/components/ui/checkbox.types";
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
checkbox_item: CheckboxItem<any>;
|
checkboxItem: CheckboxItem<any>;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@ const input = (e: Event) => {
|
||||||
<label
|
<label
|
||||||
class="cy-checkbox cy-checkbox-checked block flex h-8 items-center bg-contain bg-no-repeat pl-8 disabled:opacity-50"
|
class="cy-checkbox cy-checkbox-checked block flex h-8 items-center bg-contain bg-no-repeat pl-8 disabled:opacity-50"
|
||||||
:class="
|
:class="
|
||||||
checkbox_item.checked
|
checkboxItem.checked
|
||||||
? 'bg-[url(/static/icons/icon-checkbox-checked.svg)] hover:bg-[url(/static/icons/icon-checkbox-checked-hover.svg)]'
|
? 'bg-[url(/static/icons/icon-checkbox-checked.svg)] hover:bg-[url(/static/icons/icon-checkbox-checked-hover.svg)]'
|
||||||
: 'bg-[url(/static/icons/icon-checkbox-unchecked.svg)] hover:bg-[url(/static/icons/icon-checkbox-unchecked-hover.svg)]'
|
: 'bg-[url(/static/icons/icon-checkbox-unchecked.svg)] hover:bg-[url(/static/icons/icon-checkbox-unchecked-hover.svg)]'
|
||||||
"
|
"
|
||||||
|
|
@ -45,21 +45,21 @@ const input = (e: Event) => {
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
ref="checkbox"
|
ref="checkbox"
|
||||||
:checked="checkbox_item.checked"
|
:checked="checkboxItem.checked"
|
||||||
:value="checkbox_item.value"
|
:value="checkboxItem.value"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:data-cy="`it-checkbox-${checkbox_item.value}`"
|
:data-cy="`it-checkbox-${checkboxItem.value}`"
|
||||||
class="sr-only"
|
class="sr-only"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@keydown="keydown"
|
@keydown="keydown"
|
||||||
@input="input"
|
@input="input"
|
||||||
/>
|
/>
|
||||||
<div class="ml-4 flex-col">
|
<div class="ml-4 flex-col">
|
||||||
<div v-if="checkbox_item.label">
|
<div v-if="checkboxItem.label">
|
||||||
{{ checkbox_item.label }}
|
{{ checkboxItem.label }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="checkbox_item.subtitle" class="text-gray-900">
|
<div v-if="checkboxItem.subtitle" class="text-gray-900">
|
||||||
{{ checkbox_item.subtitle }}
|
{{ checkboxItem.subtitle }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ function updateItems(itemValue: string) {
|
||||||
<ItCheckbox
|
<ItCheckbox
|
||||||
v-for="item in items"
|
v-for="item in items"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
:checkbox_item="item"
|
:checkbox-item="item"
|
||||||
:class="item.subtitle ? 'mb-6' : 'mb-4'"
|
:class="item.subtitle ? 'mb-6' : 'mb-4'"
|
||||||
@toggle="updateItems(item.value)"
|
@toggle="updateItems(item.value)"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import type { RadioItem } from "@/pages/learningPath/learningContentPage/feedback/feedback.types";
|
||||||
|
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||||
|
import ItRadioGroup from "./ItRadioGroup.vue";
|
||||||
|
|
||||||
|
const meta: Meta<typeof ItRadioGroup> = {
|
||||||
|
title: "VBV/RadioGroup",
|
||||||
|
component: ItRadioGroup,
|
||||||
|
argTypes: { "onUpdate:modelValue": { action: "updated" } },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof ItRadioGroup>;
|
||||||
|
let modelValue = "a";
|
||||||
|
|
||||||
|
const items: RadioItem<string>[] = [
|
||||||
|
{
|
||||||
|
value: "a",
|
||||||
|
name: "A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "b",
|
||||||
|
name: "B",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const RadioGroup: Story = {
|
||||||
|
args: {
|
||||||
|
modelValue: modelValue,
|
||||||
|
items: items,
|
||||||
|
label: "Radiogroup",
|
||||||
|
"onUpdate:modelValue": (newValue: any) => {
|
||||||
|
modelValue = newValue;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1,13 @@
|
||||||
export interface CheckboxItem<T> {
|
export interface BaseItem<T> {
|
||||||
value: T;
|
value: T;
|
||||||
label?: string;
|
label?: string;
|
||||||
checked: boolean;
|
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CheckboxItem<T> extends BaseItem<T> {
|
||||||
|
checked: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RadioItem<T> extends BaseItem<T> {
|
||||||
|
selected: boolean;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -418,7 +418,7 @@ function log(data: any) {
|
||||||
<h2 class="mb-8 mt-8">Checkbox</h2>
|
<h2 class="mb-8 mt-8">Checkbox</h2>
|
||||||
|
|
||||||
<ItCheckbox
|
<ItCheckbox
|
||||||
:checkbox_item="{
|
:checkbox-item="{
|
||||||
subtitle: 'Subtitle',
|
subtitle: 'Subtitle',
|
||||||
label: 'Label',
|
label: 'Label',
|
||||||
value: 'value',
|
value: 'value',
|
||||||
|
|
@ -433,7 +433,7 @@ function log(data: any) {
|
||||||
|
|
||||||
<ItCheckbox
|
<ItCheckbox
|
||||||
:disabled="true"
|
:disabled="true"
|
||||||
:checkbox_item="{
|
:checkbox-item="{
|
||||||
subtitle: 'checked disabled',
|
subtitle: 'checked disabled',
|
||||||
label: 'Label',
|
label: 'Label',
|
||||||
value: 'value',
|
value: 'value',
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,7 @@ function userCountStatusForCircle(userId: number, translationKey: string) {
|
||||||
);
|
);
|
||||||
const grouped = groupBy(criteria, "circle.translation_key");
|
const grouped = groupBy(criteria, "circle.translation_key");
|
||||||
|
|
||||||
// @ts-ignore
|
return competenceStore.calcStatusCount(grouped[translationKey] as []);
|
||||||
return competenceStore.calcStatusCount(grouped[translationKey]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const circles = computed(() => {
|
const circles = computed(() => {
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@ const learningSequenceBorderClass = computed(() => {
|
||||||
</div>
|
</div>
|
||||||
<ItCheckbox
|
<ItCheckbox
|
||||||
v-else
|
v-else
|
||||||
:checkbox_item="{
|
:checkbox-item="{
|
||||||
value: learningContent.completion_status,
|
value: learningContent.completion_status,
|
||||||
checked: learningContent.completion_status === 'success',
|
checked: learningContent.completion_status === 'success',
|
||||||
}"
|
}"
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ const attendanceDay = computed(() => {
|
||||||
<div class="lg:mt-8">
|
<div class="lg:mt-8">
|
||||||
<div class="text-large my-4">
|
<div class="text-large my-4">
|
||||||
<div v-if="attendanceDay">{{ attendanceDay }}</div>
|
<div v-if="attendanceDay">{{ attendanceDay }}</div>
|
||||||
<div v-else>Keine Präsenztagdaten erfasst für diese Durchführung</div>
|
<div v-else>Für diese Durchführung existieren noch keine Details</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -171,9 +171,9 @@ router.beforeEach(redirectToLoginIfRequired);
|
||||||
router.beforeEach((to) => {
|
router.beforeEach((to) => {
|
||||||
const courseSessionStore = useCourseSessionsStore();
|
const courseSessionStore = useCourseSessionsStore();
|
||||||
if (to.params.courseSlug) {
|
if (to.params.courseSlug) {
|
||||||
courseSessionStore.currentCourseSlug = to.params.courseSlug as string;
|
courseSessionStore._currentCourseSlug = to.params.courseSlug as string;
|
||||||
} else {
|
} else {
|
||||||
courseSessionStore.currentCourseSlug = "";
|
courseSessionStore._currentCourseSlug = "";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ export class LearningPath implements WagtailLearningPath {
|
||||||
|
|
||||||
public async reloadCompletionData() {
|
public async reloadCompletionData() {
|
||||||
const learningPathStore = useLearningPathStore();
|
const learningPathStore = useLearningPathStore();
|
||||||
const completionData = await learningPathStore.loadCompletionData(
|
const completionData = await learningPathStore.loadCourseSessionCompletionData(
|
||||||
this.course.slug,
|
this.course.slug,
|
||||||
this.userId
|
this.userId
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,8 @@ describe("CourseSession Store", () => {
|
||||||
userStore.$patch(user);
|
userStore.$patch(user);
|
||||||
|
|
||||||
const courseSessionsStore = useCourseSessionsStore();
|
const courseSessionsStore = useCourseSessionsStore();
|
||||||
courseSessionsStore.currentCourseSlug = "test-course";
|
courseSessionsStore._currentCourseSlug = "test-course";
|
||||||
courseSessionsStore.courseSessions = courseSessions;
|
courseSessionsStore.allCourseSessions = courseSessions;
|
||||||
|
|
||||||
expect(courseSessionsStore.hasCockpit).toBeFalsy();
|
expect(courseSessionsStore.hasCockpit).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
@ -74,8 +74,8 @@ describe("CourseSession Store", () => {
|
||||||
userStore.$patch(Object.assign(user, { is_superuser: true }));
|
userStore.$patch(Object.assign(user, { is_superuser: true }));
|
||||||
|
|
||||||
const courseSessionsStore = useCourseSessionsStore();
|
const courseSessionsStore = useCourseSessionsStore();
|
||||||
courseSessionsStore.currentCourseSlug = "test-course";
|
courseSessionsStore._currentCourseSlug = "test-course";
|
||||||
courseSessionsStore.courseSessions = courseSessions;
|
courseSessionsStore.allCourseSessions = courseSessions;
|
||||||
|
|
||||||
expect(courseSessionsStore.hasCockpit).toBeTruthy();
|
expect(courseSessionsStore.hasCockpit).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
@ -87,8 +87,8 @@ describe("CourseSession Store", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const courseSessionsStore = useCourseSessionsStore();
|
const courseSessionsStore = useCourseSessionsStore();
|
||||||
courseSessionsStore.currentCourseSlug = "test-course";
|
courseSessionsStore._currentCourseSlug = "test-course";
|
||||||
courseSessionsStore.courseSessions = courseSessions;
|
courseSessionsStore.allCourseSessions = courseSessions;
|
||||||
|
|
||||||
expect(courseSessionsStore.hasCockpit).toBeTruthy();
|
expect(courseSessionsStore.hasCockpit).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@ export const useCompetenceStore = defineStore({
|
||||||
const courseSessionsStore = useCourseSessionsStore();
|
const courseSessionsStore = useCourseSessionsStore();
|
||||||
const courseSession = courseSessionsStore.currentCourseSession;
|
const courseSession = courseSessionsStore.currentCourseSession;
|
||||||
if (courseSession) {
|
if (courseSession) {
|
||||||
const completionData = await completionStore.loadCompletionData(
|
const completionData = await completionStore.loadCourseSessionCompletionData(
|
||||||
courseSession.id,
|
courseSession.id,
|
||||||
userId
|
userId
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,11 @@ export const useCompletionStore = defineStore({
|
||||||
},
|
},
|
||||||
getters: {},
|
getters: {},
|
||||||
actions: {
|
actions: {
|
||||||
async loadCompletionData(courseSessionId: number, userId: number, reload = false) {
|
async loadCourseSessionCompletionData(
|
||||||
|
courseSessionId: number,
|
||||||
|
userId: number,
|
||||||
|
reload = false
|
||||||
|
) {
|
||||||
const userCompletionData = (await itGetCached(
|
const userCompletionData = (await itGetCached(
|
||||||
`/api/course/completion/${courseSessionId}/${userId}/`,
|
`/api/course/completion/${courseSessionId}/${userId}/`,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ function loadCourseSessionsData(reload = false) {
|
||||||
loadAndUpdate(); // this will be called asynchronously, but does not block
|
loadAndUpdate(); // this will be called asynchronously, but does not block
|
||||||
|
|
||||||
// returns the empty sessions array at first, then after loading populates the ref
|
// returns the empty sessions array at first, then after loading populates the ref
|
||||||
return { courseSessions };
|
return { allCourseSessions: courseSessions };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
||||||
|
|
@ -68,26 +68,33 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
||||||
|
|
||||||
// store should do own setup, we don't want to have each component initialize it
|
// store should do own setup, we don't want to have each component initialize it
|
||||||
// that's why we call the load function in here
|
// that's why we call the load function in here
|
||||||
const { courseSessions } = loadCourseSessionsData();
|
const { allCourseSessions } = loadCourseSessionsData();
|
||||||
const selectedCourseSessionMap = useLocalStorage(
|
const selectedCourseSessionMap = useLocalStorage(
|
||||||
SELECTED_COURSE_SESSIONS_KEY,
|
SELECTED_COURSE_SESSIONS_KEY,
|
||||||
new Map<string, number>()
|
new Map<string, number>()
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentCourseSlug = ref("");
|
const _currentCourseSlug = ref("");
|
||||||
|
|
||||||
// these will become getters
|
|
||||||
const uniqueCourseSessionsByCourse = computed(() =>
|
const uniqueCourseSessionsByCourse = computed(() =>
|
||||||
// TODO: refactor after implementing of Klassenkonzept
|
// Im Dashboard wird aktuell ein Widget pro Kurs dargestellt
|
||||||
|
// mit dem Fortschritt jeweils einer Durchführung.
|
||||||
|
// Dieser Getter wird nur dort benutzt.
|
||||||
|
// TODO: Das Dashboard verschwindet evtl. in Zukunft, dann kann dieser Getter weg.
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
uniqBy(courseSessions.value, "course.id")
|
uniqBy(allCourseSessions.value, "course.id")
|
||||||
);
|
);
|
||||||
|
|
||||||
function selectedCourseSessionForCourse(courseSlug: string) {
|
function selectedCourseSessionForCourse(courseSlug: string) {
|
||||||
|
// Wir wollen pro Kurs wissen, welche Durchführung der User zuletzt ausgewählt hat.
|
||||||
|
// Die letzte Durchführung wird im localStorage via `selectedCoruseSessionMap`
|
||||||
|
// gespeichert und hier geladen.
|
||||||
|
// Wenn noch keine Durchführung ausgewählt wurde, wird die erste Durchführung
|
||||||
|
// in `courseSessionForCourse` zurückgegeben.
|
||||||
try {
|
try {
|
||||||
const courseSessionId = selectedCourseSessionMap.value.get(courseSlug);
|
const courseSessionId = selectedCourseSessionMap.value.get(courseSlug);
|
||||||
if (courseSessionId) {
|
if (courseSessionId) {
|
||||||
return courseSessions.value.find((cs) => {
|
return allCourseSessions.value.find((cs) => {
|
||||||
return cs.id === courseSessionId;
|
return cs.id === courseSessionId;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -95,6 +102,7 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
||||||
log.error("Error while parsing courseSessions from localStorage", e);
|
log.error("Error while parsing courseSessions from localStorage", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keine Durchführung ausgewählt im localStorage
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,7 +120,7 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
||||||
return courseSession;
|
return courseSession;
|
||||||
} else {
|
} else {
|
||||||
// return first if there is no selected courseSession
|
// return first if there is no selected courseSession
|
||||||
return courseSessions.value.find((cs) => {
|
return allCourseSessions.value.find((cs) => {
|
||||||
return cs.course.slug === courseSlug;
|
return cs.course.slug === courseSlug;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -121,18 +129,18 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function courseSessionsForCourse(courseSlug: string) {
|
function allCourseSessionsForCourse(courseSlug: string) {
|
||||||
return courseSessions.value.filter((cs) => {
|
return allCourseSessions.value.filter((cs) => {
|
||||||
return cs.course.slug === courseSlug;
|
return cs.course.slug === courseSlug;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentCourseSession = computed(() => {
|
const currentCourseSession = computed(() => {
|
||||||
return courseSessionForCourse(currentCourseSlug.value);
|
return courseSessionForCourse(_currentCourseSlug.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentCourseSessions = computed(() => {
|
const allCurrentCourseSessions = computed(() => {
|
||||||
return courseSessionsForCourse(currentCourseSlug.value);
|
return allCourseSessionsForCourse(_currentCourseSlug.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasCockpit = computed(() => {
|
const hasCockpit = computed(() => {
|
||||||
|
|
@ -197,7 +205,7 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
||||||
|
|
||||||
async function startUpload() {
|
async function startUpload() {
|
||||||
log.debug("loadCourseSessionsData called");
|
log.debug("loadCourseSessionsData called");
|
||||||
courseSessions.value = await itPost(`/api/core/file/start`, {
|
allCourseSessions.value = await itPost(`/api/core/file/start`, {
|
||||||
file_type: "image/png",
|
file_type: "image/png",
|
||||||
file_name: "test.png",
|
file_name: "test.png",
|
||||||
});
|
});
|
||||||
|
|
@ -238,7 +246,7 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
||||||
return {
|
return {
|
||||||
uniqueCourseSessionsByCourse,
|
uniqueCourseSessionsByCourse,
|
||||||
currentCourseSession,
|
currentCourseSession,
|
||||||
currentCourseSessions,
|
allCurrentCourseSessions,
|
||||||
courseSessionForCourse,
|
courseSessionForCourse,
|
||||||
switchCourseSession,
|
switchCourseSession,
|
||||||
hasCockpit,
|
hasCockpit,
|
||||||
|
|
@ -251,10 +259,10 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
||||||
findAttendanceDay,
|
findAttendanceDay,
|
||||||
findAssignmentDetails,
|
findAssignmentDetails,
|
||||||
|
|
||||||
// TODO: only used to be changed by router.afterEach
|
// only used so that `router.afterEach` can switch it
|
||||||
currentCourseSlug,
|
_currentCourseSlug,
|
||||||
|
|
||||||
// TODO: only used for unit testing
|
// only used for unit testing
|
||||||
courseSessions,
|
allCourseSessions,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,11 @@ export const useLearningPathStore = defineStore("learningPath", () => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadCompletionData(
|
async function loadCourseSessionCompletionData(
|
||||||
courseSlug: string,
|
courseSlug: string,
|
||||||
userId: number | undefined = undefined
|
userId: number | undefined = undefined
|
||||||
) {
|
) {
|
||||||
|
// FIXME: should not be here anymore with VBV-305
|
||||||
const completionStore = useCompletionStore();
|
const completionStore = useCompletionStore();
|
||||||
|
|
||||||
let completionData: CourseCompletion[] = [];
|
let completionData: CourseCompletion[] = [];
|
||||||
|
|
@ -52,7 +53,7 @@ export const useLearningPathStore = defineStore("learningPath", () => {
|
||||||
const courseSessionsStore = useCourseSessionsStore();
|
const courseSessionsStore = useCourseSessionsStore();
|
||||||
const courseSession = courseSessionsStore.courseSessionForCourse(courseSlug);
|
const courseSession = courseSessionsStore.courseSessionForCourse(courseSlug);
|
||||||
if (courseSession) {
|
if (courseSession) {
|
||||||
completionData = await completionStore.loadCompletionData(
|
completionData = await completionStore.loadCourseSessionCompletionData(
|
||||||
courseSession.id,
|
courseSession.id,
|
||||||
userId
|
userId
|
||||||
);
|
);
|
||||||
|
|
@ -85,7 +86,7 @@ export const useLearningPathStore = defineStore("learningPath", () => {
|
||||||
throw `No learning path found with: ${slug}`;
|
throw `No learning path found with: ${slug}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const completionData = await loadCompletionData(
|
const completionData = await loadCourseSessionCompletionData(
|
||||||
learningPathData.course.slug,
|
learningPathData.course.slug,
|
||||||
userId
|
userId
|
||||||
);
|
);
|
||||||
|
|
@ -99,19 +100,22 @@ export const useLearningPathStore = defineStore("learningPath", () => {
|
||||||
return learningPath;
|
return learningPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
function reloadCompletionData() {
|
eventBus.on("switchedCourseSession", (courseSession) => {
|
||||||
|
log.debug("handle switchedCourseSession", courseSession);
|
||||||
|
// FIXME: clean up with VBV-305
|
||||||
|
// Die Completion Daten werden nur für die aktuelle Durchführung geladen.
|
||||||
|
// Deshalb müssen die hier neu geladen werden...
|
||||||
state.learningPaths.forEach((lp) => {
|
state.learningPaths.forEach((lp) => {
|
||||||
if (lp.userId) {
|
if (lp.userId) {
|
||||||
lp.reloadCompletionData();
|
lp.reloadCompletionData();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
eventBus.on("switchedCourseSession", (courseSession) => {
|
|
||||||
log.debug("handle switchedCourseSession", courseSession);
|
|
||||||
// FIXME: clean up with VBV-305
|
|
||||||
reloadCompletionData();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return { state, learningPathForUser, loadCompletionData, loadLearningPath };
|
return {
|
||||||
|
state,
|
||||||
|
learningPathForUser,
|
||||||
|
loadCourseSessionCompletionData,
|
||||||
|
loadLearningPath,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
|
||||||
|
export function useRouteLookups() {
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
function inCourse() {
|
||||||
|
return route.path.startsWith("/course/");
|
||||||
|
}
|
||||||
|
|
||||||
|
function inCockpit() {
|
||||||
|
const regex = new RegExp("/course/[^/]+/cockpit");
|
||||||
|
return regex.test(route.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
function inLearningPath() {
|
||||||
|
const regex = new RegExp("/course/[^/]+/learn");
|
||||||
|
return regex.test(route.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
function inCompetenceProfile() {
|
||||||
|
const regex = new RegExp("/course/[^/]+/competence");
|
||||||
|
return regex.test(route.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
function inMediaLibrary() {
|
||||||
|
return route.path.startsWith("/media/");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { inMediaLibrary, inCockpit, inLearningPath, inCompetenceProfile, inCourse };
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,10 @@ module.exports = {
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ["Buenos Aires", "sans-serif"],
|
sans: [
|
||||||
|
"Buenos Aires, sans-serif",
|
||||||
|
{ fontFeatureSettings: '"salt"', fontVariationSettings: '"normal"' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
backgroundSize: {
|
backgroundSize: {
|
||||||
auto: "auto",
|
auto: "auto",
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
},
|
},
|
||||||
"strict": true
|
"strict": true
|
||||||
},
|
},
|
||||||
"exclude": ["src/**/__tests__/*"],
|
"exclude": ["src/**/__tests__/*", "src/**/*.cy.ts"],
|
||||||
"extends": "@vue/tsconfig/tsconfig.web.json",
|
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"]
|
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["es5", "dom"],
|
||||||
|
"target": "es5",
|
||||||
|
"types": ["cypress", "node"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.cy.ts"]
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,9 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "./tsconfig.vitest.json"
|
"path": "./tsconfig.vitest.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.cypress.json"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,49 +5,38 @@ describe("Competence", () => {
|
||||||
cy.manageCommand("cypress_reset");
|
cy.manageCommand("cypress_reset");
|
||||||
|
|
||||||
login("admin", "test");
|
login("admin", "test");
|
||||||
cy.visit("/course/versicherungsvermittler-in/learn/fahrzeug");
|
|
||||||
|
// test-lehrgang-lp-circle-analyse-lu-fahrzeug ist eine Selbstevaluation
|
||||||
|
// mit mehreren Schritten
|
||||||
|
cy.visit("/course/test-lehrgang/learn/analyse");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("self evaluation should be neutral", () => {
|
it("self evaluation should be neutral", () => {
|
||||||
cy.get(
|
cy.get('[data-cy="test-lehrgang-lp-circle-analyse-lu-fahrzeug"]')
|
||||||
'[data-cy="versicherungsvermittler-in-lp-circle-fahrzeug-lu-fahrzeug-1"]'
|
|
||||||
)
|
|
||||||
.find('[data-cy="unknown"]')
|
.find('[data-cy="unknown"]')
|
||||||
.should("exist");
|
.should("exist");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be able to make a happy self evaluation", () => {
|
it("should be able to make a happy self evaluation", () => {
|
||||||
cy.get(
|
cy.get('[data-cy="test-lehrgang-lp-circle-analyse-lu-fahrzeug"]').click();
|
||||||
'[data-cy="versicherungsvermittler-in-lp-circle-fahrzeug-lu-fahrzeug-1"]'
|
cy.makeSelfEvaluation([true, true]);
|
||||||
).click();
|
cy.get('[data-cy="test-lehrgang-lp-circle-analyse-lu-fahrzeug"]')
|
||||||
cy.makeSelfEvaluation([true]);
|
|
||||||
cy.get(
|
|
||||||
'[data-cy="versicherungsvermittler-in-lp-circle-fahrzeug-lu-fahrzeug-1"]'
|
|
||||||
)
|
|
||||||
.find('[data-cy="success"]')
|
.find('[data-cy="success"]')
|
||||||
.should("exist");
|
.should("exist");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be able to make a fail self evaluation", () => {
|
it("should be able to make a fail self evaluation", () => {
|
||||||
cy.get(
|
cy.get('[data-cy="test-lehrgang-lp-circle-analyse-lu-fahrzeug"]').click();
|
||||||
'[data-cy="versicherungsvermittler-in-lp-circle-fahrzeug-lu-fahrzeug-1"]'
|
cy.makeSelfEvaluation([false, false]);
|
||||||
).click();
|
cy.get('[data-cy="test-lehrgang-lp-circle-analyse-lu-fahrzeug"]')
|
||||||
cy.makeSelfEvaluation([false]);
|
|
||||||
cy.get(
|
|
||||||
'[data-cy="versicherungsvermittler-in-lp-circle-fahrzeug-lu-fahrzeug-1"]'
|
|
||||||
)
|
|
||||||
.find('[data-cy="fail"]')
|
.find('[data-cy="fail"]')
|
||||||
.should("exist");
|
.should("exist");
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skip("should be able to make a mixed self evaluation", () => {
|
it("should be able to make a mixed self evaluation", () => {
|
||||||
cy.get(
|
cy.get('[data-cy="test-lehrgang-lp-circle-analyse-lu-fahrzeug"]').click();
|
||||||
'[data-cy="versicherungsvermittler-in-alt-lp-circle-analyse-lu-fahrzeug"]'
|
cy.makeSelfEvaluation([false, true]);
|
||||||
).click();
|
cy.get('[data-cy="test-lehrgang-lp-circle-analyse-lu-fahrzeug"]')
|
||||||
cy.makeSelfEvaluation([false, true, true]);
|
|
||||||
cy.get(
|
|
||||||
'[data-cy="versicherungsvermittler-in-alt-lp-circle-analyse-lu-fahrzeug"]'
|
|
||||||
)
|
|
||||||
.find('[data-cy="fail"]')
|
.find('[data-cy="fail"]')
|
||||||
.should("exist");
|
.should("exist");
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@ describe("MediaLibrary", () => {
|
||||||
login("admin", "test");
|
login("admin", "test");
|
||||||
});
|
});
|
||||||
|
|
||||||
// skipped as long the MainNavigation is not finished
|
|
||||||
it.skip("should be accessible via link in header", () => {
|
it.skip("should be accessible via link in header", () => {
|
||||||
|
// der Link zur Mediathek fehlt aktuell im Header (auch im Design)
|
||||||
|
// das ganze Konzept der Mediathek wird noch überdacht -> skip
|
||||||
cy.visit("/course/test-lehrgang");
|
cy.visit("/course/test-lehrgang");
|
||||||
cy.get('[data-cy="medialibrary-link"]').click();
|
cy.get('[data-cy="medialibrary-link"]').click();
|
||||||
cy.get('[data-cy="Handlungsfelder-link"]').click();
|
cy.get('[data-cy="Handlungsfelder-link"]').click();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue