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 @@ + + + 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 @@ + + + + + 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 @@ + + + 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 @@ + + 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 @@ + + + + + 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 @@ + + + + + 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 @@