diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml
index 6c25695a..716cc9c6 100644
--- a/bitbucket-pipelines.yml
+++ b/bitbucket-pipelines.yml
@@ -19,6 +19,7 @@ e2e: &e2e
- source ./env/bitbucket/prepare_for_test.sh
- npm run build
- source vbvvenv/bin/activate
+ - pip install -r server/requirements/requirements-dev.txt
- ./prepare_server_cypress.sh --start-background
- npm run cypress:ci
artifacts:
@@ -28,7 +29,7 @@ e2e: &e2e
pipelines:
default:
- step:
- name: install cypress dependencies
+ name: install dependencies
services:
- postgres
caches:
@@ -75,7 +76,7 @@ pipelines:
- python -m venv vbvvenv
- source vbvvenv/bin/activate
- pip install -r server/requirements/requirements-dev.txt
- - git-crypt status -e | sort > git-crypt-encrypted-files-check.txt && diff git-crypt-encrypted-files.txt git-crypt-encrypted-files-check.txt
+ - git-crypt status -e | sort > git-crypt-encrypted-files-check.txt && diff -w git-crypt-encrypted-files.txt git-crypt-encrypted-files-check.txt
- trufflehog --exclude_paths trufflehog-exclude-patterns.txt --allow trufflehog-allow.json --entropy=True --max_depth=100 .
- ufmt check server
- step:
diff --git a/client/package-lock.json b/client/package-lock.json
index 070a6e83..e2df688f 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -14,6 +14,7 @@
"@sentry/vue": "^7.20.0",
"@urql/vue": "^1.0.2",
"d3": "^7.6.1",
+ "dayjs": "^1.11.7",
"graphql": "^16.6.0",
"lodash": "^4.17.21",
"loglevel": "^1.8.0",
@@ -5467,6 +5468,11 @@
"integrity": "sha512-qTcEYLen3r7ojZNgVUaRggOI+KM7jrKxXeSHhogh/TWxYMeONEMqY+hmkobiYQozsGIyg9OYVzO4ZIfoB4I0pQ==",
"dev": true
},
+ "node_modules/dayjs": {
+ "version": "1.11.7",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz",
+ "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ=="
+ },
"node_modules/de-indent": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
@@ -15934,6 +15940,11 @@
"integrity": "sha512-qTcEYLen3r7ojZNgVUaRggOI+KM7jrKxXeSHhogh/TWxYMeONEMqY+hmkobiYQozsGIyg9OYVzO4ZIfoB4I0pQ==",
"dev": true
},
+ "dayjs": {
+ "version": "1.11.7",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz",
+ "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ=="
+ },
"de-indent": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
diff --git a/client/package.json b/client/package.json
index 23e695e8..4d8a7a34 100644
--- a/client/package.json
+++ b/client/package.json
@@ -21,6 +21,7 @@
"@sentry/vue": "^7.20.0",
"@urql/vue": "^1.0.2",
"d3": "^7.6.1",
+ "dayjs": "^1.11.7",
"graphql": "^16.6.0",
"lodash": "^4.17.21",
"loglevel": "^1.8.0",
diff --git a/client/src/components/AppFooter.vue b/client/src/components/AppFooter.vue
index 96c68c37..19f67a71 100644
--- a/client/src/components/AppFooter.vue
+++ b/client/src/components/AppFooter.vue
@@ -1,13 +1,15 @@
@@ -22,9 +24,16 @@ async function changeLocale(event: Event) {
Deutsch
+
{{ $t("footer.contact") }}
diff --git a/client/src/components/FeedbackForm.vue b/client/src/components/FeedbackForm.vue
index 9d7fb918..7f08c8d5 100644
--- a/client/src/components/FeedbackForm.vue
+++ b/client/src/components/FeedbackForm.vue
@@ -157,12 +157,9 @@ const MAX_STEPS = 12;
const sendFeedbackMutation = graphql(`
mutation SendFeedbackMutation($input: SendFeedbackInput!) {
sendFeedback(input: $input) {
- id
- satisfaction
- goalAttainment
- proficiency
- receivedMaterials
- materialsRating
+ feedbackResponse {
+ id
+ }
errors {
field
messages
@@ -205,17 +202,19 @@ const sendFeedback = () => {
return;
}
const input: SendFeedbackInput = reactive({
- materialsRating,
- courseNegativeFeedback,
- coursePositiveFeedback,
- goalAttainment,
- instructorCompetence,
- instructorRespect,
- instructorOpenFeedback,
- satisfaction,
- proficiency,
- receivedMaterials,
- wouldRecommend,
+ data: {
+ materials_rating: materialsRating,
+ course_negative_feedback: courseNegativeFeedback,
+ course_positive_feedback: coursePositiveFeedback,
+ goald_attainment: goalAttainment,
+ instructor_competence: instructorCompetence,
+ instructor_respect: instructorRespect,
+ instructor_open_feedback: instructorOpenFeedback,
+ satisfaction,
+ proficiency,
+ received_materials: receivedMaterials,
+ would_recommend: wouldRecommend,
+ },
page: props.page.translation_key,
courseSession: courseSession.id,
});
diff --git a/client/src/components/MainNavigationBar.vue b/client/src/components/MainNavigationBar.vue
index 119ef72a..9f553174 100644
--- a/client/src/components/MainNavigationBar.vue
+++ b/client/src/components/MainNavigationBar.vue
@@ -4,9 +4,12 @@ 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";
@@ -14,7 +17,7 @@ import { onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute, useRouter } from "vue-router";
-type DropdownActions = "logout" | "settings";
+type DropdownActions = "logout" | "settings" | "profile";
interface DropdownData {
action: DropdownActions;
@@ -27,6 +30,7 @@ const router = useRouter();
const userStore = useUserStore();
const appStore = useAppStore();
const courseSessionsStore = useCourseSessionsStore();
+const notificationsStore = useNotificationsStore();
const { t } = useI18n();
const state = reactive({ showMenu: false });
@@ -60,9 +64,12 @@ function inMediaLibrary() {
function handleDropdownSelect(data: DropdownData) {
switch (data.action) {
- case "settings":
+ case "profile":
router.push("/profile");
break;
+ case "settings":
+ router.push("/settings");
+ break;
case "logout":
userStore.handleLogout();
break;
@@ -85,7 +92,14 @@ onMounted(() => {
const profileDropdownData: DropdownListItem[] = [
{
- title: t("mainNavigation.settings"),
+ title: t("mainNavigation.profile"),
+ icon: IconSettings as Component,
+ data: {
+ action: "profile",
+ },
+ },
+ {
+ title: t("general.settings"),
icon: IconSettings as Component,
data: {
action: "settings",
@@ -127,14 +141,23 @@ const profileDropdownData: DropdownListItem[] = [
-
-
-
+
{{ $t("mediaLibrary.title") }}
-
-
-
+
{
{{ userStore.first_name }} {{ userStore.last_name }}
diff --git a/client/src/components/learningPath/LearningPathCircle.vue b/client/src/components/learningPath/LearningPathCircle.vue
new file mode 100644
index 00000000..55239b18
--- /dev/null
+++ b/client/src/components/learningPath/LearningPathCircle.vue
@@ -0,0 +1,135 @@
+
+
+
+
+
{{ pieData }}
+
{{ render() }}
+
+
+
diff --git a/client/src/components/learningPath/LearningSequence.vue b/client/src/components/learningPath/LearningSequence.vue
index c1b59510..6f57cd46 100644
--- a/client/src/components/learningPath/LearningSequence.vue
+++ b/client/src/components/learningPath/LearningSequence.vue
@@ -126,7 +126,10 @@ const learningSequenceBorderClass = computed(() => {
diff --git a/client/src/components/notifications/NotificationList.vue b/client/src/components/notifications/NotificationList.vue
new file mode 100644
index 00000000..286551b8
--- /dev/null
+++ b/client/src/components/notifications/NotificationList.vue
@@ -0,0 +1,98 @@
+
+
+
+
+ {{ $t("notifications.no_notifications") }}
+
+
+
+
![Notification icon]()
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/notifications/NotificationPopover.vue b/client/src/components/notifications/NotificationPopover.vue
new file mode 100644
index 00000000..f1c0505c
--- /dev/null
+++ b/client/src/components/notifications/NotificationPopover.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/notifications/NotificationPopoverContent.vue b/client/src/components/notifications/NotificationPopoverContent.vue
new file mode 100644
index 00000000..5ceca56b
--- /dev/null
+++ b/client/src/components/notifications/NotificationPopoverContent.vue
@@ -0,0 +1,20 @@
+
+
+
+ {{ $t("general.notification") }}
+
+
+
+
+ {{ $t("general.showAll") }}
+
+
+
+
+
diff --git a/client/src/components/ui/ItCheckbox.vue b/client/src/components/ui/ItCheckbox.vue
index bcdccbb4..92d44476 100644
--- a/client/src/components/ui/ItCheckbox.vue
+++ b/client/src/components/ui/ItCheckbox.vue
@@ -1,17 +1,11 @@
+
+
+ {{ label }}
+
+
+
+
diff --git a/client/src/components/ui/checkbox.types.ts b/client/src/components/ui/checkbox.types.ts
new file mode 100644
index 00000000..bf806b60
--- /dev/null
+++ b/client/src/components/ui/checkbox.types.ts
@@ -0,0 +1,6 @@
+export interface CheckboxItem {
+ value: T;
+ label?: string;
+ checked: boolean;
+ subtitle?: string;
+}
diff --git a/client/src/gql/gql.ts b/client/src/gql/gql.ts
index 0fe86c5b..24f468fc 100644
--- a/client/src/gql/gql.ts
+++ b/client/src/gql/gql.ts
@@ -3,13 +3,13 @@ import type { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-
import * as types from "./graphql";
const documents = {
- "\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n sendFeedback(input: $input) {\n id\n satisfaction\n goalAttainment\n proficiency\n receivedMaterials\n materialsRating\n errors {\n field\n messages\n }\n }\n }\n":
+ "\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n sendFeedback(input: $input) {\n feedbackResponse {\n id\n }\n errors {\n field\n messages\n }\n }\n }\n":
types.SendFeedbackMutationDocument,
};
export function graphql(
- source: "\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n sendFeedback(input: $input) {\n id\n satisfaction\n goalAttainment\n proficiency\n receivedMaterials\n materialsRating\n errors {\n field\n messages\n }\n }\n }\n"
-): typeof documents["\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n sendFeedback(input: $input) {\n id\n satisfaction\n goalAttainment\n proficiency\n receivedMaterials\n materialsRating\n errors {\n field\n messages\n }\n }\n }\n"];
+ source: "\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n sendFeedback(input: $input) {\n feedbackResponse {\n id\n }\n errors {\n field\n messages\n }\n }\n }\n"
+): typeof documents["\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n sendFeedback(input: $input) {\n feedbackResponse {\n id\n }\n errors {\n field\n messages\n }\n }\n }\n"];
export function graphql(source: string): unknown;
export function graphql(source: string) {
diff --git a/client/src/gql/graphql.ts b/client/src/gql/graphql.ts
index fc729991..6295ed2b 100644
--- a/client/src/gql/graphql.ts
+++ b/client/src/gql/graphql.ts
@@ -17,6 +17,7 @@ export type Scalars = {
Int: number;
Float: number;
DateTime: any;
+ GenericScalar: any;
JSONString: any;
PositiveInt: any;
UUID: any;
@@ -152,6 +153,11 @@ export type CircleSiblingsArgs = {
searchQuery?: InputMaybe;
};
+export type CircleDocument = {
+ __typename?: "CircleDocument";
+ id?: Maybe;
+};
+
export type CollectionObjectType = {
__typename?: "CollectionObjectType";
ancestors: Array>;
@@ -520,6 +526,14 @@ export type ErrorType = {
messages: Array;
};
+export type FeedbackResponse = Node & {
+ __typename?: "FeedbackResponse";
+ circle: Circle;
+ courseSession: CourseSession;
+ data?: Maybe;
+ id: Scalars["ID"];
+};
+
export type FloatBlock = StreamFieldInterface & {
__typename?: "FloatBlock";
blockType: Scalars["String"];
@@ -1170,6 +1184,10 @@ export type MutationSendFeedbackArgs = {
input: SendFeedbackInput;
};
+export type Node = {
+ id: Scalars["ID"];
+};
+
export type Page = PageInterface & {
__typename?: "Page";
aliasOf?: Maybe;
@@ -1581,6 +1599,7 @@ export type RichTextBlock = StreamFieldInterface & {
export type Search =
| Circle
+ | CircleDocument
| CompetencePage
| CompetenceProfilePage
| Course
@@ -1609,37 +1628,16 @@ export type SecurityRequestResponseLog = {
export type SendFeedbackInput = {
clientMutationId?: InputMaybe;
- courseNegativeFeedback?: InputMaybe;
- coursePositiveFeedback?: InputMaybe;
- goalAttainment?: InputMaybe;
- id?: InputMaybe;
- instructorCompetence?: InputMaybe;
- instructorOpenFeedback?: InputMaybe;
- instructorRespect?: InputMaybe;
- materialsRating?: InputMaybe;
+ courseSession: Scalars["Int"];
+ data?: InputMaybe;
page: Scalars["String"];
- proficiency?: InputMaybe;
- receivedMaterials?: InputMaybe;
- satisfaction?: InputMaybe;
- wouldRecommend?: InputMaybe;
};
export type SendFeedbackPayload = {
__typename?: "SendFeedbackPayload";
clientMutationId?: Maybe;
- courseNegativeFeedback?: Maybe;
- coursePositiveFeedback?: Maybe;
errors?: Maybe>>;
- goalAttainment?: Maybe;
- id?: Maybe;
- instructorCompetence?: Maybe;
- instructorOpenFeedback?: Maybe;
- instructorRespect?: Maybe;
- materialsRating?: Maybe;
- proficiency?: Maybe;
- receivedMaterials?: Maybe;
- satisfaction?: Maybe;
- wouldRecommend?: Maybe;
+ feedbackResponse?: Maybe;
};
export type SiteObjectType = {
@@ -1851,12 +1849,7 @@ export type SendFeedbackMutationMutation = {
__typename?: "Mutation";
sendFeedback?: {
__typename?: "SendFeedbackPayload";
- id?: number | null;
- satisfaction?: number | null;
- goalAttainment?: number | null;
- proficiency?: number | null;
- receivedMaterials?: boolean | null;
- materialsRating?: number | null;
+ feedbackResponse?: { __typename?: "FeedbackResponse"; id: string } | null;
errors?: Array<{
__typename?: "ErrorType";
field: string;
@@ -1901,12 +1894,16 @@ export const SendFeedbackMutationDocument = {
selectionSet: {
kind: "SelectionSet",
selections: [
- { kind: "Field", name: { kind: "Name", value: "id" } },
- { kind: "Field", name: { kind: "Name", value: "satisfaction" } },
- { kind: "Field", name: { kind: "Name", value: "goalAttainment" } },
- { kind: "Field", name: { kind: "Name", value: "proficiency" } },
- { kind: "Field", name: { kind: "Name", value: "receivedMaterials" } },
- { kind: "Field", name: { kind: "Name", value: "materialsRating" } },
+ {
+ kind: "Field",
+ name: { kind: "Name", value: "feedbackResponse" },
+ selectionSet: {
+ kind: "SelectionSet",
+ selections: [
+ { kind: "Field", name: { kind: "Name", value: "id" } },
+ ],
+ },
+ },
{
kind: "Field",
name: { kind: "Name", value: "errors" },
diff --git a/client/src/i18n.ts b/client/src/i18n.ts
index 1bd3880f..7a222ab9 100644
--- a/client/src/i18n.ts
+++ b/client/src/i18n.ts
@@ -1,3 +1,4 @@
+import dayjs from "dayjs";
import { nextTick } from "vue";
import { createI18n } from "vue-i18n";
@@ -5,9 +6,12 @@ import { createI18n } from "vue-i18n";
export const SUPPORT_LOCALES = ["de", "fr", "it"];
let i18n: any = null;
-export function setupI18n(options = { locale: "de", legacy: false }) {
+export function setupI18n(
+ options = { locale: "de", legacy: false, fallbackLocale: "de" }
+) {
i18n = createI18n(options);
setI18nLanguage(options.locale);
+ dayjs.locale(options.locale);
return i18n;
}
diff --git a/client/src/locales/de.json b/client/src/locales/de.json
index 6476f25f..61cc2089 100644
--- a/client/src/locales/de.json
+++ b/client/src/locales/de.json
@@ -8,6 +8,7 @@
"back": "zurück",
"backCapitalized": "@.capitalize:general.back",
"save": "Speichern",
+ "send": "Senden",
"learningUnit": "Lerneinheit",
"learningPath": "Lernpfad",
"learningSequence": "Lernsequenz",
@@ -22,11 +23,13 @@
"profileLink": "Profil anzeigen",
"shop": "Shop",
"yes": "Ja",
- "no": "Nein"
+ "no": "Nein",
+ "showAll": "Alle anschauen",
+ "settings": "Kontoeinstellungen"
},
"mainNavigation": {
"logout": "Abmelden",
- "settings": "Kontoeinstellungen"
+ "profile": "Profil"
},
"dashboard": {
"welcome": "Willkommen, {name}"
@@ -83,7 +86,6 @@
"competences": "Kompetenzen",
"title": "KompetenzNavi",
"lastImprovements": "Letzte verbesserte Kompetenzen",
- "showAll": "Alle anschauen",
"assessment": "Einschätzungen",
"notAssessed": "Nicht eingeschätzt",
"assessAgain": "Sich nochmals einschätzen"
@@ -111,7 +113,9 @@
"feedbacksDone": "Abgeschickte Feedbacks von Teilnehmer.",
"examsDone": "Abgelegte Prüfungen von Teilnehmer.",
"progress": "Fortschritt",
- "profileLink": "Profil anzeigen"
+ "profileLink": "Profil anzeigen",
+ "notifyTaskDescription": "Teilnehmer benachrichtigen",
+ "notifyTask": "Benachrichtigen"
},
"messages": {
"sendMessage": "Nachricht schreiben"
@@ -148,6 +152,13 @@
"answers": "Antworten",
"noFeedbacks": "Es wurden noch keine Feedbacks abgegeben"
},
+ "notifications": {
+ "load_more": "Mehr laden",
+ "no_notifications": "Du hast derzeit keine Benachrichtigungen"
+ },
+ "settings": {
+ "emailNotifications": "Email Benachrichtigungen"
+ },
"constants": {
"yes": "Ja",
"no": "Nein",
diff --git a/client/src/main.ts b/client/src/main.ts
index 45bf3427..86ba2c70 100644
--- a/client/src/main.ts
+++ b/client/src/main.ts
@@ -18,7 +18,10 @@ declare module "pinia" {
}
}
-if (window.location.href.indexOf("localhost") >= 0) {
+if (
+ window.location.href.indexOf("localhost") >= 0 ||
+ window.location.href.indexOf("127.0.0.1") >= 0
+) {
log.setLevel("trace");
} else {
log.setLevel("warn");
diff --git a/client/src/pages/NotificationsPage.vue b/client/src/pages/NotificationsPage.vue
new file mode 100644
index 00000000..e38b96cb
--- /dev/null
+++ b/client/src/pages/NotificationsPage.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+ {{ $t("general.notification") }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/pages/SettingsPage.vue b/client/src/pages/SettingsPage.vue
new file mode 100644
index 00000000..1e13b808
--- /dev/null
+++ b/client/src/pages/SettingsPage.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+ {{ $t("general.settings") }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/pages/StyleGuidePage.vue b/client/src/pages/StyleGuidePage.vue
index f6021185..858a55fc 100644
--- a/client/src/pages/StyleGuidePage.vue
+++ b/client/src/pages/StyleGuidePage.vue
@@ -1,6 +1,7 @@