Merged in feature/VBV-304-inhaltstyp-präsenztag (pull request #52)
VBV-304 inhaltstyp präsenztag Approved-by: Ramon Wenger
This commit is contained in:
commit
1fb0415d58
|
|
@ -8,7 +8,7 @@ end_of_line = lf
|
|||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{py,rst,ini,sh}]
|
||||
[*.{py,rst,ini,sh,svg}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ cd client && npm run dev
|
|||
|
||||
# reset db and run django dev server
|
||||
./prepare_server.sh
|
||||
|
||||
# if you only want to create some specific courses to speed up the script,
|
||||
# you can use the '--courses' parameter
|
||||
# see consts.py for available course ids
|
||||
# ./prepare_server.sh --courses -3 -5
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ e2e: &e2e
|
|||
- export IT_SERVE_VUE=false
|
||||
- export IT_ALLOW_LOCAL_LOGIN=true
|
||||
- source ./env/bitbucket/prepare_for_test.sh
|
||||
- pip install -r server/requirements/requirements-dev.txt
|
||||
- npm install
|
||||
- npm run build
|
||||
- ./prepare_server_cypress.sh --start-background
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
"tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch",
|
||||
"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": {
|
||||
"@headlessui/tailwindcss": "^0.1.2",
|
||||
|
|
@ -24,11 +25,13 @@
|
|||
"@sentry/vue": "^7.20.0",
|
||||
"@urql/vue": "^1.0.2",
|
||||
"@vueuse/core": "^9.13.0",
|
||||
"cypress": "^12.9.0",
|
||||
"d3": "^7.6.1",
|
||||
"dayjs": "^1.11.7",
|
||||
"graphql": "^16.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
"loglevel": "^1.8.0",
|
||||
"mitt": "^3.0.0",
|
||||
"pinia": "^2.0.21",
|
||||
"vue": "^3.2.38",
|
||||
"vue-i18n": "^9.2.2",
|
||||
|
|
@ -38,7 +41,7 @@
|
|||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^2.13.12",
|
||||
"@graphql-codegen/client-preset": "^1.1.4",
|
||||
"@rollup/plugin-alias": "^3.1.9",
|
||||
"@rollup/plugin-alias": "^4.0.3",
|
||||
"@rushstack/eslint-patch": "^1.1.4",
|
||||
"@savvywombat/tailwindcss-grid-areas": "^3.0.0",
|
||||
"@storybook/addon-a11y": "^7.0.0-rc.5",
|
||||
|
|
@ -54,25 +57,25 @@
|
|||
"@storybook/vue3-vite": "^7.0.0-rc.5",
|
||||
"@tailwindcss/forms": "^0.5.2",
|
||||
"@tailwindcss/typography": "^0.5.4",
|
||||
"@testing-library/vue": "^6.6.1",
|
||||
"@testing-library/vue": "^7.0.0",
|
||||
"@types/d3": "^7.4.0",
|
||||
"@types/jsdom": "^20.0.0",
|
||||
"@types/jsdom": "^21.1.1",
|
||||
"@types/lodash": "^4.14.184",
|
||||
"@types/node": "^18.7.14",
|
||||
"@vitejs/plugin-vue": "^3.0.3",
|
||||
"@vitejs/plugin-vue": "4.1",
|
||||
"@volar/vue-typescript": "^1.0.9",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.0",
|
||||
"@vue/test-utils": "^2.0.2",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"autoprefixer": "^10.4.8",
|
||||
"eslint": "8.22.0",
|
||||
"eslint": "8.37",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-storybook": "^0.6.11",
|
||||
"eslint-plugin-vue": "^9.4.0",
|
||||
"jsdom": "^20.0.0",
|
||||
"jsdom": "^21.1.1",
|
||||
"postcss": "^8.4.14",
|
||||
"postcss-import": "^14.1.0",
|
||||
"postcss-import": "^15.1.0",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-plugin-organize-imports": "^3.1.1",
|
||||
"prettier-plugin-sort-json": "^1.0.0",
|
||||
|
|
@ -81,16 +84,16 @@
|
|||
"react-dom": "^18.2.0",
|
||||
"replace-in-file": "^6.3.5",
|
||||
"sass": "^1.54.6",
|
||||
"sass-loader": "^12.6.0",
|
||||
"start-server-and-test": "^1.14.0",
|
||||
"sass-loader": "^13.2.2",
|
||||
"start-server-and-test": "^2.0.0",
|
||||
"storybook": "^7.0.0-rc.5",
|
||||
"storybook-addon-designs": "^v7.0.0-beta.2",
|
||||
"storybook-tailwind-foundations": "^1.1.2",
|
||||
"storybook-vue3-router": "^3.0.0-next.1",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"typescript": "^4.8.2",
|
||||
"vite": "^3.0.9",
|
||||
"vitest": "^0.22.1",
|
||||
"typescript": "^5.0.3",
|
||||
"vite": "^4.2.1",
|
||||
"vitest": "^0.29.8",
|
||||
"vue-tsc": "^1.0.9"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<MainNavigationBar class="flex-none" />
|
||||
<RouterView v-slot="{ Component }" class="flex-auto">
|
||||
<Transition mode="out-in" name="app">
|
||||
<component :is="Component"></component>
|
||||
<component :is="Component" :key="componentKey"></component>
|
||||
</Transition>
|
||||
</RouterView>
|
||||
<AppFooter class="flex-none" />
|
||||
|
|
@ -14,13 +14,21 @@
|
|||
import log from "loglevel";
|
||||
|
||||
import AppFooter from "@/components/AppFooter.vue";
|
||||
import MainNavigationBar from "@/components/MainNavigationBar.vue";
|
||||
import { onMounted } from "vue";
|
||||
import MainNavigationBar from "@/components/header/MainNavigationBar.vue";
|
||||
import eventBus from "@/utils/eventBus";
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
const componentKey = ref(1);
|
||||
|
||||
log.debug("App created");
|
||||
|
||||
onMounted(() => {
|
||||
log.debug("App mounted");
|
||||
|
||||
eventBus.on("switchedCourseSession", () => {
|
||||
// FIXME: clean up with VBV-305
|
||||
componentKey.value++;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ async function changeLocale(language: AvailableLanguages) {
|
|||
</MenuButton>
|
||||
<div class="relative">
|
||||
<MenuItems
|
||||
class="lg:right- absolute -top-24 -right-2/3 w-40 bg-white px-4 py-4 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||
class="lg:right- absolute -right-2/3 -top-24 w-40 bg-white px-4 py-4 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||
>
|
||||
<MenuItem v-for="locale in SUPPORT_LOCALES" :key="locale" class="py-1">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -124,18 +124,18 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
PERCENTAGES,
|
||||
RATINGS,
|
||||
YES_NO,
|
||||
} from "@/components/learningPath/feedback.constants";
|
||||
import FeedbackCompletition from "@/components/learningPath/FeedbackCompletition.vue";
|
||||
import FeedbackIntro from "@/components/learningPath/FeedbackIntro.vue";
|
||||
import LearningContentNavigation from "@/components/learningPath/LearningContentNavigation.vue";
|
||||
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 { useCircleStore } from "@/stores/circle";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import type { LearningContent } from "@/types";
|
||||
|
|
@ -196,7 +196,7 @@ const nextStep = () => {
|
|||
|
||||
const sendFeedback = () => {
|
||||
log.info("sending feedback");
|
||||
const courseSession = courseSessionsStore.courseSessionForRoute;
|
||||
const courseSession = courseSessionsStore.currentCourseSession;
|
||||
if (!courseSession || !courseSession.id) {
|
||||
log.error("no course session set");
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,317 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import log from "loglevel";
|
||||
|
||||
import IconLogout from "@/components/icons/IconLogout.vue";
|
||||
import IconSettings from "@/components/icons/IconSettings.vue";
|
||||
import MobileMenu from "@/components/MobileMenu.vue";
|
||||
import NotificationPopover from "@/components/notifications/NotificationPopover.vue";
|
||||
import NotificationPopoverContent from "@/components/notifications/NotificationPopoverContent.vue";
|
||||
import ItDropdown from "@/components/ui/ItDropdown.vue";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useNotificationsStore } from "@/stores/notifications";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type { DropdownListItem } from "@/types";
|
||||
import type { Component } from "vue";
|
||||
import { onMounted, reactive } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
type DropdownActions = "logout" | "settings" | "profile";
|
||||
|
||||
interface DropdownData {
|
||||
action: DropdownActions;
|
||||
}
|
||||
|
||||
log.debug("MainNavigationBar created");
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const appStore = useAppStore();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const notificationsStore = useNotificationsStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
const state = reactive({ showMenu: false });
|
||||
|
||||
function toggleNav() {
|
||||
state.showMenu = !state.showMenu;
|
||||
}
|
||||
|
||||
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 handleDropdownSelect(data: DropdownData) {
|
||||
switch (data.action) {
|
||||
case "profile":
|
||||
router.push("/profile");
|
||||
break;
|
||||
case "settings":
|
||||
router.push("/settings");
|
||||
break;
|
||||
case "logout":
|
||||
userStore.handleLogout();
|
||||
break;
|
||||
default:
|
||||
console.log("No action");
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
userStore.handleLogout();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
log.debug("MainNavigationBar mounted");
|
||||
if (userStore.loggedIn) {
|
||||
// fixme: only when i'm logged in? should this be handled in the store?
|
||||
// courseSessionsStore.loadCourseSessionsData();
|
||||
}
|
||||
});
|
||||
|
||||
const profileDropdownData: DropdownListItem[] = [
|
||||
{
|
||||
title: t("mainNavigation.profile"),
|
||||
icon: IconSettings as Component,
|
||||
data: {
|
||||
action: "profile",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("general.settings"),
|
||||
icon: IconSettings as Component,
|
||||
data: {
|
||||
action: "settings",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("mainNavigation.logout"),
|
||||
icon: IconLogout as Component,
|
||||
data: {
|
||||
action: "logout",
|
||||
},
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Teleport to="body">
|
||||
<MobileMenu
|
||||
v-if="userStore.loggedIn"
|
||||
:show="state.showMenu"
|
||||
:course-session="courseSessionsStore.courseSessionForRoute"
|
||||
:media-url="courseSessionsStore.courseSessionForRoute?.media_library_url"
|
||||
:user="userStore"
|
||||
@closemodal="state.showMenu = false"
|
||||
@logout="userStore.handleLogout()"
|
||||
/>
|
||||
</Teleport>
|
||||
<Transition name="nav">
|
||||
<div v-if="appStore.showMainNavigationBar" class="navigation bg-blue-900">
|
||||
<nav class="mx-auto px-8 py-2 lg:flex lg:items-center lg:justify-start lg:py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<router-link to="/" class="flex">
|
||||
<it-icon-vbv class="mr-3 -mt-6 -ml-3 h-8 w-16" />
|
||||
</router-link>
|
||||
<router-link to="/" class="flex">
|
||||
<div class="ml-1 border-l border-white pr-10 pl-3 text-2xl text-white">
|
||||
{{ $t("general.title") }}
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center lg:hidden">
|
||||
<div v-if="userStore.loggedIn" class="mr-6 flex flex-row items-center">
|
||||
<NotificationPopover>
|
||||
<template #toggleButtonContent>
|
||||
<div class="nav-item flex">
|
||||
<it-icon-notification class="h-6 w-6" />
|
||||
<div
|
||||
v-if="notificationsStore.hasUnread"
|
||||
aria-label="unread notifications"
|
||||
class="mt-1 h-1.5 w-1.5 rounded-full bg-sky-500"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #popoverContent>
|
||||
<NotificationPopoverContent />
|
||||
</template>
|
||||
</NotificationPopover>
|
||||
</div>
|
||||
<router-link
|
||||
v-if="userStore.loggedIn"
|
||||
to="/messages"
|
||||
class="nav-item flex flex-row items-center"
|
||||
data-cy="messages-link"
|
||||
>
|
||||
<it-icon-persons class="mr-6 h-6 w-6" />
|
||||
</router-link>
|
||||
<!-- Mobile menu button -->
|
||||
<div class="flex" @click="toggleNav">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu open: "block", Menu closed: "hidden" -->
|
||||
<div
|
||||
v-if="appStore.userLoaded && appStore.routingFinished && userStore.loggedIn"
|
||||
:class="state.showMenu ? 'flex' : 'hidden'"
|
||||
class="mt-8 flex-auto lg:mt-0 lg:flex lg:flex-row lg:items-center lg:space-y-0 lg:space-x-10"
|
||||
>
|
||||
<!-- <router-link-->
|
||||
<!-- v-if="inCourse() && courseSessionsStore.courseSessionForRoute"-->
|
||||
<!-- :to="`${courseSessionsStore.courseSessionForRoute.course_url}/cockpit`"-->
|
||||
<!-- class="nav-item"-->
|
||||
<!-- :class="{ 'nav-item--active': inCockpit() }"-->
|
||||
<!-- >-->
|
||||
<!-- Cockpit-->
|
||||
<!-- </router-link>-->
|
||||
|
||||
<router-link
|
||||
v-if="
|
||||
inCourse() &&
|
||||
courseSessionsStore.courseSessionForRoute &&
|
||||
courseSessionsStore.hasCockpit
|
||||
"
|
||||
:to="`${courseSessionsStore.courseSessionForRoute.course_url}/cockpit`"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inCockpit() }"
|
||||
>
|
||||
{{ $t("cockpit.title") }}
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-if="inCourse() && courseSessionsStore.courseSessionForRoute"
|
||||
:to="courseSessionsStore.courseSessionForRoute.learning_path_url"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inLearningPath() }"
|
||||
>
|
||||
{{ $t("general.learningPath") }}
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-if="inCourse() && courseSessionsStore.courseSessionForRoute"
|
||||
:to="courseSessionsStore.courseSessionForRoute.competence_url"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inCompetenceProfile() }"
|
||||
>
|
||||
{{ $t("competences.title") }}
|
||||
</router-link>
|
||||
|
||||
<div class="hidden flex-auto lg:block"></div>
|
||||
<a
|
||||
class="nav-item"
|
||||
target="_blank"
|
||||
href="https://bildung.vbv.ch/ilp/pages/catalogsearch.jsf"
|
||||
>
|
||||
{{ $t("general.shop") }}
|
||||
</a>
|
||||
<router-link
|
||||
v-if="courseSessionsStore.courseSessionForRoute"
|
||||
:to="courseSessionsStore.courseSessionForRoute.media_library_url"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inMediaLibrary() }"
|
||||
data-cy="medialibrary-link"
|
||||
>
|
||||
{{ $t("mediaLibrary.title") }}
|
||||
</router-link>
|
||||
<div v-if="userStore.loggedIn" class="mr-6 flex items-center">
|
||||
<NotificationPopover>
|
||||
<template #toggleButtonContent>
|
||||
<div class="nav-item flex">
|
||||
<it-icon-notification class="h-6 w-6" />
|
||||
<div
|
||||
v-if="notificationsStore.hasUnread"
|
||||
aria-label="unread notifications"
|
||||
class="mt-1 h-1.5 w-1.5 rounded-full bg-sky-500"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #popoverContent>
|
||||
<NotificationPopoverContent />
|
||||
</template>
|
||||
</NotificationPopover>
|
||||
</div>
|
||||
<router-link
|
||||
to="/messages"
|
||||
class="nav-item flex flex-row items-center"
|
||||
data-cy="messages-link"
|
||||
>
|
||||
<it-icon-persons class="mr-6 h-6 w-6" />
|
||||
</router-link>
|
||||
<div v-if="userStore.loggedIn" class="nav-item flex items-center">
|
||||
<ItDropdown
|
||||
:button-classes="[]"
|
||||
:list-items="profileDropdownData"
|
||||
:align="'right'"
|
||||
@select="handleDropdownSelect"
|
||||
>
|
||||
<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>
|
||||
</ItDropdown>
|
||||
</div>
|
||||
<div v-else><a class="" href="/login">Login</a></div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.nav-item {
|
||||
@apply text-2xl font-bold text-white hover:text-sky-500 lg:text-base lg:font-normal;
|
||||
}
|
||||
|
||||
.nav-item--active {
|
||||
@apply underline decoration-sky-500 decoration-4 underline-offset-[21px];
|
||||
}
|
||||
|
||||
.nav-enter-active,
|
||||
.nav-leave-active {
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-enter-from,
|
||||
.nav-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-80px);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -4,6 +4,7 @@ import ItProgress from "@/components/ui/ItProgress.vue";
|
|||
import ItToggleArrow from "@/components/ui/ItToggleArrow.vue";
|
||||
import { useCompetenceStore } from "@/stores/competence";
|
||||
import type { CompetencePage } from "@/types";
|
||||
import log from "loglevel";
|
||||
import { ref } from "vue";
|
||||
|
||||
const competenceStore = useCompetenceStore();
|
||||
|
|
@ -20,6 +21,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
isInline: false,
|
||||
});
|
||||
|
||||
log.debug("PerformanceCriteriaRow created", props);
|
||||
|
||||
const isOpen = ref(false);
|
||||
|
||||
const togglePerformanceCriteria = () => {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
></it-icon-smiley-thinking>
|
||||
<it-icon-smiley-neutral v-else></it-icon-smiley-neutral>
|
||||
</div>
|
||||
<div class="mb-4 pr-5 lg:mr-10 lg:mb-0">
|
||||
<div class="mb-4 pr-5 lg:mb-0 lg:mr-10">
|
||||
<h4 class="text-bold mb-2">
|
||||
{{ criteria.competence_id }} {{ criteria.title }}
|
||||
</h4>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
// @ts-nocheck
|
||||
import avatar from "../../../.storybook/uk1.patrizia.huggel.jpg";
|
||||
|
||||
import AccountMenuContent from "@/components/header/AccountMenuContent.vue";
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/7.0/vue/writing-stories/introduction
|
||||
const meta: Meta<typeof AccountMenuContent> = {
|
||||
title: "VBV/Header/AccountMenuContent",
|
||||
component: AccountMenuContent,
|
||||
tags: ["autodocs"],
|
||||
argTypes: { onClosemodal: { action: "closeModal" } },
|
||||
parameters: {
|
||||
viewport: {
|
||||
defaultViewport: "mobile1",
|
||||
},
|
||||
},
|
||||
decorators: [() => ({ template: '<div class=""><story /></div>' })],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AccountMenuContent>;
|
||||
|
||||
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: {
|
||||
allCourseSessions: courseSessions,
|
||||
user: loggedInUser,
|
||||
},
|
||||
};
|
||||
|
||||
export const NoUser: Story = {
|
||||
args: {
|
||||
allCourseSessions: courseSessions,
|
||||
user: {
|
||||
loggedIn: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<script setup lang="ts">
|
||||
import CourseSessionsMenu from "@/components/header/CourseSessionsMenu.vue";
|
||||
import type { UserState } from "@/stores/user";
|
||||
import type { CourseSession } from "@/types";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSessions: CourseSession[];
|
||||
user: UserState;
|
||||
selectedCourseSession?: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["selectCourseSession", "logout"]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-black">
|
||||
<div class="border-b py-4">
|
||||
<div class="flex justify-start">
|
||||
<div v-if="user.avatar_url">
|
||||
<img
|
||||
class="inline-block h-20 w-20 rounded-full"
|
||||
:src="user.avatar_url"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-6">
|
||||
<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">
|
||||
<router-link class="link" to="/profile">Profil anzeigen</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="props.courseSessions.length" class="border-b py-4">
|
||||
<CourseSessionsMenu
|
||||
:items="courseSessions"
|
||||
:selected="selectedCourseSession"
|
||||
@select="emit('selectCourseSession', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="button" class="mt-6 flex items-center" @click="emit('logout')">
|
||||
<it-icon-logout class="inline-block" />
|
||||
<span class="ml-1">{{ $t("mainNavigation.logout") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -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],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
<script setup lang="ts">
|
||||
import log from "loglevel";
|
||||
|
||||
import AccountMenu from "@/components/header/AccountMenu.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 { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useNotificationsStore } from "@/stores/notifications";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { useRouteLookups } from "@/utils/route";
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/vue";
|
||||
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
|
||||
import { computed, onMounted, reactive } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
log.debug("MainNavigationBar created");
|
||||
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind);
|
||||
const userStore = useUserStore();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const notificationsStore = useNotificationsStore();
|
||||
const { inCockpit, inCompetenceProfile, inCourse, inLearningPath } = useRouteLookups();
|
||||
|
||||
const { t } = useI18n();
|
||||
const state = reactive({
|
||||
showMobileNavigationMenu: false,
|
||||
showMobileProfileMenu: false,
|
||||
});
|
||||
|
||||
function popoverClick(event: Event) {
|
||||
if (breakpoints.smaller("lg").value) {
|
||||
event.preventDefault();
|
||||
state.showMobileProfileMenu = true;
|
||||
}
|
||||
}
|
||||
|
||||
const selectedCourseSessionTitle = computed(() => {
|
||||
return courseSessionsStore.currentCourseSession?.title;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
log.debug("MainNavigationBar mounted");
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Teleport to="body">
|
||||
<MobileMenu
|
||||
v-if="userStore.loggedIn"
|
||||
:show="state.showMobileNavigationMenu"
|
||||
:course-session="courseSessionsStore.currentCourseSession"
|
||||
:media-url="courseSessionsStore.currentCourseSession?.media_library_url"
|
||||
: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 />
|
||||
</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 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>
|
||||
|
||||
<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="hidden space-x-8 lg:flex">
|
||||
<!-- Navigation Links Desktop -->
|
||||
<router-link
|
||||
v-if="
|
||||
inCourse() &&
|
||||
courseSessionsStore.currentCourseSession &&
|
||||
courseSessionsStore.hasCockpit
|
||||
"
|
||||
:to="`${courseSessionsStore.currentCourseSession.course_url}/cockpit`"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inCockpit() }"
|
||||
>
|
||||
{{ t("cockpit.title") }}
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-if="inCourse() && courseSessionsStore.currentCourseSession"
|
||||
:to="courseSessionsStore.currentCourseSession.learning_path_url"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inLearningPath() }"
|
||||
>
|
||||
{{ t("general.learningPath") }}
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-if="inCourse() && courseSessionsStore.currentCourseSession"
|
||||
:to="courseSessionsStore.currentCourseSession.competence_url"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inCompetenceProfile() }"
|
||||
>
|
||||
{{ t("competences.title") }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-stretch justify-start space-x-8">
|
||||
<!-- Notification Bell & Menu -->
|
||||
<div v-if="userStore.loggedIn" class="nav-item">
|
||||
<NotificationPopover>
|
||||
<template #toggleButtonContent>
|
||||
<div class="flex">
|
||||
<it-icon-notification class="h-6 w-6" />
|
||||
<div
|
||||
v-if="notificationsStore.hasUnread"
|
||||
aria-label="unread notifications"
|
||||
class="mt-1 h-1.5 w-1.5 rounded-full bg-sky-500"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #popoverContent>
|
||||
<NotificationPopoverContent />
|
||||
</template>
|
||||
</NotificationPopover>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="selectedCourseSessionTitle"
|
||||
class="hidden items-center lg:inline-flex"
|
||||
>
|
||||
<div class="">
|
||||
{{ selectedCourseSessionTitle }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-item">
|
||||
<div v-if="userStore.loggedIn" class="flex items-center">
|
||||
<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
|
||||
class="absolute -right-2 top-8 z-50 w-[500px] bg-white shadow-lg"
|
||||
>
|
||||
<div class="p-4">
|
||||
<AccountMenu />
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
</div>
|
||||
<div v-else><a class="" href="/login">Login</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</Transition>
|
||||
</div>
|
||||
</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--active {
|
||||
@apply border-sky-500;
|
||||
}
|
||||
|
||||
.nav-enter-active,
|
||||
.nav-leave-active {
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-enter-from,
|
||||
.nav-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-80px);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
// @ts-nocheck
|
||||
import avatar from "../../.storybook/uk1.patrizia.huggel.jpg";
|
||||
import avatar from "../../../.storybook/uk1.patrizia.huggel.jpg";
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import MobileMenu from "./MobileMenu.vue";
|
||||
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/7.0/vue/writing-stories/introduction
|
||||
const meta: Meta<typeof MobileMenu> = {
|
||||
title: "VBV/MobileMenu",
|
||||
title: "VBV/Header/MobileMenu",
|
||||
component: MobileMenu,
|
||||
tags: ["autodocs"],
|
||||
argTypes: { onClosemodal: { action: "closeModal" } },
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
<script setup lang="ts">
|
||||
import IconLogout from "@/components/icons/IconLogout.vue";
|
||||
import IconSettings from "@/components/icons/IconSettings.vue";
|
||||
import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
|
||||
import type { UserState } from "@/stores/user";
|
||||
import type { CourseSession } from "@/types";
|
||||
|
|
@ -44,7 +42,7 @@ const clickLink = (to: string | undefined) => {
|
|||
class="mt-2 inline-block flex items-center"
|
||||
@click="clickLink('/settings')"
|
||||
>
|
||||
<IconSettings class="inline-block" />
|
||||
<it-icon-settings class="inline-block" />
|
||||
<span class="ml-3">{{ $t("general.settings") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -90,7 +88,7 @@ const clickLink = (to: string | undefined) => {
|
|||
class="mt-6 flex items-center"
|
||||
@click="$emit('logout')"
|
||||
>
|
||||
<IconLogout class="inline-block" />
|
||||
<it-icon-logout class="inline-block" />
|
||||
<span class="ml-1">{{ $t("mainNavigation.logout") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<template>
|
||||
<svg
|
||||
width="30"
|
||||
height="30"
|
||||
viewBox="0 0 30 30"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M26.3292 15.7779C26.3292 15.7625 26.3474 15.7474 26.3536 15.7321C26.3619 15.71 26.3691 15.6877 26.3749 15.6649V15.6222C26.39 15.5448 26.39 15.465 26.3749 15.3876V15.3449C26.3691 15.3221 26.3619 15.2998 26.3536 15.2777C26.3536 15.2623 26.3383 15.2472 26.3292 15.2319C26.32 15.2132 26.3098 15.1949 26.2985 15.1771L26.262 15.1313L26.2315 15.0918L20.9236 9.44226C20.8147 9.31721 20.6597 9.24193 20.494 9.23364C20.3283 9.22556 20.1665 9.2851 20.0457 9.39888C19.9249 9.51244 19.8558 9.67046 19.8541 9.83634C19.8522 10.002 19.9181 10.1615 20.0363 10.2776L24.3779 14.8906H7.47836C7.1417 14.8906 6.86865 15.1634 6.86865 15.5003C6.86865 15.8371 7.14172 16.11 7.47836 16.11H24.3779L20.0363 20.7229C19.8213 20.9698 19.8398 21.3426 20.0782 21.5672C20.3169 21.7916 20.6898 21.7877 20.9236 21.5583L26.2315 15.918L26.2619 15.8784L26.2985 15.8327C26.3098 15.8149 26.32 15.7966 26.3291 15.7779L26.3292 15.7779Z"
|
||||
fill="#0A0A0A"
|
||||
/>
|
||||
<path
|
||||
d="M6.65855 27H11.5884C12.5588 27 13.4894 26.6146 14.1755 25.9286C14.8616 25.2423 15.247 24.3119 15.247 23.3414V21.2926C15.247 20.9559 14.9741 20.6829 14.6372 20.6829C14.3006 20.6829 14.0275 20.956 14.0275 21.2926V23.3506C14.0275 23.9975 13.7706 24.6179 13.3132 25.0753C12.8558 25.5328 12.2354 25.7897 11.5885 25.7897H6.65861C6.01166 25.7897 5.39134 25.5328 4.93386 25.0753C4.47642 24.6179 4.21952 23.9975 4.21952 23.3506V7.65855C4.21952 7.01161 4.47642 6.39129 4.93386 5.93381C5.39131 5.47636 6.01166 5.21946 6.65861 5.21946H11.5885C12.2354 5.21946 12.8557 5.47637 13.3132 5.93381C13.7706 6.39125 14.0275 7.01161 14.0275 7.65855V9.7074C14.0275 10.0441 14.3006 10.3171 14.6372 10.3171C14.9741 10.3171 15.247 10.044 15.247 9.7074V7.65855C15.247 6.68817 14.8616 5.75774 14.1755 5.07143C13.4894 4.38535 12.5588 4 11.5884 4H6.65855C5.68817 4 4.75774 4.38535 4.07143 5.07143C3.38535 5.75768 3 6.68811 3 7.65855V23.3413C3 24.3117 3.38535 25.2422 4.07143 25.9285C4.75768 26.6145 5.68811 26.9999 6.65855 26.9999V27Z"
|
||||
fill="#0A0A0A"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,7 +0,0 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--mdi"
|
||||
width="24"
|
||||
height="24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<template>
|
||||
<svg viewBox="0 0 39 35" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M38.3507 28.111C38.3898 28.1686 38.4124 28.2358 38.4162 28.3053C38.4201 28.3748 38.4049 28.4441 38.3725 28.5057C38.34 28.5672 38.2914 28.6188 38.2319 28.655C38.1724 28.6911 38.1042 28.7104 38.0346 28.7108H36.3463C36.2839 28.7105 36.2224 28.6949 36.1673 28.6655C36.1122 28.636 36.0652 28.5936 36.0302 28.5419L32.0792 22.7066L28.1282 28.5419C28.0932 28.5936 28.0461 28.636 27.991 28.6654C27.9359 28.6949 27.8745 28.7104 27.812 28.7108H26.122C26.0523 28.7104 25.9841 28.6911 25.9246 28.655C25.8651 28.6188 25.8165 28.5672 25.7841 28.5057C25.7516 28.4441 25.7365 28.3748 25.7403 28.3053C25.7442 28.2358 25.7668 28.1686 25.8058 28.111L31.7648 19.312C31.8 19.26 31.8475 19.2175 31.9029 19.1881C31.9584 19.1586 32.0202 19.1432 32.083 19.1432C32.1458 19.1432 32.2076 19.1586 32.2631 19.1881C32.3186 19.2175 32.366 19.26 32.4012 19.312L38.3507 28.111Z"
|
||||
fill="#007AC3"
|
||||
/>
|
||||
<path
|
||||
d="M12.6181 28.111C12.6572 28.1686 12.6798 28.2358 12.6836 28.3053C12.6874 28.3748 12.6723 28.4441 12.6399 28.5057C12.6074 28.5672 12.5588 28.6188 12.4993 28.655C12.4398 28.6911 12.3716 28.7104 12.302 28.7108H10.6119C10.5495 28.7104 10.488 28.6949 10.4329 28.6654C10.3779 28.636 10.3308 28.5936 10.2958 28.5419L6.34521 22.7066L2.3942 28.5419C2.35919 28.5936 2.31211 28.636 2.25702 28.6654C2.20193 28.6949 2.1405 28.7104 2.07805 28.7108H0.383918C0.314304 28.7104 0.246102 28.6911 0.186595 28.655C0.127088 28.6188 0.0785123 28.5672 0.0460511 28.5057C0.01359 28.4441 -0.00153491 28.3748 0.00228956 28.3053C0.00611403 28.2358 0.0287451 28.1686 0.0677665 28.111L6.0268 19.312C6.06202 19.26 6.10943 19.2175 6.1649 19.1881C6.22037 19.1586 6.2822 19.1432 6.34498 19.1432C6.40777 19.1432 6.4696 19.1586 6.52506 19.1881C6.58053 19.2175 6.62795 19.26 6.66317 19.312L12.6181 28.111Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M0.0660575 7.0891C0.027036 7.03145 0.00440495 6.96429 0.000580477 6.89478C-0.003244 6.82527 0.0118844 6.75603 0.0443455 6.69444C0.0768067 6.63286 0.125382 6.58125 0.184889 6.54512C0.244396 6.50899 0.312599 6.48971 0.382213 6.48932H2.07047C2.13293 6.48967 2.19436 6.50523 2.24944 6.53466C2.30453 6.56409 2.35161 6.60651 2.38662 6.65824L6.33718 12.4935L10.2882 6.65824C10.3232 6.60651 10.3703 6.56409 10.4254 6.53466C10.4805 6.50523 10.5419 6.48967 10.6043 6.48932H12.2944C12.364 6.48971 12.4322 6.50899 12.4917 6.54512C12.5512 6.58125 12.5998 6.63286 12.6323 6.69444C12.6647 6.75603 12.6799 6.82527 12.676 6.89478C12.6722 6.96429 12.6496 7.03145 12.6106 7.0891L6.65966 15.8876C6.62451 15.9397 6.57711 15.9824 6.52164 16.0118C6.46616 16.0413 6.40429 16.0568 6.34147 16.0568C6.27865 16.0568 6.21678 16.0413 6.16131 16.0118C6.10583 15.9824 6.05844 15.9397 6.02328 15.8876L0.0660575 7.0891Z"
|
||||
fill="#007AC3"
|
||||
/>
|
||||
<path
|
||||
d="M25.7982 7.0891C25.7592 7.03145 25.7366 6.96429 25.7328 6.89478C25.7289 6.82527 25.7441 6.75603 25.7765 6.69444C25.809 6.63286 25.8576 6.58125 25.9171 6.54512C25.9766 6.50899 26.0448 6.48971 26.1144 6.48932H27.8026C27.8651 6.48963 27.9265 6.50517 27.9816 6.53461C28.0367 6.56405 28.0838 6.60648 28.1188 6.65824L32.0698 12.4935L36.0208 6.65824C36.0558 6.60651 36.1029 6.56409 36.158 6.53466C36.2131 6.50523 36.2745 6.48967 36.337 6.48932H38.027C38.0966 6.48971 38.1648 6.50899 38.2244 6.54512C38.2839 6.58125 38.3324 6.63286 38.3649 6.69444C38.3974 6.75603 38.4125 6.82527 38.4087 6.89478C38.4048 6.96429 38.3822 7.03145 38.3432 7.0891L32.3841 15.8881C32.349 15.9402 32.3016 15.9828 32.2461 16.0123C32.1906 16.0418 32.1288 16.0572 32.066 16.0572C32.0031 16.0572 31.9413 16.0418 31.8858 16.0123C31.8303 15.9828 31.7829 15.9402 31.7478 15.8881L25.7982 7.0891Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M24.2526 20.5202C23.1808 19.5633 21.7844 19.0514 20.3481 19.0889C17.1866 19.0889 15.1465 21.3851 15.1465 24.9395V34.1161C15.1466 34.218 15.1872 34.3156 15.2592 34.3876C15.3313 34.4596 15.429 34.5 15.5308 34.5H17.1035C17.2053 34.5 17.303 34.4596 17.3751 34.3876C17.4472 34.3156 17.4877 34.218 17.4878 34.1161V28.5739H22.0228C22.1247 28.5738 22.2223 28.5333 22.2943 28.4612C22.3663 28.3891 22.4067 28.2914 22.4067 28.1896V26.7895C22.4067 26.6876 22.3663 26.5899 22.2943 26.5179C22.2223 26.4458 22.1247 26.4052 22.0228 26.4051H17.4874V24.8722C17.4874 22.5431 18.5262 21.2591 20.4163 21.2591C21.3288 21.2362 22.2134 21.5752 22.8769 22.2021L23.0395 22.3471C23.0789 22.3821 23.125 22.4086 23.1751 22.4251C23.2252 22.4415 23.2782 22.4474 23.3307 22.4425C23.3831 22.4376 23.434 22.4219 23.4802 22.3965C23.5264 22.3711 23.5668 22.3364 23.5991 22.2947L24.4545 21.1859C24.5125 21.1105 24.5406 21.0163 24.5331 20.9214C24.5257 20.8265 24.4834 20.7378 24.4143 20.6724C24.3343 20.596 24.263 20.5283 24.2526 20.5202Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M19.2443 6.13249C19.2444 6.08179 19.2546 6.03161 19.2742 5.98484C19.2938 5.93808 19.3224 5.89566 19.3585 5.86001C19.3945 5.82437 19.4373 5.79622 19.4843 5.77717C19.5313 5.75813 19.5816 5.74857 19.6323 5.74904C19.7226 5.74904 19.7989 5.74904 19.8129 5.7522C21.1089 5.881 22.3057 6.50307 23.1558 7.4897C24.0059 8.47632 24.4442 9.75198 24.38 11.0527C24.2917 12.3636 23.7038 13.5907 22.7374 14.4808C21.7709 15.3709 20.4998 15.8562 19.186 15.8366H15.5277C15.4258 15.8365 15.3282 15.7959 15.2562 15.7238C15.1842 15.6518 15.1438 15.5541 15.1438 15.4522V0.884351C15.1438 0.782493 15.1842 0.6848 15.2562 0.612733C15.3282 0.540666 15.4258 0.50012 15.5277 0.5H17.0976C17.1996 0.5 17.2973 0.540494 17.3694 0.612574C17.4415 0.684654 17.482 0.782414 17.482 0.884351V13.2405C17.482 13.3425 17.5225 13.4402 17.5946 13.5123C17.6666 13.5844 17.7644 13.6249 17.8663 13.6249H19.2326C19.9527 13.6327 20.6501 13.3736 21.1905 12.8975C21.7308 12.4215 22.0758 11.7622 22.1588 11.0469C22.2259 10.2951 21.9924 9.5474 21.5092 8.96758C21.0261 8.38775 20.3328 8.0231 19.5812 7.95353C19.4866 7.94417 19.3988 7.89972 19.3353 7.82892C19.2718 7.75812 19.2371 7.66611 19.238 7.57099L19.2443 6.13249Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -29,7 +29,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
<div
|
||||
v-if="icon"
|
||||
:class="[`bg-${icon}`]"
|
||||
class="-ml-8 h-32 bg-contain bg-left bg-no-repeat lg:ml-0 lg:-mr-8 lg:block lg:w-2/6 lg:bg-right"
|
||||
class="-ml-8 h-32 bg-contain bg-left bg-no-repeat lg:-mr-8 lg:ml-0 lg:block lg:w-2/6 lg:bg-right"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ function onNotificationClick(notification: Notification) {
|
|||
<template>
|
||||
<div
|
||||
v-if="0 === state.notifications.length"
|
||||
class="mt-14 mb-14 text-center text-black"
|
||||
class="mb-14 mt-14 text-center text-black"
|
||||
data-cy="no-notifications"
|
||||
>
|
||||
{{ $t("notifications.no_notifications") }}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { CheckboxItem } from "@/components/ui/checkbox.types";
|
|||
import log from "loglevel";
|
||||
|
||||
const props = defineProps<{
|
||||
checkbox_item: CheckboxItem<any>;
|
||||
checkboxItem: CheckboxItem<any>;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ const input = (e: Event) => {
|
|||
<label
|
||||
class="cy-checkbox cy-checkbox-checked block flex h-8 items-center bg-contain bg-no-repeat pl-8 disabled:opacity-50"
|
||||
: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-unchecked.svg)] hover:bg-[url(/static/icons/icon-checkbox-unchecked-hover.svg)]'
|
||||
"
|
||||
|
|
@ -45,21 +45,21 @@ const input = (e: Event) => {
|
|||
>
|
||||
<input
|
||||
ref="checkbox"
|
||||
:checked="checkbox_item.checked"
|
||||
:value="checkbox_item.value"
|
||||
:checked="checkboxItem.checked"
|
||||
:value="checkboxItem.value"
|
||||
:disabled="disabled"
|
||||
:data-cy="`it-checkbox-${checkbox_item.value}`"
|
||||
:data-cy="`it-checkbox-${checkboxItem.value}`"
|
||||
class="sr-only"
|
||||
type="checkbox"
|
||||
@keydown="keydown"
|
||||
@input="input"
|
||||
/>
|
||||
<div class="ml-4 flex-col">
|
||||
<div v-if="checkbox_item.label">
|
||||
{{ checkbox_item.label }}
|
||||
<div v-if="checkboxItem.label">
|
||||
{{ checkboxItem.label }}
|
||||
</div>
|
||||
<div v-if="checkbox_item.subtitle" class="text-gray-900">
|
||||
{{ checkbox_item.subtitle }}
|
||||
<div v-if="checkboxItem.subtitle" class="text-gray-900">
|
||||
{{ checkboxItem.subtitle }}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ function updateItems(itemValue: string) {
|
|||
<ItCheckbox
|
||||
v-for="item in items"
|
||||
:key="item.value"
|
||||
:checkbox_item="item"
|
||||
:checkbox-item="item"
|
||||
:class="item.subtitle ? 'mb-6' : 'mb-4'"
|
||||
@toggle="updateItems(item.value)"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ function setIsOpen(value: boolean) {
|
|||
<Dialog :open="modelValue" class="relative z-50" @close="setIsOpen">
|
||||
<div class="fixed inset-0 bg-black/30" aria-hidden="true"></div>
|
||||
<div class="fixed inset-0 flex items-center justify-center p-4">
|
||||
<DialogPanel class="w-full max-w-2xl bg-white px-8 pt-8 pb-4">
|
||||
<DialogPanel class="w-full max-w-2xl bg-white px-8 pb-4 pt-8">
|
||||
<DialogTitle class="relative mb-8 flex flex-row">
|
||||
<slot name="title"></slot>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ const endBadgeClasses = computed(() => {
|
|||
<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
|
||||
class="inline-flex h-7 w-7 items-center justify-center rounded-full py-1 px-3"
|
||||
class="inline-flex h-7 w-7 items-center justify-center rounded-full px-3 py-1"
|
||||
:class="getPillClasses(step)"
|
||||
>
|
||||
{{ step }}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
{{ label }}
|
||||
</RadioGroupLabel>
|
||||
<div
|
||||
class="align-items-center flex flex-col justify-between justify-items-center space-y-6 md:flex-row md:space-y-0 md:space-x-6"
|
||||
class="align-items-center flex flex-col justify-between justify-items-center space-y-6 md:flex-row md:space-x-6 md:space-y-0"
|
||||
>
|
||||
<RadioGroupOption
|
||||
v-for="item in items"
|
||||
|
|
@ -29,14 +29,9 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { RadioItem } from "@/components/learningPath/feedback.types";
|
||||
import type { RadioItem } from "@/pages/learningPath/learningContentPage/feedback/feedback.types";
|
||||
|
||||
import {
|
||||
RadioGroup,
|
||||
// RadioGroupDescription,
|
||||
RadioGroupLabel,
|
||||
RadioGroupOption,
|
||||
} from "@headlessui/vue";
|
||||
import { RadioGroup, RadioGroupLabel, RadioGroupOption } from "@headlessui/vue";
|
||||
|
||||
defineProps<{
|
||||
modelValue: any;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
export interface CheckboxItem<T> {
|
||||
export interface BaseItem<T> {
|
||||
value: T;
|
||||
label?: string;
|
||||
checked: boolean;
|
||||
subtitle?: string;
|
||||
}
|
||||
|
||||
export interface CheckboxItem<T> extends BaseItem<T> {
|
||||
checked: boolean;
|
||||
}
|
||||
|
||||
export interface RadioItem<T> extends BaseItem<T> {
|
||||
selected: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,12 +28,15 @@ onMounted(async () => {
|
|||
<h1 data-cy="welcome-message">
|
||||
{{ $t("dashboard.welcome", { name: userStore.first_name }) }}
|
||||
</h1>
|
||||
<div v-if="courseSessionsStore.courseSessions.length > 0" class="mb-14">
|
||||
<h2 class="mt-12 mb-3">Kurse</h2>
|
||||
<div
|
||||
v-if="courseSessionsStore.uniqueCourseSessionsByCourse.length > 0"
|
||||
class="mb-14"
|
||||
>
|
||||
<h2 class="mb-3 mt-12">Kurse</h2>
|
||||
|
||||
<div class="grid auto-rows-fr grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div
|
||||
v-for="courseSession in courseSessionsStore.coursesFromCourseSessions"
|
||||
v-for="courseSession in courseSessionsStore.uniqueCourseSessionsByCourse"
|
||||
:key="courseSession.id"
|
||||
>
|
||||
<div class="bg-white p-6 md:h-full">
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ const userStore = useUserStore();
|
|||
v-model="state.username"
|
||||
type="text"
|
||||
name="username"
|
||||
class="mt-1 block w-96 max-w-full border py-2 px-3"
|
||||
class="mt-1 block w-96 max-w-full border px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
|
|
@ -49,7 +49,7 @@ const userStore = useUserStore();
|
|||
v-model="state.password"
|
||||
type="password"
|
||||
name="password"
|
||||
class="mt-1 block w-96 max-w-full border py-2 px-3"
|
||||
class="mt-1 block w-96 max-w-full border px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ onMounted(async () => {
|
|||
<template>
|
||||
<div class="bg-gray-200">
|
||||
<div class="container-large">
|
||||
<header class="mt-12 mb-8">
|
||||
<header class="mb-8 mt-12">
|
||||
<h1>{{ $t("general.settings") }}</h1>
|
||||
</header>
|
||||
<main>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
<script setup lang="ts">
|
||||
import IconLogout from "@/components/icons/IconLogout.vue";
|
||||
import IconSettings from "@/components/icons/IconSettings.vue";
|
||||
import LearningPathCircle from "@/components/learningPath/page/LearningPathCircle.vue";
|
||||
import HorizontalBarChart from "@/components/ui/HorizontalBarChart.vue";
|
||||
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||
import ItCheckboxGroup from "@/components/ui/ItCheckboxGroup.vue";
|
||||
|
|
@ -13,6 +10,7 @@ import ItTextarea from "@/components/ui/ItTextarea.vue";
|
|||
import ItToggleSwitch from "@/components/ui/ItToggleSwitch.vue";
|
||||
import RatingScale from "@/components/ui/RatingScale.vue";
|
||||
import VerticalBarChart from "@/components/ui/VerticalBarChart.vue";
|
||||
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
||||
import logger from "loglevel";
|
||||
import { reactive, ref } from "vue";
|
||||
|
||||
|
|
@ -39,19 +37,19 @@ const state = reactive({
|
|||
const dropdownData = [
|
||||
{
|
||||
title: "Option 1",
|
||||
icon: IconLogout,
|
||||
icon: "it-icon-logout",
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
title: "Option 2",
|
||||
icon: IconLogout,
|
||||
icon: "it-icon-logout",
|
||||
data: {
|
||||
test: 12,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Option 3",
|
||||
icon: IconSettings,
|
||||
icon: "it-icon-settings",
|
||||
data: {
|
||||
amount: 34,
|
||||
},
|
||||
|
|
@ -134,7 +132,7 @@ function log(data: any) {
|
|||
Use them like <it-icon-message/>
|
||||
</p>
|
||||
|
||||
<div class="mt-8 mb-8 flex flex-col flex-wrap gap-4 lg:flex-row">
|
||||
<div class="mb-8 mt-8 flex flex-col flex-wrap gap-4 lg:flex-row">
|
||||
<div class="inline-flex flex-col">
|
||||
message
|
||||
<it-icon-message class="it-icon" />
|
||||
|
|
@ -189,9 +187,14 @@ function log(data: any) {
|
|||
menu
|
||||
<it-icon-menu />
|
||||
</div>
|
||||
|
||||
<div class="inline-flex flex-col">
|
||||
settings
|
||||
<it-icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 mb-8 flex flex-col flex-wrap gap-4 lg:flex-row">
|
||||
<div class="mb-8 mt-8 flex flex-col flex-wrap gap-4 lg:flex-row">
|
||||
<div class="inline-flex flex-col">
|
||||
ls-apply
|
||||
<it-icon-ls-apply />
|
||||
|
|
@ -228,7 +231,7 @@ function log(data: any) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 mb-8 flex flex-col flex-wrap gap-4 lg:flex-row">
|
||||
<div class="mb-8 mt-8 flex flex-col flex-wrap gap-4 lg:flex-row">
|
||||
<div class="inline-flex flex-col">
|
||||
lc-assignment
|
||||
<it-icon-lc-assignment />
|
||||
|
|
@ -275,7 +278,7 @@ function log(data: any) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 mb-8 flex flex-col flex-wrap gap-4 lg:flex-row">
|
||||
<div class="mb-8 mt-8 flex flex-col flex-wrap gap-4 lg:flex-row">
|
||||
<div class="inline-flex flex-col">
|
||||
smiley-happy
|
||||
<it-icon-smiley-happy />
|
||||
|
|
@ -292,7 +295,7 @@ function log(data: any) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 mb-8 flex flex-col flex-wrap gap-4 lg:flex-row">
|
||||
<div class="mb-8 mt-8 flex flex-col flex-wrap gap-4 lg:flex-row">
|
||||
<div class="inline-flex flex-col text-orange-500">
|
||||
message big
|
||||
<it-icon-message class="h-16 w-16 text-orange-500" />
|
||||
|
|
@ -361,7 +364,7 @@ function log(data: any) {
|
|||
<h2 class="heading-1">Components</h2>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-8 mb-8">Buttons</h2>
|
||||
<h2 class="mb-8 mt-8">Buttons</h2>
|
||||
|
||||
<div class="mb-16 flex flex-col flex-wrap content-center gap-4 lg:flex-row">
|
||||
<button class="btn-primary">Primary</button>
|
||||
|
|
@ -394,7 +397,7 @@ function log(data: any) {
|
|||
|
||||
<button type="button" class="btn-primary inline-flex items-center px-3 py-2">
|
||||
Button text
|
||||
<it-icon-message class="ml-3 -mr-1 h-5 w-5"></it-icon-message>
|
||||
<it-icon-message class="-mr-1 ml-3 h-5 w-5"></it-icon-message>
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn-text inline-flex items-center px-3 py-2">
|
||||
|
|
@ -403,7 +406,7 @@ function log(data: any) {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-8 mb-8">Dropdown (Work-in-progress)</h2>
|
||||
<h2 class="mb-8 mt-8">Dropdown (Work-in-progress)</h2>
|
||||
|
||||
<ItDropdownSelect
|
||||
v-model="state.dropdownSelected"
|
||||
|
|
@ -412,10 +415,10 @@ function log(data: any) {
|
|||
></ItDropdownSelect>
|
||||
{{ state.dropdownSelected }}
|
||||
|
||||
<h2 class="mt-8 mb-8">Checkbox</h2>
|
||||
<h2 class="mb-8 mt-8">Checkbox</h2>
|
||||
|
||||
<ItCheckbox
|
||||
:checkbox_item="{
|
||||
:checkbox-item="{
|
||||
subtitle: 'Subtitle',
|
||||
label: 'Label',
|
||||
value: 'value',
|
||||
|
|
@ -430,7 +433,7 @@ function log(data: any) {
|
|||
|
||||
<ItCheckbox
|
||||
:disabled="true"
|
||||
:checkbox_item="{
|
||||
:checkbox-item="{
|
||||
subtitle: 'checked disabled',
|
||||
label: 'Label',
|
||||
value: 'value',
|
||||
|
|
@ -441,7 +444,7 @@ function log(data: any) {
|
|||
Disabled
|
||||
</ItCheckbox>
|
||||
|
||||
<h2 class="mt-8 mb-8">Checkbox Group</h2>
|
||||
<h2 class="mb-8 mt-8">Checkbox Group</h2>
|
||||
|
||||
<ItCheckboxGroup
|
||||
label="Label"
|
||||
|
|
@ -466,7 +469,7 @@ function log(data: any) {
|
|||
]"
|
||||
/>
|
||||
|
||||
<h2 class="mt-8 mb-8">Dropdown</h2>
|
||||
<h2 class="mb-8 mt-8">Dropdown</h2>
|
||||
|
||||
<div class="h-60">
|
||||
<ItDropdown
|
||||
|
|
@ -500,7 +503,7 @@ function log(data: any) {
|
|||
<HorizontalBarChart title="Frage X" text="Fragentext" :items="barChartItems" />
|
||||
</div>
|
||||
|
||||
<h2 class="mt-8 mb-8">LearningPathCircle</h2>
|
||||
<h2 class="mb-8 mt-8">LearningPathCircle</h2>
|
||||
|
||||
<LearningPathCircle
|
||||
class="h-48 w-48"
|
||||
|
|
@ -520,11 +523,11 @@ function log(data: any) {
|
|||
]"
|
||||
></LearningPathCircle>
|
||||
|
||||
<h2 class="mt-8 mb-8">ItToggleSwitch</h2>
|
||||
<h2 class="mb-8 mt-8">ItToggleSwitch</h2>
|
||||
|
||||
<ItToggleSwitch></ItToggleSwitch>
|
||||
|
||||
<h2 class="mt-8 mb-8">ItNavigationProgress</h2>
|
||||
<h2 class="mb-8 mt-8">ItNavigationProgress</h2>
|
||||
|
||||
<ItNavigationProgress
|
||||
:steps="4"
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ function userCountStatusForCircle(userId: number, translationKey: string) {
|
|||
);
|
||||
const grouped = groupBy(criteria, "circle.translation_key");
|
||||
|
||||
return competenceStore.calcStatusCount(grouped[translationKey]);
|
||||
return competenceStore.calcStatusCount(grouped[translationKey] as []);
|
||||
}
|
||||
|
||||
const circles = computed(() => {
|
||||
|
|
@ -131,8 +131,8 @@ function setActiveClasses(translationKey: string) {
|
|||
learningPathStore.learningPathForUser(props.courseSlug, userStore.id)
|
||||
?.circles || []
|
||||
"
|
||||
:course-id="courseSessionStore.courseSessionForRoute?.course.id || 0"
|
||||
:url="courseSessionStore.courseSessionForRoute?.course_url || ''"
|
||||
:course-id="courseSessionStore.currentCourseSession?.course.id || 0"
|
||||
:url="courseSessionStore.currentCourseSession?.course_url || ''"
|
||||
></FeedbackSummary>
|
||||
<div>
|
||||
<!-- progress -->
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import CirclePage from "@/pages/learningPath/CirclePage.vue";
|
||||
import CirclePage from "@/pages/learningPath/circlePage/CirclePage.vue";
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import * as log from "loglevel";
|
||||
import { computed, onMounted } from "vue";
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ function setActiveClasses(isActive: boolean) {
|
|||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<div v-if="learningPath" class="mb-8 w-full bg-white pt-8 pb-4">
|
||||
<div v-if="learningPath" class="mb-8 w-full bg-white pb-4 pt-8">
|
||||
<LearningPathDiagram
|
||||
class="mx-auto max-h-[90px] w-full max-w-[1920px] px-4 lg:max-h-[380px]"
|
||||
diagram-type="horizontal"
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ const feedbackData = reactive<FeedbackData>({ amount: 0, questions: {} });
|
|||
onMounted(async () => {
|
||||
log.debug("FeedbackPage mounted");
|
||||
const data = await itGet(
|
||||
`/api/core/feedback/${courseSessionStore.courseSessionForRoute?.course.id}/${props.circleId}`
|
||||
`/api/core/feedback/${courseSessionStore.currentCourseSession?.course.id}/${props.circleId}`
|
||||
);
|
||||
Object.assign(feedbackData, data);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ const props = defineProps<{
|
|||
courseSlug: string;
|
||||
}>();
|
||||
|
||||
log.debug("CompetenceIndexPage created", props);
|
||||
|
||||
const competenceStore = useCompetenceStore();
|
||||
|
||||
const failedCriteria = computed(() => {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ import type { Ref } from "vue";
|
|||
import { computed, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
log.debug("PerformanceCriteriaPage created");
|
||||
|
||||
const props = defineProps<{
|
||||
courseSlug: string;
|
||||
}>();
|
||||
|
|
@ -20,6 +18,8 @@ interface MenuItem {
|
|||
iconName: string;
|
||||
}
|
||||
|
||||
log.debug("PerformanceCriteriaPage created", props);
|
||||
|
||||
const competenceStore = useCompetenceStore();
|
||||
|
||||
const shownCriteria = computed(() => {
|
||||
|
|
@ -93,7 +93,7 @@ function updateActiveState(status: CourseCompletionStatus) {
|
|||
'bg-gray-200': activeMenuItem.id === item.id,
|
||||
'mr-6': item.id !== 'unknown',
|
||||
}"
|
||||
class="mr-6 inline-block py-4 px-2"
|
||||
class="mr-6 inline-block px-2 py-4"
|
||||
@click="updateActiveState(item.id)"
|
||||
>
|
||||
<div class="flex flex-row items-center">
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import { useCompetenceStore } from "@/stores/competence";
|
||||
import type { CompetencePage, PerformanceCriteria } from "@/types";
|
||||
import * as log from "loglevel";
|
||||
|
||||
import LearningContentContainer from "@/components/learningPath/LearningContentContainer.vue";
|
||||
import LearningContentContainer from "@/pages/learningPath/learningContentPage/LearningContentContainer.vue";
|
||||
import { computed } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSlug: string;
|
||||
criteriaSlug: string;
|
||||
}>();
|
||||
|
||||
log.debug("SinglePerformanceCriteriaPage.vue setup");
|
||||
|
||||
const competenceStore = useCompetenceStore();
|
||||
|
|
@ -15,68 +20,40 @@ const circleStore = useCircleStore();
|
|||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
let currentQuestion: PerformanceCriteria | undefined;
|
||||
let competencePage: CompetencePage | undefined;
|
||||
const singleCriteria = computed(() => {
|
||||
return competenceStore.flatPerformanceCriteria().find((criteria) => {
|
||||
return criteria.slug === props.criteriaSlug;
|
||||
});
|
||||
});
|
||||
|
||||
const findCriteria = () => {
|
||||
for (const page of competenceStore.competenceProfilePage()
|
||||
?.children as CompetencePage[]) {
|
||||
for (const criteria of page.children) {
|
||||
if (criteria.slug === route.params["criteriaSlug"]) {
|
||||
currentQuestion = criteria;
|
||||
competencePage = page;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (competencePage) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
findCriteria();
|
||||
// onMounted(() => {
|
||||
// console.log(route.params)
|
||||
// });
|
||||
|
||||
// const questions = computed(() => props.learningUnit?.children);
|
||||
// const currentQuestion = computed(() => questions.value[state.questionIndex]);
|
||||
//
|
||||
// function handleContinue() {
|
||||
// log.debug("handleContinue");
|
||||
// if (state.questionIndex + 1 < questions.value.length) {
|
||||
// log.debug("increment questionIndex", state.questionIndex);
|
||||
// state.questionIndex += 1;
|
||||
// } else {
|
||||
// log.debug("continue to next learning content");
|
||||
// circleStore.continueFromSelfEvaluation(props.learningUnit);
|
||||
// }
|
||||
// }
|
||||
function close() {
|
||||
router.push(`/course/${props.courseSlug}/competence`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="competencePage" class="absolute top-0 bottom-0 w-full bg-white">
|
||||
<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()"
|
||||
>
|
||||
<div v-if="currentQuestion" class="container-medium">
|
||||
<div v-if="singleCriteria" class="container-medium">
|
||||
<div class="mt-4 border p-6 lg:mt-8 lg:p-12">
|
||||
<h2 class="heading-2">
|
||||
{{ currentQuestion.competence_id }} {{ currentQuestion.title }}
|
||||
{{ singleCriteria.competence_id }} {{ singleCriteria.title }}
|
||||
</h2>
|
||||
|
||||
<div class="mt-4 flex flex-col justify-between gap-6 lg:mt-8 lg:flex-row">
|
||||
<button
|
||||
class="inline-flex flex-1 items-center border p-4 text-left"
|
||||
:class="{
|
||||
'border-green-500': currentQuestion.completion_status === 'success',
|
||||
'border-2': currentQuestion.completion_status === 'success',
|
||||
'border-green-500': singleCriteria.completion_status === 'success',
|
||||
'border-2': singleCriteria.completion_status === 'success',
|
||||
}"
|
||||
data-cy="success"
|
||||
@click="circleStore.markCompletion(currentQuestion, 'success')"
|
||||
@click="circleStore.markCompletion(singleCriteria, '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>
|
||||
|
|
@ -84,11 +61,11 @@ findCriteria();
|
|||
<button
|
||||
class="inline-flex flex-1 items-center border p-4 text-left"
|
||||
:class="{
|
||||
'border-orange-500': currentQuestion.completion_status === 'fail',
|
||||
'border-2': currentQuestion.completion_status === 'fail',
|
||||
'border-orange-500': singleCriteria.completion_status === 'fail',
|
||||
'border-2': singleCriteria.completion_status === 'fail',
|
||||
}"
|
||||
data-cy="fail"
|
||||
@click="circleStore.markCompletion(currentQuestion, 'fail')"
|
||||
@click="circleStore.markCompletion(singleCriteria, 'fail')"
|
||||
>
|
||||
<it-icon-smiley-thinking class="mr-4 h-16 w-16"></it-icon-smiley-thinking>
|
||||
<span class="text-xl font-bold">{{ $t("selfEvaluation.no") }}</span>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import { showIcon } from "@/pages/learningPath/circlePage/learningSequenceUtils";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import type { DefaultArcObject } from "d3";
|
||||
import * as d3 from "d3";
|
||||
import pick from "lodash/pick";
|
||||
import * as log from "loglevel";
|
||||
import { computed, onMounted } from "vue";
|
||||
import { showIcon } from "./learningSequenceUtils";
|
||||
|
||||
// @ts-ignore
|
||||
import colors from "@/colors.json";
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import CircleDiagram from "@/components/learningPath/CircleDiagram.vue";
|
||||
import CircleOverview from "@/components/learningPath/CircleOverview.vue";
|
||||
import DocumentUploadForm from "@/components/learningPath/DocumentUploadForm.vue";
|
||||
import LearningSequence from "@/components/learningPath/LearningSequence.vue";
|
||||
import ItModal from "@/components/ui/ItModal.vue";
|
||||
import * as log from "loglevel";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import CircleDiagram from "./CircleDiagram.vue";
|
||||
import CircleOverview from "./CircleOverview.vue";
|
||||
import DocumentUploadForm from "./DocumentUploadForm.vue";
|
||||
import LearningSequence from "./LearningSequence.vue";
|
||||
|
||||
import { uploadCircleDocument } from "@/services/files";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
|
|
@ -112,12 +112,12 @@ async function uploadDocument(data: DocumentUploadData) {
|
|||
isUploading.value = true;
|
||||
showUploadErrorMessage.value = false;
|
||||
try {
|
||||
if (!courseSessionsStore.courseSessionForRoute) {
|
||||
if (!courseSessionsStore.currentCourseSession) {
|
||||
throw new Error("No course session found");
|
||||
}
|
||||
const newDocument = await uploadCircleDocument(
|
||||
data,
|
||||
courseSessionsStore.courseSessionForRoute.id
|
||||
courseSessionsStore.currentCourseSession.id
|
||||
);
|
||||
const courseSessionStore = useCourseSessionsStore();
|
||||
courseSessionStore.addDocument(newDocument);
|
||||
|
|
@ -280,7 +280,7 @@ async function uploadDocument(data: DocumentUploadData) {
|
|||
v-for="expert in courseSessionsStore.circleExperts"
|
||||
:key="expert.user_id"
|
||||
>
|
||||
<div class="mt-2 mb-2 flex flex-row items-center">
|
||||
<div class="mb-2 mt-2 flex flex-row items-center">
|
||||
<img
|
||||
class="mr-2 h-[45px] rounded-full"
|
||||
:src="expert.avatar_url"
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||
import LearningContentBadge from "@/pages/learningPath/LearningContentTypeBadge.vue";
|
||||
import { showIcon } from "@/pages/learningPath/circlePage/learningSequenceUtils";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import type {
|
||||
CourseCompletionStatus,
|
||||
|
|
@ -9,8 +11,6 @@ import type {
|
|||
import { humanizeDuration } from "@/utils/humanizeDuration";
|
||||
import findLast from "lodash/findLast";
|
||||
import { computed } from "vue";
|
||||
import LearningContentBadge from "./LearningContentTypeBadge.vue";
|
||||
import { showIcon } from "./learningSequenceUtils";
|
||||
|
||||
type Props = {
|
||||
learningSequence: LearningSequence;
|
||||
|
|
@ -92,7 +92,7 @@ const learningSequenceBorderClass = computed(() => {
|
|||
<template>
|
||||
<div :id="learningSequence.slug" class="learning-sequence mb-8">
|
||||
<div class="mb-2 flex items-center gap-4 text-blue-900">
|
||||
<component v-if="showIcon(learningSequence.icon)" :is="learningSequence.icon" />
|
||||
<component :is="learningSequence.icon" v-if="showIcon(learningSequence.icon)" />
|
||||
<h3 class="text-large font-semibold">
|
||||
{{ learningSequence.title }}
|
||||
</h3>
|
||||
|
|
@ -127,7 +127,7 @@ const learningSequenceBorderClass = computed(() => {
|
|||
</div>
|
||||
<ItCheckbox
|
||||
v-else
|
||||
:checkbox_item="{
|
||||
:checkbox-item="{
|
||||
value: learningContent.completion_status,
|
||||
checked: learningContent.completion_status === 'success',
|
||||
}"
|
||||
|
|
@ -1,17 +1,19 @@
|
|||
<script setup lang="ts">
|
||||
import LearningContentContainer from "@/components/learningPath/LearningContentContainer.vue";
|
||||
import LearningContentContainer from "@/pages/learningPath/learningContentPage/LearningContentContainer.vue";
|
||||
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 DescriptionBlock from "@/components/learningPath/blocks/DescriptionBlock.vue";
|
||||
import DescriptionTextBlock from "@/components/learningPath/blocks/DescriptionTextBlock.vue";
|
||||
import FeedbackBlock from "@/components/learningPath/blocks/FeedbackBlock.vue";
|
||||
import IframeBlock from "@/components/learningPath/blocks/IframeBlock.vue";
|
||||
import MediaLibraryBlock from "@/components/learningPath/blocks/MediaLibraryBlock.vue";
|
||||
import PlaceholderBlock from "@/components/learningPath/blocks/PlaceholderBlock.vue";
|
||||
import VideoBlock from "@/components/learningPath/blocks/VideoBlock.vue";
|
||||
import AttendanceDayBlock from "@/pages/learningPath/learningContentPage/blocks/AttendanceDayBlock.vue";
|
||||
import DescriptionBlock from "./blocks/DescriptionBlock.vue";
|
||||
import DescriptionTextBlock from "./blocks/DescriptionTextBlock.vue";
|
||||
import FeedbackBlock from "./blocks/FeedbackBlock.vue";
|
||||
import IframeBlock from "./blocks/IframeBlock.vue";
|
||||
import MediaLibraryBlock from "./blocks/MediaLibraryBlock.vue";
|
||||
import PlaceholderBlock from "./blocks/PlaceholderBlock.vue";
|
||||
import VideoBlock from "./blocks/VideoBlock.vue";
|
||||
|
||||
log.debug("LearningContent.vue setup");
|
||||
|
||||
|
|
@ -30,8 +32,7 @@ const block = computed(() => {
|
|||
});
|
||||
|
||||
// can't use the type as component name, as some are reserved HTML components, e.g. video
|
||||
const COMPONENTS: Record<LearningContentType, any> = {
|
||||
// todo: can we find a better type here than any? ^
|
||||
const COMPONENTS: Record<LearningContentType, Component> = {
|
||||
placeholder: PlaceholderBlock,
|
||||
video: VideoBlock,
|
||||
assignment: DescriptionTextBlock,
|
||||
|
|
@ -44,6 +45,7 @@ const COMPONENTS: Record<LearningContentType, any> = {
|
|||
document: DescriptionBlock,
|
||||
media_library: MediaLibraryBlock,
|
||||
online_training: DescriptionBlock,
|
||||
attendance_day: AttendanceDayBlock,
|
||||
};
|
||||
const DEFAULT_BLOCK = DescriptionBlock;
|
||||
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import LearningContentBadge from "@/components/learningPath/LearningContentTypeBadge.vue";
|
||||
import LearningContentBadge from "@/pages/learningPath/LearningContentTypeBadge.vue";
|
||||
import type { LearningContentBlock } from "@/types";
|
||||
import * as log from "loglevel";
|
||||
|
||||
log.debug("LeariningContentContainer.vue setup");
|
||||
log.debug("LearningContentContainer.vue setup");
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
|
|
@ -26,7 +26,7 @@ defineEmits(["next", "exit"]);
|
|||
<div>
|
||||
<div class="h-full"></div>
|
||||
<!-- just here to not make the footer jump during the transition -->
|
||||
<div class="absolute top-0 bottom-0 w-full bg-white">
|
||||
<div class="absolute bottom-0 top-0 w-full bg-white">
|
||||
<div class="h-content overflow-y-scroll">
|
||||
<header
|
||||
class="relative flex h-12 w-full items-center justify-between bg-white px-4 py-4 lg:h-16 lg:px-8"
|
||||
|
|
@ -18,7 +18,7 @@ defineEmits(["back", "continue"]);
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<nav class="mt-16 mb-4 flex">
|
||||
<nav class="mb-4 mt-16 flex">
|
||||
<button
|
||||
v-if="showBackButton"
|
||||
class="btn-secondary mr-2 flex items-center"
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import LearningContent from "@/components/learningPath/LearningContent.vue";
|
||||
import LearningContent from "@/pages/learningPath/learningContentPage/LearningContent.vue";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import type { LearningContent as LearningContentType } from "@/types";
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<script setup lang="ts">
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import type { LearningContent } from "@/types";
|
||||
import { computed } from "vue";
|
||||
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
|
||||
interface Value {
|
||||
description: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
value: Value;
|
||||
content: LearningContent;
|
||||
}>();
|
||||
|
||||
const attendanceDay = computed(() => {
|
||||
return courseSessionsStore.findAttendanceDay(props.content.id);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container-medium">
|
||||
<div class="lg:mt-8">
|
||||
<div class="text-large my-4">
|
||||
<div v-if="attendanceDay">{{ attendanceDay }}</div>
|
||||
<div v-else>Für diese Durchführung existieren noch keine Details</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { RadioItem } from "@/components/learningPath/feedback.types";
|
||||
import type { RadioItem } from "@/pages/learningPath/learningContentPage/feedback/feedback.types";
|
||||
|
||||
export const YES_NO: RadioItem<boolean>[] = [
|
||||
{
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
|
||||
import LearningPathCircle from "@/components/learningPath/page/LearningPathCircle.vue";
|
||||
|
||||
//👇 This default export determines where your story goes in the story list
|
||||
const meta: Meta<typeof LearningPathCircle> = {
|
||||
/* 👇 The title prop is optional.
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import type { CircleSectorData } from "@/components/learningPath/page/LearningPathCircle.vue";
|
||||
import LearningPathCircle from "@/components/learningPath/page/LearningPathCircle.vue";
|
||||
import LearningPathContinueButton from "@/components/learningPath/page/LearningPathContinueButton.vue";
|
||||
import { calculateCircleSectorData } from "@/components/learningPath/page/utils";
|
||||
import type { CircleSectorData } from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
||||
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
||||
import LearningPathContinueButton from "@/pages/learningPath/learningPathPage/LearningPathContinueButton.vue";
|
||||
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
|
||||
import type { Circle } from "@/services/circle";
|
||||
import type { LearningPath } from "@/services/learningPath";
|
||||
import type { Topic } from "@/types";
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import LearningPathCircle from "@/components/learningPath/page/LearningPathCircle.vue";
|
||||
import LearningPathContinueButton from "@/components/learningPath/page/LearningPathContinueButton.vue";
|
||||
import { calculateCircleSectorData } from "@/components/learningPath/page/utils";
|
||||
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
||||
import LearningPathContinueButton from "@/pages/learningPath/learningPathPage/LearningPathContinueButton.vue";
|
||||
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
|
||||
import type { Circle } from "@/services/circle";
|
||||
import type { LearningPath } from "@/services/learningPath";
|
||||
import { onMounted, ref } from "vue";
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import type { CircleSectorData } from "@/components/learningPath/page/LearningPathCircle.vue";
|
||||
import LearningPathCircleListTile from "@/components/learningPath/page/LearningPathCircleListTile.vue";
|
||||
import type { CircleSectorData } from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
||||
import LearningPathCircleListTile from "@/pages/learningPath/learningPathPage/LearningPathCircleListTile.vue";
|
||||
import type { Circle } from "@/services/circle";
|
||||
import type { LearningPath } from "@/services/learningPath";
|
||||
import { computed } from "vue";
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from "loglevel";
|
||||
|
||||
import LearningPathAppointmentsMock from "@/components/learningPath/page/LearningPathAppointmentsMock.vue";
|
||||
import LearningPathListView from "@/components/learningPath/page/LearningPathListView.vue";
|
||||
import LearningPathPathView from "@/components/learningPath/page/LearningPathPathView.vue";
|
||||
import CircleProgress from "@/components/learningPath/page/LearningPathProgress.vue";
|
||||
import LearningPathTopics from "@/components/learningPath/page/LearningPathTopics.vue";
|
||||
import type { ViewType } from "@/components/learningPath/page/LearningPathViewSwitch.vue";
|
||||
import LearningPathViewSwitch from "@/components/learningPath/page/LearningPathViewSwitch.vue";
|
||||
import LearningPathAppointmentsMock from "@/pages/learningPath/learningPathPage/LearningPathAppointmentsMock.vue";
|
||||
import LearningPathListView from "@/pages/learningPath/learningPathPage/LearningPathListView.vue";
|
||||
import LearningPathPathView from "@/pages/learningPath/learningPathPage/LearningPathPathView.vue";
|
||||
import CircleProgress from "@/pages/learningPath/learningPathPage/LearningPathProgress.vue";
|
||||
import LearningPathTopics from "@/pages/learningPath/learningPathPage/LearningPathTopics.vue";
|
||||
import type { ViewType } from "@/pages/learningPath/learningPathPage/LearningPathViewSwitch.vue";
|
||||
import LearningPathViewSwitch from "@/pages/learningPath/learningPathPage/LearningPathViewSwitch.vue";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
|
||||
|
|
@ -38,9 +38,9 @@ onMounted(async () => {
|
|||
});
|
||||
|
||||
const learningPath = computed(() => {
|
||||
if (userStore.loggedIn && learningPathStore.learningPaths.size > 0) {
|
||||
if (userStore.loggedIn && learningPathStore.state.learningPaths.size > 0) {
|
||||
const learningPathKey = `${props.courseSlug}-lp-${userStore.id}`;
|
||||
return learningPathStore.learningPaths.get(learningPathKey);
|
||||
return learningPathStore.state.learningPaths.get(learningPathKey);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import type { CircleSectorData } from "@/components/learningPath/page/LearningPathCircle.vue";
|
||||
import LearningPathCircleColumn from "@/components/learningPath/page/LearningPathCircleColumn.vue";
|
||||
import LearningPathScrollButton from "@/components/learningPath/page/LearningPathScrollButton.vue";
|
||||
import type { CircleSectorData } from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
||||
import LearningPathCircleColumn from "@/pages/learningPath/learningPathPage/LearningPathCircleColumn.vue";
|
||||
import LearningPathScrollButton from "@/pages/learningPath/learningPathPage/LearningPathScrollButton.vue";
|
||||
import type { Circle } from "@/services/circle";
|
||||
import type { LearningPath } from "@/services/learningPath";
|
||||
import { useScroll } from "@vueuse/core";
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import type {
|
||||
CircleSectorData,
|
||||
CircleSectorProgress,
|
||||
} from "@/components/learningPath/page/LearningPathCircle.vue";
|
||||
} from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
||||
import type { Circle } from "@/services/circle";
|
||||
|
||||
export function calculateCircleSectorData(circle: Circle): CircleSectorData[] {
|
||||
|
|
@ -3,11 +3,11 @@ import { useCircleStore } from "@/stores/circle";
|
|||
import type { LearningUnit } from "@/types";
|
||||
import * as log from "loglevel";
|
||||
|
||||
import LearningContentContainer from "@/components/learningPath/LearningContentContainer.vue";
|
||||
import { COMPLETION_FAILURE, COMPLETION_SUCCESS } from "@/constants";
|
||||
import LearningContentContainer from "@/pages/learningPath/learningContentPage/LearningContentContainer.vue";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { computed, reactive } from "vue";
|
||||
import LearningContentNavigation from "./LearningContentNavigation.vue";
|
||||
import LearningContentNavigation from "../learningContentPage/LearningContentNavigation.vue";
|
||||
|
||||
log.debug("LearningContent.vue setup");
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ function handleBack() {
|
|||
<div class="mt-6 lg:mt-12">
|
||||
{{ $t("selfEvaluation.progressText") }}
|
||||
<router-link
|
||||
:to="courseSession.courseSessionForRoute?.competence_url || '/'"
|
||||
:to="courseSession.currentCourseSession?.competence_url || '/'"
|
||||
class="text-primary-500 underline"
|
||||
>
|
||||
{{ $t("selfEvaluation.progressLink") }}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import * as log from "loglevel";
|
||||
|
||||
import SelfEvaluation from "@/components/learningPath/SelfEvaluation.vue";
|
||||
import SelfEvaluation from "@/pages/learningPath/selfEvaluationPage/SelfEvaluation.vue";
|
||||
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
|
|
@ -2,6 +2,7 @@ import DashboardPage from "@/pages/DashboardPage.vue";
|
|||
import LoginPage from "@/pages/LoginPage.vue";
|
||||
import { redirectToLoginIfRequired, updateLoggedIn } from "@/router/guards";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
|
||||
const router = createRouter({
|
||||
|
|
@ -69,6 +70,7 @@ const router = createRouter({
|
|||
},
|
||||
{
|
||||
path: "criteria",
|
||||
props: true,
|
||||
component: () => import("@/pages/competence/PerformanceCriteriaPage.vue"),
|
||||
},
|
||||
{
|
||||
|
|
@ -81,22 +83,25 @@ const router = createRouter({
|
|||
},
|
||||
{
|
||||
path: "/course/:courseSlug/learn",
|
||||
component: () => import("../pages/learningPath/LearningPathPage.vue"),
|
||||
component: () =>
|
||||
import("../pages/learningPath/learningPathPage/LearningPathPage.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/course/:courseSlug/learn/:circleSlug",
|
||||
component: () => import("../pages/learningPath/CirclePage.vue"),
|
||||
component: () => import("../pages/learningPath/circlePage/CirclePage.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/course/:courseSlug/learn/:circleSlug/evaluate/:learningUnitSlug",
|
||||
component: () => import("../pages/learningPath/SelfEvaluationPage.vue"),
|
||||
component: () =>
|
||||
import("../pages/learningPath/selfEvaluationPage/SelfEvaluationPage.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/course/:courseSlug/learn/:circleSlug/:contentSlug",
|
||||
component: () => import("../pages/learningPath/LearningContentPage.vue"),
|
||||
component: () =>
|
||||
import("../pages/learningPath/learningContentPage/LearningContentPage.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
|
|
@ -163,6 +168,15 @@ const router = createRouter({
|
|||
router.beforeEach(updateLoggedIn);
|
||||
router.beforeEach(redirectToLoginIfRequired);
|
||||
|
||||
router.beforeEach((to) => {
|
||||
const courseSessionStore = useCourseSessionsStore();
|
||||
if (to.params.courseSlug) {
|
||||
courseSessionStore._currentCourseSlug = to.params.courseSlug as string;
|
||||
} else {
|
||||
courseSessionStore._currentCourseSlug = "";
|
||||
}
|
||||
});
|
||||
|
||||
router.afterEach(() => {
|
||||
const appStore = useAppStore();
|
||||
appStore.routingFinished = true;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import orderBy from "lodash/orderBy";
|
||||
|
||||
import { Circle } from "@/services/circle";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import type {
|
||||
Course,
|
||||
CourseCompletion,
|
||||
|
|
@ -15,11 +17,16 @@ export interface ContinueData {
|
|||
has_no_progress: boolean;
|
||||
}
|
||||
|
||||
function getLastCompleted(courseId: number, completionData: CourseCompletion[]) {
|
||||
function getLastCompleted(courseSlug: string, completionData: CourseCompletion[]) {
|
||||
if (completionData.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const courseSession = courseSessionsStore.courseSessionForCourse(courseSlug);
|
||||
return orderBy(completionData, ["updated_at"], "desc").find((c: CourseCompletion) => {
|
||||
return (
|
||||
c.completion_status === "success" &&
|
||||
c.course === courseId &&
|
||||
c.course_session === courseSession?.id &&
|
||||
c.page_type === "learnpath.LearningContent"
|
||||
);
|
||||
});
|
||||
|
|
@ -33,7 +40,8 @@ export class LearningPath implements WagtailLearningPath {
|
|||
|
||||
public static fromJson(
|
||||
json: WagtailLearningPath,
|
||||
completionData: CourseCompletion[]
|
||||
completionData: CourseCompletion[],
|
||||
userId: number | undefined
|
||||
): LearningPath {
|
||||
return new LearningPath(
|
||||
json.id,
|
||||
|
|
@ -43,6 +51,7 @@ export class LearningPath implements WagtailLearningPath {
|
|||
json.frontend_url,
|
||||
json.course,
|
||||
json.children,
|
||||
userId,
|
||||
completionData
|
||||
);
|
||||
}
|
||||
|
|
@ -55,6 +64,7 @@ export class LearningPath implements WagtailLearningPath {
|
|||
public readonly frontend_url: string,
|
||||
public readonly course: Course,
|
||||
public children: LearningPathChild[],
|
||||
public userId: number | undefined,
|
||||
completionData?: CourseCompletion[]
|
||||
) {
|
||||
// parse children
|
||||
|
|
@ -72,7 +82,7 @@ export class LearningPath implements WagtailLearningPath {
|
|||
}
|
||||
if (page.type === "learnpath.Circle") {
|
||||
const circle = Circle.fromJson(page, this);
|
||||
if (completionData) {
|
||||
if (completionData && completionData.length > 0) {
|
||||
circle.parseCompletionData(completionData);
|
||||
}
|
||||
if (topic) {
|
||||
|
|
@ -96,11 +106,22 @@ export class LearningPath implements WagtailLearningPath {
|
|||
}
|
||||
}
|
||||
|
||||
public async reloadCompletionData() {
|
||||
const learningPathStore = useLearningPathStore();
|
||||
const completionData = await learningPathStore.loadCourseSessionCompletionData(
|
||||
this.course.slug,
|
||||
this.userId
|
||||
);
|
||||
for (const circle of this.circles) {
|
||||
circle.parseCompletionData(completionData);
|
||||
}
|
||||
}
|
||||
|
||||
public calcNextLearningContent(completionData: CourseCompletion[]): void {
|
||||
this.nextLearningContent = undefined;
|
||||
|
||||
const lastCompletedLearningContent = getLastCompleted(
|
||||
this.course.id,
|
||||
this.course.slug,
|
||||
completionData
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { itGetCached, itPost } from "@/fetchHelpers";
|
||||
import type { CourseSession } from "@/types";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { beforeEach, describe, expect, vi } from "vitest";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useCourseSessionsStore } from "../courseSessions";
|
||||
import { useUserStore } from "../user";
|
||||
|
||||
|
|
@ -53,38 +51,44 @@ describe("CourseSession Store", () => {
|
|||
competence_url: "/course/test-course/competence/",
|
||||
course_url: "/course/test-course/",
|
||||
media_library_url: "/course/test-course/media/",
|
||||
attendance_days: [],
|
||||
additional_json_data: {},
|
||||
documents: [],
|
||||
users: [],
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
it("normal user has no cockpit", () => {
|
||||
const userStore = useUserStore();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
userStore.$patch(user);
|
||||
courseSessionsStore.$patch({ courseSessions });
|
||||
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
courseSessionsStore._currentCourseSlug = "test-course";
|
||||
courseSessionsStore.allCourseSessions = courseSessions;
|
||||
|
||||
expect(courseSessionsStore.hasCockpit).toBeFalsy();
|
||||
});
|
||||
|
||||
it("superuser has cockpit", () => {
|
||||
const userStore = useUserStore();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
userStore.$patch(Object.assign(user, { is_superuser: true }));
|
||||
courseSessionsStore.$patch({ courseSessions });
|
||||
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
courseSessionsStore._currentCourseSlug = "test-course";
|
||||
courseSessionsStore.allCourseSessions = courseSessions;
|
||||
|
||||
expect(courseSessionsStore.hasCockpit).toBeTruthy();
|
||||
});
|
||||
|
||||
it("expert has cockpit", () => {
|
||||
const userStore = useUserStore();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
userStore.$patch(
|
||||
Object.assign(user, { course_session_experts: [courseSessions[0].id] })
|
||||
);
|
||||
courseSessionsStore.$patch({ courseSessions });
|
||||
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
courseSessionsStore._currentCourseSlug = "test-course";
|
||||
courseSessionsStore.allCourseSessions = courseSessions;
|
||||
|
||||
expect(courseSessionsStore.hasCockpit).toBeTruthy();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export type AppState = {
|
|||
userLoaded: boolean;
|
||||
routingFinished: boolean;
|
||||
showMainNavigationBar: boolean;
|
||||
currentCourseSlug: string;
|
||||
};
|
||||
|
||||
const showMainNavigationBarInitialState = () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { itGetCached } from "@/fetchHelpers";
|
||||
import { useCompletionStore } from "@/stores/completion";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type {
|
||||
CircleLight,
|
||||
|
|
@ -164,29 +165,34 @@ export const useCompetenceStore = defineStore({
|
|||
const competenceProfilePage = this.competenceProfilePages.get(userId);
|
||||
if (competenceProfilePage) {
|
||||
const completionStore = useCompletionStore();
|
||||
const completionData = await completionStore.loadCompletionData(
|
||||
competenceProfilePage.course.id,
|
||||
userId
|
||||
);
|
||||
|
||||
if (completionData) {
|
||||
competenceProfilePage.children.forEach((competence) => {
|
||||
competence.children.forEach((performanceCriteria) => {
|
||||
const completion = completionData.find(
|
||||
(c) => c.page_key === performanceCriteria.translation_key
|
||||
);
|
||||
if (completion) {
|
||||
performanceCriteria.completion_status = completion.completion_status;
|
||||
performanceCriteria.completion_status_updated_at =
|
||||
completion.updated_at;
|
||||
} else {
|
||||
performanceCriteria.completion_status = "unknown";
|
||||
performanceCriteria.completion_status_updated_at = "";
|
||||
}
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const courseSession = courseSessionsStore.currentCourseSession;
|
||||
if (courseSession) {
|
||||
const completionData = await completionStore.loadCourseSessionCompletionData(
|
||||
courseSession.id,
|
||||
userId
|
||||
);
|
||||
|
||||
if (completionData) {
|
||||
competenceProfilePage.children.forEach((competence) => {
|
||||
competence.children.forEach((performanceCriteria) => {
|
||||
const completion = completionData.find(
|
||||
(c) => c.page_key === performanceCriteria.translation_key
|
||||
);
|
||||
if (completion) {
|
||||
performanceCriteria.completion_status = completion.completion_status;
|
||||
performanceCriteria.completion_status_updated_at =
|
||||
completion.updated_at;
|
||||
} else {
|
||||
performanceCriteria.completion_status = "unknown";
|
||||
performanceCriteria.completion_status_updated_at = "";
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.competenceProfilePages.set(userId, competenceProfilePage);
|
||||
this.competenceProfilePages.set(userId, competenceProfilePage);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { bustItGetCache, itGetCached, itPost } from "@/fetchHelpers";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type { BaseCourseWagtailPage, CourseCompletion } from "@/types";
|
||||
import { defineStore } from "pinia";
|
||||
|
|
@ -10,37 +11,55 @@ export const useCompletionStore = defineStore({
|
|||
},
|
||||
getters: {},
|
||||
actions: {
|
||||
async loadCompletionData(courseId: number, userId: number, reload = false) {
|
||||
async loadCourseSessionCompletionData(
|
||||
courseSessionId: number,
|
||||
userId: number,
|
||||
reload = false
|
||||
) {
|
||||
const userCompletionData = (await itGetCached(
|
||||
`/api/course/completion/${courseId}/${userId}/`,
|
||||
`/api/course/completion/${courseSessionId}/${userId}/`,
|
||||
{
|
||||
reload: reload,
|
||||
}
|
||||
)) as CourseCompletion[];
|
||||
|
||||
if (userCompletionData === undefined) {
|
||||
throw `No completionData found with: ${courseId}, ${userId}`;
|
||||
throw `No completionData found with: ${courseSessionId}, ${userId}`;
|
||||
}
|
||||
return userCompletionData || [];
|
||||
},
|
||||
async markPage(
|
||||
page: BaseCourseWagtailPage,
|
||||
userId: number | undefined = undefined
|
||||
userId: number | undefined = undefined,
|
||||
courseSessionId: number | undefined = undefined
|
||||
) {
|
||||
const completionData = await itPost("/api/course/completion/mark/", {
|
||||
page_key: page.translation_key,
|
||||
completion_status: page.completion_status,
|
||||
});
|
||||
|
||||
if (completionData && completionData.length > 0) {
|
||||
if (!userId) {
|
||||
const userStore = useUserStore();
|
||||
userId = userStore.id;
|
||||
if (!courseSessionId) {
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const courseSession = courseSessionsStore.currentCourseSession;
|
||||
if (courseSession) {
|
||||
courseSessionId = courseSession.id;
|
||||
}
|
||||
bustItGetCache(`/api/course/completion/${completionData[0].course}/${userId}/`);
|
||||
}
|
||||
|
||||
return completionData as CourseCompletion[];
|
||||
if (courseSessionId) {
|
||||
const completionData = await itPost("/api/course/completion/mark/", {
|
||||
page_key: page.translation_key,
|
||||
completion_status: page.completion_status,
|
||||
course_session_id: courseSessionId,
|
||||
});
|
||||
|
||||
if (completionData && completionData.length > 0) {
|
||||
if (!userId) {
|
||||
const userStore = useUserStore();
|
||||
userId = userStore.id;
|
||||
}
|
||||
bustItGetCache(`/api/course/completion/${courseSessionId}/${userId}/`);
|
||||
}
|
||||
|
||||
return completionData as CourseCompletion[];
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,27 +3,27 @@ import { deleteCircleDocument } from "@/services/files";
|
|||
import type {
|
||||
CircleDocument,
|
||||
CourseSession,
|
||||
CourseSessionAttendanceDay,
|
||||
CourseSessionUser,
|
||||
ExpertSessionUser,
|
||||
} from "@/types";
|
||||
import eventBus from "@/utils/eventBus";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import uniqBy from "lodash/uniqBy";
|
||||
import log from "loglevel";
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useCircleStore } from "./circle";
|
||||
import { useUserStore } from "./user";
|
||||
|
||||
export type CourseSessionsStoreState = {
|
||||
courseSessions: CourseSession[] | undefined;
|
||||
};
|
||||
|
||||
export type LearningSequenceCircleDocument = {
|
||||
id: number;
|
||||
title: string;
|
||||
documents: CircleDocument[];
|
||||
};
|
||||
|
||||
const SELECTED_COURSE_SESSIONS_KEY = "selectedCourseSessionMap";
|
||||
|
||||
function loadCourseSessionsData(reload = false) {
|
||||
log.debug("loadCourseSessionsData called");
|
||||
const courseSessions = ref<CourseSession[]>([]);
|
||||
|
|
@ -57,7 +57,7 @@ function loadCourseSessionsData(reload = false) {
|
|||
loadAndUpdate(); // this will be called asynchronously, but does not block
|
||||
|
||||
// returns the empty sessions array at first, then after loading populates the ref
|
||||
return { courseSessions };
|
||||
return { allCourseSessions: courseSessions };
|
||||
}
|
||||
|
||||
export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
||||
|
|
@ -67,29 +67,86 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
|||
|
||||
// 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
|
||||
const { courseSessions } = loadCourseSessionsData();
|
||||
|
||||
// these will become getters
|
||||
const coursesFromCourseSessions = computed(() =>
|
||||
// TODO: refactor after implementing of Klassenkonzept
|
||||
uniqBy(courseSessions.value, "course.id")
|
||||
const { allCourseSessions } = loadCourseSessionsData();
|
||||
const selectedCourseSessionMap = useLocalStorage(
|
||||
SELECTED_COURSE_SESSIONS_KEY,
|
||||
new Map<string, number>()
|
||||
);
|
||||
|
||||
const courseSessionForRoute = computed(() => {
|
||||
const route = useRoute();
|
||||
const routePath = decodeURI(route.path);
|
||||
const courseSlug = routePath.split("/")[2];
|
||||
const _currentCourseSlug = ref("");
|
||||
|
||||
return courseSessions.value.find((cs) => {
|
||||
const uniqueCourseSessionsByCourse = computed(() =>
|
||||
// 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
|
||||
uniqBy(allCourseSessions.value, "course.id")
|
||||
);
|
||||
|
||||
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 {
|
||||
const courseSessionId = selectedCourseSessionMap.value.get(courseSlug);
|
||||
if (courseSessionId) {
|
||||
return allCourseSessions.value.find((cs) => {
|
||||
return cs.id === courseSessionId;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
log.error("Error while parsing courseSessions from localStorage", e);
|
||||
}
|
||||
|
||||
// Keine Durchführung ausgewählt im localStorage
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function switchCourseSession(courseSession: CourseSession) {
|
||||
log.debug("switchCourseSession", courseSession);
|
||||
selectedCourseSessionMap.value.set(courseSession.course.slug, courseSession.id);
|
||||
// FIXME: clean up with VBV-305
|
||||
eventBus.emit("switchedCourseSession", courseSession.id);
|
||||
}
|
||||
|
||||
function courseSessionForCourse(courseSlug: string) {
|
||||
if (courseSlug) {
|
||||
const courseSession = selectedCourseSessionForCourse(courseSlug);
|
||||
if (courseSession) {
|
||||
return courseSession;
|
||||
} else {
|
||||
// return first if there is no selected courseSession
|
||||
return allCourseSessions.value.find((cs) => {
|
||||
return cs.course.slug === courseSlug;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function allCourseSessionsForCourse(courseSlug: string) {
|
||||
return allCourseSessions.value.filter((cs) => {
|
||||
return cs.course.slug === courseSlug;
|
||||
});
|
||||
}
|
||||
|
||||
const currentCourseSession = computed(() => {
|
||||
return courseSessionForCourse(_currentCourseSlug.value);
|
||||
});
|
||||
|
||||
const allCurrentCourseSessions = computed(() => {
|
||||
return allCourseSessionsForCourse(_currentCourseSlug.value);
|
||||
});
|
||||
|
||||
const hasCockpit = computed(() => {
|
||||
if (courseSessionForRoute.value) {
|
||||
if (currentCourseSession.value) {
|
||||
const userStore = useUserStore();
|
||||
return (
|
||||
userStore.course_session_experts.includes(courseSessionForRoute.value.id) ||
|
||||
userStore.course_session_experts.includes(currentCourseSession.value.id) ||
|
||||
userStore.is_superuser
|
||||
);
|
||||
}
|
||||
|
|
@ -101,8 +158,8 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
|||
const circleStore = useCircleStore();
|
||||
const circleTranslationKey = circleStore.circle?.translation_key;
|
||||
|
||||
if (courseSessionForRoute.value && circleTranslationKey) {
|
||||
return courseSessionForRoute.value.users.filter((u) => {
|
||||
if (currentCourseSession.value && circleTranslationKey) {
|
||||
return currentCourseSession.value.users.filter((u) => {
|
||||
if (u.role === "EXPERT") {
|
||||
return (u as ExpertSessionUser).circles
|
||||
.map((c) => c.translation_key)
|
||||
|
|
@ -127,11 +184,11 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
|||
return circleStore.circle?.learningSequences
|
||||
.map((ls) => ({ id: ls.id, title: ls.title, documents: [] }))
|
||||
.map((ls: { id: number; title: string; documents: CircleDocument[] }) => {
|
||||
if (courseSessionForRoute.value === undefined) {
|
||||
if (currentCourseSession.value === undefined) {
|
||||
return ls;
|
||||
}
|
||||
|
||||
for (const document of courseSessionForRoute.value.documents) {
|
||||
for (const document of currentCourseSession.value.documents) {
|
||||
if (document.learning_sequence === ls.id) {
|
||||
ls.documents.push(document);
|
||||
}
|
||||
|
|
@ -142,12 +199,12 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
|||
});
|
||||
|
||||
function addDocument(document: CircleDocument) {
|
||||
courseSessionForRoute.value?.documents.push(document);
|
||||
currentCourseSession.value?.documents.push(document);
|
||||
}
|
||||
|
||||
async function startUpload() {
|
||||
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_name: "test.png",
|
||||
});
|
||||
|
|
@ -156,18 +213,31 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
|||
async function removeDocument(documentId: number) {
|
||||
await deleteCircleDocument(documentId);
|
||||
|
||||
if (courseSessionForRoute.value === undefined) {
|
||||
if (currentCourseSession.value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
courseSessionForRoute.value.documents =
|
||||
courseSessionForRoute.value?.documents.filter((d) => d.id !== documentId);
|
||||
currentCourseSession.value.documents = currentCourseSession.value?.documents.filter(
|
||||
(d) => d.id !== documentId
|
||||
);
|
||||
}
|
||||
|
||||
function findAttendanceDay(
|
||||
contentId: number
|
||||
): CourseSessionAttendanceDay | undefined {
|
||||
if (currentCourseSession.value) {
|
||||
return currentCourseSession.value.attendance_days.find(
|
||||
(attendanceDay) => attendanceDay.learningContentId === contentId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
courseSessions,
|
||||
coursesFromCourseSessions,
|
||||
courseSessionForRoute,
|
||||
uniqueCourseSessionsByCourse,
|
||||
currentCourseSession,
|
||||
allCurrentCourseSessions,
|
||||
courseSessionForCourse,
|
||||
switchCourseSession,
|
||||
hasCockpit,
|
||||
canUploadCircleDocuments,
|
||||
circleDocuments,
|
||||
|
|
@ -175,5 +245,12 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
|||
addDocument,
|
||||
startUpload,
|
||||
removeDocument,
|
||||
findAttendanceDay,
|
||||
|
||||
// only used so that `router.afterEach` can switch it
|
||||
_currentCourseSlug,
|
||||
|
||||
// only used for unit testing
|
||||
allCourseSessions,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
import { itGetCached } from "@/fetchHelpers";
|
||||
import { LearningPath } from "@/services/learningPath";
|
||||
import { useCompletionStore } from "@/stores/completion";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type { CourseCompletion } from "@/types";
|
||||
import eventBus from "@/utils/eventBus";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import log from "loglevel";
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, reactive } from "vue";
|
||||
|
||||
export type LearningPathStoreState = {
|
||||
learningPaths: Map<string, LearningPath>;
|
||||
|
||||
page: "INDEX" | "OVERVIEW";
|
||||
};
|
||||
|
||||
|
|
@ -20,62 +24,98 @@ function getLearningPathKey(
|
|||
return `${slug}-${userId}`;
|
||||
}
|
||||
|
||||
export const useLearningPathStore = defineStore({
|
||||
id: "learningPath",
|
||||
state: () => {
|
||||
return {
|
||||
learningPaths: new Map<LearningPathKey, LearningPath>(),
|
||||
page: "INDEX",
|
||||
loading: false,
|
||||
} as LearningPathStoreState;
|
||||
},
|
||||
getters: {
|
||||
learningPathForUser: (state) => {
|
||||
return (courseSlug: string, userId: string | number | undefined) => {
|
||||
if (state.learningPaths.size > 0) {
|
||||
const learningPathKey = getLearningPathKey(`${courseSlug}-lp`, userId);
|
||||
return state.learningPaths.get(learningPathKey);
|
||||
}
|
||||
export const useLearningPathStore = defineStore("learningPath", () => {
|
||||
const state: LearningPathStoreState = reactive({
|
||||
learningPaths: new Map<LearningPathKey, LearningPath>(),
|
||||
page: "INDEX",
|
||||
});
|
||||
|
||||
return undefined;
|
||||
};
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async loadLearningPath(
|
||||
slug: string,
|
||||
userId: number | undefined = undefined,
|
||||
reload = false,
|
||||
fail = true
|
||||
) {
|
||||
if (!userId) {
|
||||
const userStore = useUserStore();
|
||||
userId = userStore.id;
|
||||
const learningPathForUser = computed(() => {
|
||||
return (courseSlug: string, userId: string | number | undefined) => {
|
||||
if (state.learningPaths.size > 0) {
|
||||
const learningPathKey = getLearningPathKey(`${courseSlug}-lp`, userId);
|
||||
return state.learningPaths.get(learningPathKey);
|
||||
}
|
||||
|
||||
const key = getLearningPathKey(slug, userId);
|
||||
return undefined;
|
||||
};
|
||||
});
|
||||
|
||||
if (this.learningPaths.has(key) && !reload) {
|
||||
return this.learningPaths.get(key);
|
||||
async function loadCourseSessionCompletionData(
|
||||
courseSlug: string,
|
||||
userId: number | undefined = undefined
|
||||
) {
|
||||
// FIXME: should not be here anymore with VBV-305
|
||||
const completionStore = useCompletionStore();
|
||||
|
||||
let completionData: CourseCompletion[] = [];
|
||||
if (userId) {
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const courseSession = courseSessionsStore.courseSessionForCourse(courseSlug);
|
||||
if (courseSession) {
|
||||
completionData = await completionStore.loadCourseSessionCompletionData(
|
||||
courseSession.id,
|
||||
userId
|
||||
);
|
||||
return completionData;
|
||||
}
|
||||
}
|
||||
|
||||
const learningPathData = await itGetCached(`/api/course/page/${slug}/`);
|
||||
if (!learningPathData && fail) {
|
||||
throw `No learning path found with: ${slug}`;
|
||||
return [];
|
||||
}
|
||||
|
||||
async function loadLearningPath(
|
||||
slug: string,
|
||||
userId: number | undefined = undefined,
|
||||
reload = false,
|
||||
fail = true
|
||||
) {
|
||||
if (!userId) {
|
||||
const userStore = useUserStore();
|
||||
userId = userStore.id;
|
||||
}
|
||||
|
||||
const key = getLearningPathKey(slug, userId);
|
||||
|
||||
if (state.learningPaths.has(key) && !reload) {
|
||||
return state.learningPaths.get(key);
|
||||
}
|
||||
|
||||
const learningPathData = await itGetCached(`/api/course/page/${slug}/`);
|
||||
if (!learningPathData && fail) {
|
||||
throw `No learning path found with: ${slug}`;
|
||||
}
|
||||
|
||||
const completionData = await loadCourseSessionCompletionData(
|
||||
learningPathData.course.slug,
|
||||
userId
|
||||
);
|
||||
|
||||
const learningPath = LearningPath.fromJson(
|
||||
cloneDeep(learningPathData),
|
||||
completionData,
|
||||
userId
|
||||
);
|
||||
state.learningPaths.set(key, learningPath);
|
||||
return learningPath;
|
||||
}
|
||||
|
||||
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) => {
|
||||
if (lp.userId) {
|
||||
lp.reloadCompletionData();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const completionStore = useCompletionStore();
|
||||
const completionData = await completionStore.loadCompletionData(
|
||||
learningPathData.course.id,
|
||||
userId
|
||||
);
|
||||
|
||||
const learningPath = LearningPath.fromJson(
|
||||
cloneDeep(learningPathData),
|
||||
completionData
|
||||
);
|
||||
this.learningPaths.set(key, learningPath);
|
||||
return learningPath;
|
||||
},
|
||||
},
|
||||
return {
|
||||
state,
|
||||
learningPathForUser,
|
||||
loadCourseSessionCompletionData,
|
||||
loadLearningPath,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue