Merge pull request #269 from SELab-2/feat/discussions
feat: Discussions pagina's
This commit is contained in:
		
						commit
						f6c2f71edb
					
				
					 22 changed files with 885 additions and 247 deletions
				
			
		|  | @ -1,4 +1,4 @@ | ||||||
| import { Request, Response } from 'express'; | import { Response } from 'express'; | ||||||
| import { themes } from '../data/themes.js'; | import { themes } from '../data/themes.js'; | ||||||
| import { FALLBACK_LANG } from '../config.js'; | import { FALLBACK_LANG } from '../config.js'; | ||||||
| import learningPathService from '../services/learning-paths/learning-path-service.js'; | import learningPathService from '../services/learning-paths/learning-path-service.js'; | ||||||
|  | @ -15,7 +15,7 @@ import { requireFields } from './error-helper.js'; | ||||||
| /** | /** | ||||||
|  * Fetch learning paths based on query parameters. |  * Fetch learning paths based on query parameters. | ||||||
|  */ |  */ | ||||||
| export async function getLearningPaths(req: Request, res: Response): Promise<void> { | export async function getLearningPaths(req: AuthenticatedRequest, res: Response): Promise<void> { | ||||||
|     const admin = req.query.admin; |     const admin = req.query.admin; | ||||||
|     if (admin) { |     if (admin) { | ||||||
|         const paths = await learningPathService.getLearningPathsAdministratedBy(admin as string); |         const paths = await learningPathService.getLearningPathsAdministratedBy(admin as string); | ||||||
|  | @ -59,6 +59,19 @@ export async function getLearningPaths(req: Request, res: Response): Promise<voi | ||||||
|             return; |             return; | ||||||
|         } else { |         } else { | ||||||
|             hruidList = themes.flatMap((theme) => theme.hruids); |             hruidList = themes.flatMap((theme) => theme.hruids); | ||||||
|  | 
 | ||||||
|  |             const apiLearningPathResponse = await learningPathService.fetchLearningPaths(hruidList, language as Language, 'All themes', forGroup); | ||||||
|  |             const apiLearningPaths: LearningPath[] = apiLearningPathResponse.data || []; | ||||||
|  |             let allLearningPaths: LearningPath[] = apiLearningPaths; | ||||||
|  | 
 | ||||||
|  |             if (req.auth) { | ||||||
|  |                 const adminUsername = req.auth.username; | ||||||
|  |                 const userLearningPaths = (await learningPathService.getLearningPathsAdministratedBy(adminUsername)) || []; | ||||||
|  |                 allLearningPaths = apiLearningPaths.concat(userLearningPaths); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             res.json(allLearningPaths); | ||||||
|  |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const learningPaths = await learningPathService.fetchLearningPaths( |         const learningPaths = await learningPathService.fetchLearningPaths( | ||||||
|  |  | ||||||
|  | @ -7,14 +7,20 @@ import { LearningPathTransition } from '../../entities/content/learning-path-tra | ||||||
| 
 | 
 | ||||||
| export class LearningPathRepository extends DwengoEntityRepository<LearningPath> { | export class LearningPathRepository extends DwengoEntityRepository<LearningPath> { | ||||||
|     public async findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> { |     public async findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> { | ||||||
|         return this.findOne({ hruid: hruid, language: language }, { populate: ['nodes', 'nodes.transitions', 'admins'] }); |         return this.findOne( | ||||||
|  |             { | ||||||
|  |                 hruid: hruid, | ||||||
|  |                 language: language, | ||||||
|  |             }, | ||||||
|  |             { populate: ['nodes', 'nodes.transitions', 'admins'] } | ||||||
|  |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Returns all learning paths which have the given language and whose title OR description contains the |      * Returns all learning paths which have the given language and whose title OR description contains the | ||||||
|      * query string. |      * query string. | ||||||
|      * |      * | ||||||
|      * @param query The query string we want to seach for in the title or description. |      * @param query The query string we want to search for in the title or description. | ||||||
|      * @param language The language of the learning paths we want to find. |      * @param language The language of the learning paths we want to find. | ||||||
|      */ |      */ | ||||||
|     public async findByQueryStringAndLanguage(query: string, language: Language): Promise<LearningPath[]> { |     public async findByQueryStringAndLanguage(query: string, language: Language): Promise<LearningPath[]> { | ||||||
|  |  | ||||||
|  | @ -18,13 +18,14 @@ export class QuestionRepository extends DwengoEntityRepository<Question> { | ||||||
|             content: question.content, |             content: question.content, | ||||||
|             timestamp: new Date(), |             timestamp: new Date(), | ||||||
|         }); |         }); | ||||||
|  |         await this.insert(questionEntity); | ||||||
|         questionEntity.learningObjectHruid = question.loId.hruid; |         questionEntity.learningObjectHruid = question.loId.hruid; | ||||||
|         questionEntity.learningObjectLanguage = question.loId.language; |         questionEntity.learningObjectLanguage = question.loId.language; | ||||||
|         questionEntity.learningObjectVersion = question.loId.version; |         questionEntity.learningObjectVersion = question.loId.version; | ||||||
|         questionEntity.author = question.author; |         questionEntity.author = question.author; | ||||||
|         questionEntity.inGroup = question.inGroup; |         questionEntity.inGroup = question.inGroup; | ||||||
|         questionEntity.content = question.content; |         questionEntity.content = question.content; | ||||||
|         return await this.insert(questionEntity); |         return questionEntity; | ||||||
|     } |     } | ||||||
|     public async findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> { |     public async findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> { | ||||||
|         return this.findAll({ |         return this.findAll({ | ||||||
|  |  | ||||||
|  | @ -62,6 +62,7 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { | ||||||
|             data: learningPaths, |             data: learningPaths, | ||||||
|         }; |         }; | ||||||
|     }, |     }, | ||||||
|  | 
 | ||||||
|     async searchLearningPaths(query: string, language: string, personalizedFor: Group): Promise<LearningPath[]> { |     async searchLearningPaths(query: string, language: string, personalizedFor: Group): Promise<LearningPath[]> { | ||||||
|         const apiUrl = `${DWENGO_API_BASE}/learningPath/search`; |         const apiUrl = `${DWENGO_API_BASE}/learningPath/search`; | ||||||
|         const params = { all: query, language }; |         const params = { all: query, language }; | ||||||
|  | @ -75,7 +76,8 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|     async getLearningPathsAdministratedBy(_adminUsername: string) { |     async getLearningPathsAdministratedBy(_adminUsername: string) { | ||||||
|         return []; // Learning paths fetched from the Dwengo API cannot be administrated by a user.
 |         // Dwengo API does not have the concept of admins, so we cannot filter by them.
 | ||||||
|  |         return []; | ||||||
|     }, |     }, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -14,11 +14,14 @@ import { | ||||||
|     testLearningObjectEssayQuestion, |     testLearningObjectEssayQuestion, | ||||||
|     testLearningObjectMultipleChoice, |     testLearningObjectMultipleChoice, | ||||||
| } from '../../test_assets/content/learning-objects.testdata'; | } from '../../test_assets/content/learning-objects.testdata'; | ||||||
| import { testLearningPathWithConditions } from '../../test_assets/content/learning-paths.testdata'; | import { testLearningPath02, testLearningPathWithConditions } from '../../test_assets/content/learning-paths.testdata'; | ||||||
| import { mapToLearningPath } from '../../../src/services/learning-paths/learning-path-service'; | import { mapToLearningPath } from '../../../src/services/learning-paths/learning-path-service'; | ||||||
| import { getTestGroup01, getTestGroup02 } from '../../test_assets/assignments/groups.testdata'; | import { getTestGroup01, getTestGroup02 } from '../../test_assets/assignments/groups.testdata'; | ||||||
| import { Group } from '../../../src/entities/assignments/group.entity.js'; | import { Group } from '../../../src/entities/assignments/group.entity.js'; | ||||||
|  | import { Teacher } from '../../../src/entities/users/teacher.entity.js'; | ||||||
| import { RequiredEntityData } from '@mikro-orm/core'; | import { RequiredEntityData } from '@mikro-orm/core'; | ||||||
|  | import { getFooFighters, getLimpBizkit } from '../../test_assets/users/teachers.testdata'; | ||||||
|  | import { mapToTeacherDTO } from '../../../src/interfaces/teacher'; | ||||||
| 
 | 
 | ||||||
| function expectBranchingObjectNode(result: LearningPathResponse): LearningObjectNode { | function expectBranchingObjectNode(result: LearningPathResponse): LearningObjectNode { | ||||||
|     const branchingObjectMatches = result.data![0].nodes.filter((it) => it.learningobject_hruid === testLearningObjectMultipleChoice.hruid); |     const branchingObjectMatches = result.data![0].nodes.filter((it) => it.learningobject_hruid === testLearningObjectMultipleChoice.hruid); | ||||||
|  | @ -33,6 +36,8 @@ describe('DatabaseLearningPathProvider', () => { | ||||||
|     let finalLearningObject: RequiredEntityData<LearningObject>; |     let finalLearningObject: RequiredEntityData<LearningObject>; | ||||||
|     let groupA: Group; |     let groupA: Group; | ||||||
|     let groupB: Group; |     let groupB: Group; | ||||||
|  |     let teacherA: Teacher; | ||||||
|  |     let teacherB: Teacher; | ||||||
| 
 | 
 | ||||||
|     beforeAll(async () => { |     beforeAll(async () => { | ||||||
|         await setupTestApp(); |         await setupTestApp(); | ||||||
|  | @ -42,6 +47,8 @@ describe('DatabaseLearningPathProvider', () => { | ||||||
|         finalLearningObject = testLearningObjectEssayQuestion; |         finalLearningObject = testLearningObjectEssayQuestion; | ||||||
|         groupA = getTestGroup01(); |         groupA = getTestGroup01(); | ||||||
|         groupB = getTestGroup02(); |         groupB = getTestGroup02(); | ||||||
|  |         teacherA = getFooFighters(); | ||||||
|  |         teacherB = getLimpBizkit(); | ||||||
| 
 | 
 | ||||||
|         // Place different submissions for group A and B.
 |         // Place different submissions for group A and B.
 | ||||||
|         const submissionRepo = getSubmissionRepository(); |         const submissionRepo = getSubmissionRepository(); | ||||||
|  | @ -140,4 +147,18 @@ describe('DatabaseLearningPathProvider', () => { | ||||||
|             expect(result.length).toBe(0); |             expect(result.length).toBe(0); | ||||||
|         }); |         }); | ||||||
|     }); |     }); | ||||||
|  | 
 | ||||||
|  |     describe('getLearningPathsAdministratedBy', () => { | ||||||
|  |         it('returns the learning path owned by the admin', async () => { | ||||||
|  |             const expectedLearningPath = mapToLearningPath(testLearningPath02, [mapToTeacherDTO(teacherB)]); | ||||||
|  |             const result = await databaseLearningPathProvider.getLearningPathsAdministratedBy([teacherB], expectedLearningPath.language); | ||||||
|  |             expect(result.length).toBe(1); | ||||||
|  |             expect(result[0].title).toBe(expectedLearningPath.title); | ||||||
|  |             expect(result[0].description).toBe(expectedLearningPath.description); | ||||||
|  |         }); | ||||||
|  |         it('returns an empty result when querying admins that do not have custom learning paths', async () => { | ||||||
|  |             const result = await databaseLearningPathProvider.getLearningPathsAdministratedBy([teacherA], testLearningPath.language); | ||||||
|  |             expect(result.length).toBe(0); | ||||||
|  |         }); | ||||||
|  |     }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -14,10 +14,12 @@ import { | ||||||
|     testLearningObjectMultipleChoice, |     testLearningObjectMultipleChoice, | ||||||
|     testLearningObjectPnNotebooks, |     testLearningObjectPnNotebooks, | ||||||
| } from './learning-objects.testdata'; | } from './learning-objects.testdata'; | ||||||
|  | import { getLimpBizkit } from '../users/teachers.testdata'; | ||||||
| 
 | 
 | ||||||
| export function makeTestLearningPaths(_em: EntityManager): LearningPath[] { | export function makeTestLearningPaths(_em: EntityManager): LearningPath[] { | ||||||
|     const learningPath01 = mapToLearningPath(testLearningPath01, []); |     const learningPath01 = mapToLearningPath(testLearningPath01, []); | ||||||
|     const learningPath02 = mapToLearningPath(testLearningPath02, []); |     const learningPath02 = mapToLearningPath(testLearningPath02, []); | ||||||
|  |     learningPath02.admins = [getLimpBizkit()]; | ||||||
| 
 | 
 | ||||||
|     const partiallyDatabasePartiallyDwengoApiLearningPath = mapToLearningPath(testPartiallyDatabaseAndPartiallyDwengoApiLearningPath, []); |     const partiallyDatabasePartiallyDwengoApiLearningPath = mapToLearningPath(testPartiallyDatabaseAndPartiallyDwengoApiLearningPath, []); | ||||||
|     const learningPathWithConditions = mapToLearningPath(testLearningPathWithConditions, []); |     const learningPathWithConditions = mapToLearningPath(testLearningPathWithConditions, []); | ||||||
|  |  | ||||||
							
								
								
									
										10
									
								
								common/src/util/match-mode.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								common/src/util/match-mode.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | export enum MatchMode { | ||||||
|  |     /** | ||||||
|  |      * Match any | ||||||
|  |      */ | ||||||
|  |     ANY = 'ANY', | ||||||
|  |     /** | ||||||
|  |      * Match all | ||||||
|  |      */ | ||||||
|  |     ALL = 'ALL', | ||||||
|  | } | ||||||
							
								
								
									
										48
									
								
								frontend/src/components/DiscussionSideBarElement.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								frontend/src/components/DiscussionSideBarElement.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,48 @@ | ||||||
|  | <script setup lang="ts"> | ||||||
|  |     import type { LearningObject } from "@/data-objects/learning-objects/learning-object"; | ||||||
|  |     import type { LearningPath } from "@/data-objects/learning-paths/learning-path"; | ||||||
|  |     import { useLearningObjectListForPathQuery } from "@/queries/learning-objects"; | ||||||
|  |     import { useRoute } from "vue-router"; | ||||||
|  |     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||||
|  | 
 | ||||||
|  |     const route = useRoute(); | ||||||
|  | 
 | ||||||
|  |     const props = defineProps<{ | ||||||
|  |         path: LearningPath; | ||||||
|  |         activeObjectId: string; | ||||||
|  |     }>(); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |     <v-expansion-panel :value="props.path.hruid"> | ||||||
|  |         <v-expansion-panel-title> | ||||||
|  |             {{ path.title }} | ||||||
|  |         </v-expansion-panel-title> | ||||||
|  |         <v-expansion-panel-text> | ||||||
|  |             <v-lazy> | ||||||
|  |                 <using-query-result | ||||||
|  |                     :query-result="useLearningObjectListForPathQuery(props.path)" | ||||||
|  |                     v-slot="learningObjects: { data: LearningObject[] }" | ||||||
|  |                 > | ||||||
|  |                     <template | ||||||
|  |                         v-for="node in learningObjects.data" | ||||||
|  |                         :key="node.key" | ||||||
|  |                     > | ||||||
|  |                         <v-list-item | ||||||
|  |                             link | ||||||
|  |                             :to="{ | ||||||
|  |                                 path: `/discussion-reload/${props.path.hruid}/${node.language}/${node.key}`, | ||||||
|  |                                 query: route.query, | ||||||
|  |                             }" | ||||||
|  |                             :title="node.title" | ||||||
|  |                             :active="node.key === props.activeObjectId" | ||||||
|  |                         > | ||||||
|  |                         </v-list-item> | ||||||
|  |                     </template> | ||||||
|  |                 </using-query-result> | ||||||
|  |             </v-lazy> | ||||||
|  |         </v-expansion-panel-text> | ||||||
|  |     </v-expansion-panel> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <style scoped></style> | ||||||
							
								
								
									
										70
									
								
								frontend/src/components/DiscussionsSideBar.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								frontend/src/components/DiscussionsSideBar.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,70 @@ | ||||||
|  | <script setup lang="ts"> | ||||||
|  |     import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; | ||||||
|  |     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||||
|  |     import DiscussionSideBarElement from "@/components/DiscussionSideBarElement.vue"; | ||||||
|  |     import { useI18n } from "vue-i18n"; | ||||||
|  |     import { useGetAllLearningPaths } from "@/queries/learning-paths.ts"; | ||||||
|  |     import { ref, watch } from "vue"; | ||||||
|  |     import { useRoute } from "vue-router"; | ||||||
|  | 
 | ||||||
|  |     const { t, locale } = useI18n(); | ||||||
|  |     const route = useRoute(); | ||||||
|  | 
 | ||||||
|  |     const navigationDrawerShown = ref(true); | ||||||
|  |     const currentLocale = ref(locale.value); | ||||||
|  |     const expanded = ref([route.params.hruid]); | ||||||
|  | 
 | ||||||
|  |     watch(locale, (newLocale) => { | ||||||
|  |         currentLocale.value = newLocale; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const allLearningPathsResult = useGetAllLearningPaths(() => currentLocale.value); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |     <v-navigation-drawer | ||||||
|  |         v-model="navigationDrawerShown" | ||||||
|  |         :width="350" | ||||||
|  |         app | ||||||
|  |     > | ||||||
|  |         <div class="d-flex flex-column h-100"> | ||||||
|  |             <v-list-item> | ||||||
|  |                 <template v-slot:title> | ||||||
|  |                     <div class="title">{{ t("discussions") }}</div> | ||||||
|  |                 </template> | ||||||
|  |             </v-list-item> | ||||||
|  |             <v-divider></v-divider> | ||||||
|  |             <v-expansion-panels v-model="expanded"> | ||||||
|  |                 <using-query-result | ||||||
|  |                     :query-result="allLearningPathsResult" | ||||||
|  |                     v-slot="learningPaths: { data: LearningPath[] }" | ||||||
|  |                 > | ||||||
|  |                     <DiscussionSideBarElement | ||||||
|  |                         v-for="learningPath in learningPaths.data" | ||||||
|  |                         :path="learningPath" | ||||||
|  |                         :activeObjectId="'' as string" | ||||||
|  |                         :key="learningPath.hruid" | ||||||
|  |                     /> | ||||||
|  |                 </using-query-result> | ||||||
|  |             </v-expansion-panels> | ||||||
|  |         </div> | ||||||
|  |     </v-navigation-drawer> | ||||||
|  |     <div class="control-bar-above-content"> | ||||||
|  |         <v-btn | ||||||
|  |             :icon="navigationDrawerShown ? 'mdi-menu-open' : 'mdi-menu'" | ||||||
|  |             class="navigation-drawer-toggle-button" | ||||||
|  |             variant="plain" | ||||||
|  |             @click="navigationDrawerShown = !navigationDrawerShown" | ||||||
|  |         ></v-btn> | ||||||
|  |     </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  |     .title { | ||||||
|  |         color: #0e6942; | ||||||
|  |         text-transform: uppercase; | ||||||
|  |         font-weight: bolder; | ||||||
|  |         padding-top: 2%; | ||||||
|  |         font-size: 36px; | ||||||
|  |     } | ||||||
|  | </style> | ||||||
|  | @ -87,14 +87,13 @@ | ||||||
|             > |             > | ||||||
|                 {{ t("classes") }} |                 {{ t("classes") }} | ||||||
|             </v-btn> |             </v-btn> | ||||||
|             <!-- TODO Re-enable this button when the discussion page is ready --> |             <v-btn | ||||||
|             <!--            <v-btn--> |                 class="menu_item" | ||||||
|             <!--                class="menu_item"--> |                 variant="text" | ||||||
|             <!--                variant="text"--> |                 to="/discussion" | ||||||
|             <!--                to="/user/discussion"--> |             > | ||||||
|             <!--            >--> |                 {{ t("discussions") }} | ||||||
|             <!--                {{ t("discussions") }}--> |             </v-btn> | ||||||
|             <!--            </v-btn>--> |  | ||||||
|         </v-toolbar-items> |         </v-toolbar-items> | ||||||
|         <v-menu |         <v-menu | ||||||
|             open-on-hover |             open-on-hover | ||||||
|  | @ -231,7 +230,7 @@ | ||||||
|             </v-list-item> |             </v-list-item> | ||||||
| 
 | 
 | ||||||
|             <v-list-item |             <v-list-item | ||||||
|                 to="/user/discussion" |                 to="/discussion" | ||||||
|                 link |                 link | ||||||
|             > |             > | ||||||
|                 <v-list-item-title class="menu_item">{{ t("discussions") }}</v-list-item-title> |                 <v-list-item-title class="menu_item">{{ t("discussions") }}</v-list-item-title> | ||||||
|  |  | ||||||
|  | @ -1,6 +1,9 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|     import type { QuestionDTO } from "@dwengo-1/common/interfaces/question"; |     import type { QuestionDTO } from "@dwengo-1/common/interfaces/question"; | ||||||
|     import SingleQuestion from "./SingleQuestion.vue"; |     import SingleQuestion from "./SingleQuestion.vue"; | ||||||
|  |     import { useI18n } from "vue-i18n"; | ||||||
|  | 
 | ||||||
|  |     const { t } = useI18n(); | ||||||
| 
 | 
 | ||||||
|     defineProps<{ |     defineProps<{ | ||||||
|         questions: QuestionDTO[]; |         questions: QuestionDTO[]; | ||||||
|  | @ -8,13 +11,28 @@ | ||||||
| </script> | </script> | ||||||
| <template> | <template> | ||||||
|     <div class="space-y-4"> |     <div class="space-y-4"> | ||||||
|         <div |         <div v-if="questions.length != 0"> | ||||||
|             v-for="question in questions" |             <div | ||||||
|             :key="(question.sequenceNumber, question.content)" |                 v-for="question in questions" | ||||||
|             class="border rounded-2xl p-4 shadow-sm bg-white" |                 :key="(question.sequenceNumber, question.content)" | ||||||
|         > |             > | ||||||
|             <SingleQuestion :question="question"></SingleQuestion> |                 <SingleQuestion :question="question"></SingleQuestion> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |         <div v-else> | ||||||
|  |             <p class="no-questions">{{ t("no-questions") }}</p> | ||||||
|         </div> |         </div> | ||||||
|     </div> |     </div> | ||||||
| </template> | </template> | ||||||
| <style scoped></style> | <style scoped> | ||||||
|  |     .no-questions { | ||||||
|  |         display: flex; | ||||||
|  |         justify-content: center; | ||||||
|  |         align-items: center; | ||||||
|  |         height: 40vh; | ||||||
|  |         text-align: center; | ||||||
|  |         font-size: 18px; | ||||||
|  |         color: #666; | ||||||
|  |         padding: 0 20px; | ||||||
|  |     } | ||||||
|  | </style> | ||||||
|  |  | ||||||
							
								
								
									
										134
									
								
								frontend/src/components/QuestionBox.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								frontend/src/components/QuestionBox.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,134 @@ | ||||||
|  | <script setup lang="ts"> | ||||||
|  |     import authService from "@/services/auth/auth-service.ts"; | ||||||
|  |     import { Language } from "@/data-objects/language.ts"; | ||||||
|  |     import { computed, type ComputedRef, ref } from "vue"; | ||||||
|  |     import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; | ||||||
|  |     import { useStudentAssignmentsQuery, useStudentGroupsQuery } from "@/queries/students.ts"; | ||||||
|  |     import type { GroupDTO, GroupDTOId } from "@dwengo-1/common/interfaces/group"; | ||||||
|  |     import type { QuestionData } from "@dwengo-1/common/interfaces/question"; | ||||||
|  |     import type { LearningObjectIdentifierDTO } from "@dwengo-1/interfaces/learning-content"; | ||||||
|  |     import { useCreateQuestionMutation, useQuestionsQuery } from "@/queries/questions.ts"; | ||||||
|  |     import { LearningPathNode } from "@/data-objects/learning-paths/learning-path-node.ts"; | ||||||
|  |     import { useGetLearningPathQuery } from "@/queries/learning-paths.ts"; | ||||||
|  |     import { useLearningObjectListForPathQuery } from "@/queries/learning-objects.ts"; | ||||||
|  |     import { useI18n } from "vue-i18n"; | ||||||
|  |     import { AccountType } from "@dwengo-1/common/util/account-types.ts"; | ||||||
|  | 
 | ||||||
|  |     const props = defineProps<{ | ||||||
|  |         hruid: string; | ||||||
|  |         language: Language; | ||||||
|  |         learningObjectHruid?: string; | ||||||
|  |         forGroup?: GroupDTOId | undefined; | ||||||
|  |         withTitle?: boolean; | ||||||
|  |     }>(); | ||||||
|  | 
 | ||||||
|  |     const { t } = useI18n(); | ||||||
|  | 
 | ||||||
|  |     const studentAssignmentsQueryResult = useStudentAssignmentsQuery( | ||||||
|  |         authService.authState.user?.profile.preferred_username, | ||||||
|  |     ); | ||||||
|  |     const learningPathQueryResult = useGetLearningPathQuery(props.hruid, props.language, props.forGroup); | ||||||
|  |     const learningObjectListQueryResult = useLearningObjectListForPathQuery(learningPathQueryResult.data); | ||||||
|  | 
 | ||||||
|  |     const pathIsAssignment = computed(() => { | ||||||
|  |         const assignments = (studentAssignmentsQueryResult.data.value?.assignments as AssignmentDTO[]) || []; | ||||||
|  |         return assignments.some( | ||||||
|  |             (assignment) => assignment.learningPath === props.hruid && assignment.language === props.language, | ||||||
|  |         ); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const nodesList: ComputedRef<LearningPathNode[] | null> = computed( | ||||||
|  |         () => learningPathQueryResult.data.value?.nodesAsList ?? null, | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     const currentNode = computed(() => { | ||||||
|  |         const currentHruid = props.learningObjectHruid; | ||||||
|  |         return nodesList.value?.find((it) => it.learningobjectHruid === currentHruid); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const getQuestionsQuery = useQuestionsQuery( | ||||||
|  |         computed( | ||||||
|  |             () => | ||||||
|  |                 ({ | ||||||
|  |                     language: currentNode.value?.language, | ||||||
|  |                     hruid: currentNode.value?.learningobjectHruid, | ||||||
|  |                     version: currentNode.value?.version, | ||||||
|  |                 }) as LearningObjectIdentifierDTO, | ||||||
|  |         ), | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     const questionInput = ref(""); | ||||||
|  | 
 | ||||||
|  |     const loID: ComputedRef<LearningObjectIdentifierDTO> = computed(() => ({ | ||||||
|  |         hruid: props.learningObjectHruid as string, | ||||||
|  |         language: props.language, | ||||||
|  |     })); | ||||||
|  |     const createQuestionMutation = useCreateQuestionMutation(loID); | ||||||
|  |     const groupsQueryResult = useStudentGroupsQuery(authService.authState.user?.profile.preferred_username); | ||||||
|  | 
 | ||||||
|  |     const showQuestionBox = computed( | ||||||
|  |         () => authService.authState.activeRole === AccountType.Student && pathIsAssignment.value, | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     function submitQuestion(): void { | ||||||
|  |         const assignments = studentAssignmentsQueryResult.data.value?.assignments as AssignmentDTO[]; | ||||||
|  |         const assignment = assignments.find( | ||||||
|  |             (assignment) => assignment.learningPath === props.hruid && assignment.language === props.language, | ||||||
|  |         ); | ||||||
|  |         const groups = groupsQueryResult.data.value?.groups as GroupDTO[]; | ||||||
|  |         const group = groups?.find((group) => group.assignment === assignment?.id) as GroupDTO; | ||||||
|  |         const questionData: QuestionData = { | ||||||
|  |             author: authService.authState.user?.profile.preferred_username, | ||||||
|  |             content: questionInput.value, | ||||||
|  |             inGroup: group, | ||||||
|  |         }; | ||||||
|  |         if (questionInput.value !== "") { | ||||||
|  |             createQuestionMutation.mutate(questionData, { | ||||||
|  |                 onSuccess: async () => { | ||||||
|  |                     questionInput.value = ""; // Clear the input field after submission | ||||||
|  |                     await getQuestionsQuery.refetch(); // Reload the questions | ||||||
|  |                 }, | ||||||
|  |                 onError: (_) => { | ||||||
|  |                     // TODO Handle error | ||||||
|  |                     // - console.error(e); | ||||||
|  |                 }, | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |     <h3 v-if="props.withTitle && showQuestionBox">{{ t("askAQuestion") }}:</h3> | ||||||
|  |     <div | ||||||
|  |         class="question-box" | ||||||
|  |         v-if="showQuestionBox" | ||||||
|  |     > | ||||||
|  |         <v-textarea | ||||||
|  |             :label="t('question-input-placeholder')" | ||||||
|  |             v-model="questionInput" | ||||||
|  |             class="question-field" | ||||||
|  |             density="compact" | ||||||
|  |             rows="1" | ||||||
|  |             variant="outlined" | ||||||
|  |             auto-grow | ||||||
|  |         > | ||||||
|  |             <template v-slot:append-inner> | ||||||
|  |                 <v-btn | ||||||
|  |                     icon="mdi mdi-send" | ||||||
|  |                     size="small" | ||||||
|  |                     variant="plain" | ||||||
|  |                     class="question-button" | ||||||
|  |                     @click="submitQuestion" | ||||||
|  |                 /> | ||||||
|  |             </template> | ||||||
|  |         </v-textarea> | ||||||
|  |     </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  |     .question-box { | ||||||
|  |         width: 100%; | ||||||
|  |         max-width: 400px; | ||||||
|  |         margin: 20px auto; | ||||||
|  |     } | ||||||
|  | </style> | ||||||
|  | @ -5,17 +5,34 @@ | ||||||
|     import UsingQueryResult from "./UsingQueryResult.vue"; |     import UsingQueryResult from "./UsingQueryResult.vue"; | ||||||
|     import type { AnswersResponse } from "@/controllers/answers"; |     import type { AnswersResponse } from "@/controllers/answers"; | ||||||
|     import type { AnswerData, AnswerDTO } from "@dwengo-1/common/interfaces/answer"; |     import type { AnswerData, AnswerDTO } from "@dwengo-1/common/interfaces/answer"; | ||||||
|  |     import type { UserDTO } from "@dwengo-1/common/interfaces/user"; | ||||||
|     import authService from "@/services/auth/auth-service"; |     import authService from "@/services/auth/auth-service"; | ||||||
|  |     import { useI18n } from "vue-i18n"; | ||||||
|     import { AccountType } from "@dwengo-1/common/util/account-types"; |     import { AccountType } from "@dwengo-1/common/util/account-types"; | ||||||
| 
 | 
 | ||||||
|  |     const { t } = useI18n(); | ||||||
|  | 
 | ||||||
|     const props = defineProps<{ |     const props = defineProps<{ | ||||||
|         question: QuestionDTO; |         question: QuestionDTO; | ||||||
|     }>(); |     }>(); | ||||||
| 
 | 
 | ||||||
|     const expanded = ref(false); |     const expanded = ref(false); | ||||||
|  |     const answersContainer = ref<HTMLElement | null>(null); // Ref for the answers container | ||||||
| 
 | 
 | ||||||
|     function toggle(): void { |     function toggle(): void { | ||||||
|         expanded.value = !expanded.value; |         expanded.value = !expanded.value; | ||||||
|  | 
 | ||||||
|  |         // Scroll to the answers container if expanded | ||||||
|  |         if (expanded.value && answersContainer.value) { | ||||||
|  |             setTimeout(() => { | ||||||
|  |                 if (answersContainer.value) { | ||||||
|  |                     answersContainer.value.scrollIntoView({ | ||||||
|  |                         behavior: "smooth", | ||||||
|  |                         block: "start", | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|  |             }, 100); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     function formatDate(timestamp: string | Date): string { |     function formatDate(timestamp: string | Date): string { | ||||||
|  | @ -49,141 +66,121 @@ | ||||||
|             createAnswerMutation.mutate(answerData, { |             createAnswerMutation.mutate(answerData, { | ||||||
|                 onSuccess: async () => { |                 onSuccess: async () => { | ||||||
|                     answer.value = ""; |                     answer.value = ""; | ||||||
|  |                     expanded.value = true; | ||||||
|                     await answersQuery.refetch(); |                     await answersQuery.refetch(); | ||||||
|                 }, |                 }, | ||||||
|             }); |             }); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     function displayNameFor(user: UserDTO) { | ||||||
|  |         if (user.firstName && user.lastName) { | ||||||
|  |             return `${user.firstName} ${user.lastName}`; | ||||||
|  |         } else { | ||||||
|  |             return user.username; | ||||||
|  |         } | ||||||
|  |     } | ||||||
| </script> | </script> | ||||||
| <template> | <template> | ||||||
|     <div class="space-y-4"> |     <div class="space-y-4"> | ||||||
|         <div |         <v-card class="question-card"> | ||||||
|             class="flex justify-between items-center mb-2" |             <v-card-title class="author-title">{{ displayNameFor(question.author) }}</v-card-title> | ||||||
|             style=" |             <v-card-subtitle>{{ formatDate(question.timestamp) }}</v-card-subtitle> | ||||||
|                 margin-right: 5px; |             <v-card-text> | ||||||
|                 margin-left: 5px; |                 {{ question.content }} | ||||||
|                 font-weight: bold; |             </v-card-text> | ||||||
|                 display: flex; |             <template | ||||||
|                 flex-direction: row; |                 v-slot:actions | ||||||
|                 justify-content: space-between; |                 v-if=" | ||||||
|             " |                     authService.authState.activeRole === AccountType.Teacher || | ||||||
|         > |                     answersQuery.data?.value?.answers?.length > 0 | ||||||
|             <span class="font-semibold text-lg text-gray-800">{{ |                 " | ||||||
|                 question.author.firstName + " " + question.author.lastName |  | ||||||
|             }}</span> |  | ||||||
|             <span class="text-sm text-gray-500">{{ formatDate(question.timestamp) }}</span> |  | ||||||
|         </div> |  | ||||||
| 
 |  | ||||||
|         <div |  | ||||||
|             class="text-gray-700 mb-3" |  | ||||||
|             style="margin-left: 10px" |  | ||||||
|         > |  | ||||||
|             {{ question.content }} |  | ||||||
|         </div> |  | ||||||
|         <div |  | ||||||
|             v-if="authService.authState.activeRole === AccountType.Teacher" |  | ||||||
|             class="answer-input-container" |  | ||||||
|         > |  | ||||||
|             <input |  | ||||||
|                 v-model="answer" |  | ||||||
|                 type="text" |  | ||||||
|                 placeholder="answer: ..." |  | ||||||
|                 class="answer-input" |  | ||||||
|             /> |  | ||||||
|             <button |  | ||||||
|                 @click="submitAnswer" |  | ||||||
|                 class="submit-button" |  | ||||||
|             > |             > | ||||||
|                 ▶ |                 <div class="question-actions-container"> | ||||||
|             </button> |                     <v-textarea | ||||||
|         </div> |                         v-if="authService.authState.activeRole === AccountType.Teacher" | ||||||
|         <using-query-result |                         :label="t('answer-input-placeholder')" | ||||||
|             :query-result="answersQuery" |                         v-model="answer" | ||||||
|             v-slot="answersResponse: { data: AnswersResponse }" |                         class="answer-field" | ||||||
|         > |                         density="compact" | ||||||
|             <button |                         rows="1" | ||||||
|                 v-if="answersResponse.data.answers && answersResponse.data.answers.length > 0" |                         variant="outlined" | ||||||
|                 @click="toggle()" |                         auto-grow | ||||||
|                 class="text-blue-600 hover:underline text-sm" |  | ||||||
|             > |  | ||||||
|                 {{ expanded ? "Hide Answers" : "Show Answers" }} |  | ||||||
|             </button> |  | ||||||
| 
 |  | ||||||
|             <div |  | ||||||
|                 v-if="expanded" |  | ||||||
|                 class="mt-3 pl-4 border-l-2 border-blue-200 space-y-2" |  | ||||||
|             > |  | ||||||
|                 <div |  | ||||||
|                     v-for="(answer, answerIndex) in answersResponse.data.answers as AnswerDTO[]" |  | ||||||
|                     :key="answerIndex" |  | ||||||
|                     class="text-gray-600" |  | ||||||
|                 > |  | ||||||
|                     <div |  | ||||||
|                         class="flex justify-between items-center mb-2" |  | ||||||
|                         style=" |  | ||||||
|                             margin-right: 5px; |  | ||||||
|                             margin-left: 5px; |  | ||||||
|                             font-weight: bold; |  | ||||||
|                             display: flex; |  | ||||||
|                             flex-direction: row; |  | ||||||
|                             justify-content: space-between; |  | ||||||
|                         " |  | ||||||
|                     > |                     > | ||||||
|                         <span class="font-semibold text-lg text-gray-800">{{ answer.author.username }}</span> |                         <template v-slot:append-inner> | ||||||
|                         <span class="text-sm text-gray-500">{{ formatDate(answer.timestamp) }}</span> |                             <v-btn | ||||||
|                     </div> |                                 icon="mdi mdi-send" | ||||||
| 
 |                                 size="small" | ||||||
|                     <div |                                 variant="plain" | ||||||
|                         class="text-gray-700 mb-3" |                                 class="answer-button" | ||||||
|                         style="margin-left: 10px" |                                 @click="submitAnswer" | ||||||
|  |                             /> | ||||||
|  |                         </template> | ||||||
|  |                     </v-textarea> | ||||||
|  |                     <using-query-result | ||||||
|  |                         :query-result="answersQuery" | ||||||
|  |                         v-slot="answersResponse: { data: AnswersResponse }" | ||||||
|                     > |                     > | ||||||
|                         {{ answer.content }} |                         <v-btn | ||||||
|                     </div> |                             v-if="answersResponse.data.answers && answersResponse.data.answers.length > 0" | ||||||
|  |                             @click="toggle()" | ||||||
|  |                         > | ||||||
|  |                             {{ expanded ? t("answers-toggle-hide") : t("answers-toggle-show") }} | ||||||
|  |                         </v-btn> | ||||||
|  | 
 | ||||||
|  |                         <div | ||||||
|  |                             v-show="expanded" | ||||||
|  |                             ref="answersContainer" | ||||||
|  |                             class="mt-3 pl-4 border-l-2 border-blue-200 space-y-2" | ||||||
|  |                         > | ||||||
|  |                             <v-card | ||||||
|  |                                 v-for="(answer, answerIndex) in answersResponse.data.answers as AnswerDTO[]" | ||||||
|  |                                 :key="answerIndex" | ||||||
|  |                                 class="answer-card" | ||||||
|  |                             > | ||||||
|  |                                 <v-card-title class="author-title">{{ displayNameFor(answer.author) }}</v-card-title> | ||||||
|  |                                 <v-card-subtitle>{{ formatDate(answer.timestamp) }}</v-card-subtitle> | ||||||
|  |                                 <v-card-text> | ||||||
|  |                                     {{ answer.content }} | ||||||
|  |                                 </v-card-text> | ||||||
|  |                             </v-card> | ||||||
|  |                         </div> | ||||||
|  |                     </using-query-result> | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </template> | ||||||
|         </using-query-result> |         </v-card> | ||||||
|     </div> |     </div> | ||||||
| </template> | </template> | ||||||
| <style scoped> | <style scoped> | ||||||
|     .answer-input { |     .answer-field { | ||||||
|         flex-grow: 1; |         max-width: 500px; | ||||||
|         outline: none; |  | ||||||
|         border: none; |  | ||||||
|         background: transparent; |  | ||||||
|         color: #374151; /* gray-700 */ |  | ||||||
|         font-size: 0.875rem; /* smaller font size */ |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .answer-input::placeholder { |     .answer-button { | ||||||
|         color: #9ca3af; /* gray-400 */ |         margin: auto; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .submit-button { |  | ||||||
|         margin-left: 0.25rem; |  | ||||||
|         padding: 0.25rem; |  | ||||||
|         background-color: #f3f4f6; /* gray-100 */ |  | ||||||
|         border-radius: 9999px; |  | ||||||
|         transition: background-color 0.2s; |  | ||||||
|         border: none; |  | ||||||
|         cursor: pointer; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     .submit-button:hover { |  | ||||||
|         background-color: #e5e7eb; /* gray-200 */ |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     .submit-icon { |  | ||||||
|         width: 0.75rem; |  | ||||||
|         height: 0.75rem; |  | ||||||
|         color: #4b5563; /* gray-600 */ |  | ||||||
|     } |  | ||||||
|     .answer-input-container { |     .answer-input-container { | ||||||
|         display: flex; |         margin: 5px; | ||||||
|         align-items: center; |     } | ||||||
|         border: 1px solid #d1d5db; /* gray-300 */ | 
 | ||||||
|         border-radius: 9999px; |     .question-card { | ||||||
|         padding: 0.5rem 1rem; |         margin: 10px; | ||||||
|         max-width: 28rem; |     } | ||||||
|  | 
 | ||||||
|  |     .question-actions-container { | ||||||
|         width: 100%; |         width: 100%; | ||||||
|  |         margin-left: 10px; | ||||||
|  |         margin-right: 10px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .answer-card { | ||||||
|  |         margin-top: 10px; | ||||||
|  |         margin-bottom: 10px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .author-title { | ||||||
|  |         font-size: 14pt; | ||||||
|  |         margin-bottom: -10px; | ||||||
|     } |     } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -181,5 +181,15 @@ | ||||||
|     "current-groups": "Aktuelle Gruppen", |     "current-groups": "Aktuelle Gruppen", | ||||||
|     "group-size-label": "Gruppengröße", |     "group-size-label": "Gruppengröße", | ||||||
|     "save": "Speichern", |     "save": "Speichern", | ||||||
|     "unassigned": "Nicht zugewiesen" |     "unassigned": "Nicht zugewiesen", | ||||||
|  |     "questions": "Fragen", | ||||||
|  |     "view-questions": "Fragen anzeigen auf ", | ||||||
|  |     "question-input-placeholder": "Ihre Frage...", | ||||||
|  |     "answer-input-placeholder": "Ihre Antwort...", | ||||||
|  |     "answers-toggle-hide": "Antworten verstecken", | ||||||
|  |     "answers-toggle-show": "Antworten anzeigen", | ||||||
|  |     "no-questions": "Keine Fragen", | ||||||
|  |     "no-discussion-tip": "Wählen Sie ein Lernobjekt aus, um dessen Fragen anzuzeigen", | ||||||
|  |     "askAQuestion": "Eine Frage stellen", | ||||||
|  |     "questionsCapitalized": "Fragen" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ | ||||||
|     "teacher": "teacher", |     "teacher": "teacher", | ||||||
|     "assignments": "Assignments", |     "assignments": "Assignments", | ||||||
|     "classes": "Classes", |     "classes": "Classes", | ||||||
|     "discussions": "discussions", |     "discussions": "Discussions", | ||||||
|     "logout": "log out", |     "logout": "log out", | ||||||
|     "error_title": "Error", |     "error_title": "Error", | ||||||
|     "previous": "Previous", |     "previous": "Previous", | ||||||
|  | @ -182,5 +182,15 @@ | ||||||
|     "current-groups": "Current groups", |     "current-groups": "Current groups", | ||||||
|     "group-size-label": "Group size", |     "group-size-label": "Group size", | ||||||
|     "save": "Save", |     "save": "Save", | ||||||
|     "unassigned": "Unassigned" |     "unassigned": "Unassigned", | ||||||
|  |     "questions": "questions", | ||||||
|  |     "view-questions": "View questions in ", | ||||||
|  |     "question-input-placeholder": "Your question...", | ||||||
|  |     "answer-input-placeholder": "Your answer...", | ||||||
|  |     "answers-toggle-hide": "Hide answers", | ||||||
|  |     "answers-toggle-show": "Show answers", | ||||||
|  |     "no-questions": "No questions asked yet", | ||||||
|  |     "no-discussion-tip": "Choose a learning object to view its questions", | ||||||
|  |     "askAQuestion": "Ask a question", | ||||||
|  |     "questionsCapitalized": "Questions" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -182,5 +182,15 @@ | ||||||
|     "current-groups": "Groupes actuels", |     "current-groups": "Groupes actuels", | ||||||
|     "group-size-label": "Taille des groupes", |     "group-size-label": "Taille des groupes", | ||||||
|     "save": "Enregistrer", |     "save": "Enregistrer", | ||||||
|     "unassigned": "Non assigné" |     "unassigned": "Non assigné", | ||||||
|  |     "questions": "Questions", | ||||||
|  |     "view-questions": "Voir les questions dans ", | ||||||
|  |     "question-input-placeholder": "Votre question...", | ||||||
|  |     "answer-input-placeholder": "Votre réponse...", | ||||||
|  |     "answers-toggle-hide": "Masquer réponses", | ||||||
|  |     "answers-toggle-show": "Afficher réponse", | ||||||
|  |     "no-questions": "Aucune question trouvée", | ||||||
|  |     "no-discussion-tip": "Sélectionnez un objet d'apprentissage pour afficher les questions qui s'y rapportent", | ||||||
|  |     "askAQuestion": "Pose une question", | ||||||
|  |     "questionsCapitalized": "Questions" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ | ||||||
|     "teacher": "leerkracht", |     "teacher": "leerkracht", | ||||||
|     "assignments": "Opdrachten", |     "assignments": "Opdrachten", | ||||||
|     "classes": "Klassen", |     "classes": "Klassen", | ||||||
|     "discussions": "discussies", |     "discussions": "Discussies", | ||||||
|     "logout": "log uit", |     "logout": "log uit", | ||||||
|     "error_title": "Fout", |     "error_title": "Fout", | ||||||
|     "previous": "Vorige", |     "previous": "Vorige", | ||||||
|  | @ -181,5 +181,15 @@ | ||||||
|     "current-groups": "Huidige groepen", |     "current-groups": "Huidige groepen", | ||||||
|     "group-size-label": "Grootte van groepen", |     "group-size-label": "Grootte van groepen", | ||||||
|     "save": "Opslaan", |     "save": "Opslaan", | ||||||
|     "unassigned": "Niet toegewezen" |     "unassigned": "Niet toegewezen", | ||||||
|  |     "questions": "vragen", | ||||||
|  |     "view-questions": "Bekijk vragen in ", | ||||||
|  |     "question-input-placeholder": "Uw vraag...", | ||||||
|  |     "answer-input-placeholder": "Uw antwoord...", | ||||||
|  |     "answers-toggle-hide": "Verberg antwoorden", | ||||||
|  |     "answers-toggle-show": "Toon antwoorden", | ||||||
|  |     "no-questions": "Nog geen vragen gesteld", | ||||||
|  |     "no-discussion-tip": "Kies een leerobject om zijn vragen te bekijken", | ||||||
|  |     "askAQuestion": "Stel een vraag", | ||||||
|  |     "questionsCapitalized": "Vragen" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -14,6 +14,8 @@ import UserHomePage from "@/views/homepage/UserHomePage.vue"; | ||||||
| import SingleTheme from "@/views/SingleTheme.vue"; | import SingleTheme from "@/views/SingleTheme.vue"; | ||||||
| import LearningObjectView from "@/views/learning-paths/learning-object/LearningObjectView.vue"; | import LearningObjectView from "@/views/learning-paths/learning-object/LearningObjectView.vue"; | ||||||
| import authService from "@/services/auth/auth-service"; | import authService from "@/services/auth/auth-service"; | ||||||
|  | import DiscussionForward from "@/views/discussions/DiscussionForward.vue"; | ||||||
|  | import NoDiscussion from "@/views/discussions/NoDiscussion.vue"; | ||||||
| import OwnLearningContentPage from "@/views/own-learning-content/OwnLearningContentPage.vue"; | import OwnLearningContentPage from "@/views/own-learning-content/OwnLearningContentPage.vue"; | ||||||
| import { allowRedirect, Redirect } from "@/utils/redirect.ts"; | import { allowRedirect, Redirect } from "@/utils/redirect.ts"; | ||||||
| 
 | 
 | ||||||
|  | @ -57,12 +59,6 @@ const router = createRouter({ | ||||||
|                     name: "UserClasses", |                     name: "UserClasses", | ||||||
|                     component: UserClasses, |                     component: UserClasses, | ||||||
|                 }, |                 }, | ||||||
|                 // TODO Re-enable this route when the discussion page is ready
 |  | ||||||
|                 // {
 |  | ||||||
|                 //     Path: "discussion",
 |  | ||||||
|                 //     Name: "UserDiscussions",
 |  | ||||||
|                 //     Component: UserDiscussions,
 |  | ||||||
|                 // },
 |  | ||||||
|             ], |             ], | ||||||
|         }, |         }, | ||||||
| 
 | 
 | ||||||
|  | @ -102,9 +98,23 @@ const router = createRouter({ | ||||||
|             meta: { requiresAuth: true }, |             meta: { requiresAuth: true }, | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             path: "/discussion/:id", |             path: "/discussion", | ||||||
|  |             name: "Discussions", | ||||||
|  |             component: NoDiscussion, | ||||||
|  |             meta: { requiresAuth: true }, | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             path: "/discussion/:hruid/:language/:learningObjectHruid", | ||||||
|             name: "SingleDiscussion", |             name: "SingleDiscussion", | ||||||
|             component: SingleDiscussion, |             component: SingleDiscussion, | ||||||
|  |             props: true, | ||||||
|  |             meta: { requiresAuth: true }, | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             path: "/discussion-reload/:hruid/:language/:learningObjectHruid", | ||||||
|  |             name: "DiscussionForwardWorkaround", | ||||||
|  |             component: DiscussionForward, | ||||||
|  |             props: true, | ||||||
|             meta: { requiresAuth: true }, |             meta: { requiresAuth: true }, | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|  |  | ||||||
							
								
								
									
										25
									
								
								frontend/src/views/discussions/DiscussionForward.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								frontend/src/views/discussions/DiscussionForward.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,25 @@ | ||||||
|  | <script setup lang="ts"> | ||||||
|  |     import type { Language } from "@/data-objects/language"; | ||||||
|  |     import { onMounted } from "vue"; | ||||||
|  |     import { useRouter } from "vue-router"; | ||||||
|  | 
 | ||||||
|  |     const router = useRouter(); | ||||||
|  | 
 | ||||||
|  |     const props = defineProps<{ | ||||||
|  |         hruid: string; | ||||||
|  |         language: Language; | ||||||
|  |         learningObjectHruid?: string; | ||||||
|  |     }>(); | ||||||
|  | 
 | ||||||
|  |     const discussionURL = "/discussion" + "/" + props.hruid + "/" + props.language + "/" + props.learningObjectHruid; | ||||||
|  | 
 | ||||||
|  |     onMounted(async () => { | ||||||
|  |         await router.replace(discussionURL); | ||||||
|  |     }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |     <main></main> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <style scoped></style> | ||||||
							
								
								
									
										105
									
								
								frontend/src/views/discussions/NoDiscussion.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								frontend/src/views/discussions/NoDiscussion.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,105 @@ | ||||||
|  | <script setup lang="ts"> | ||||||
|  |     import { useI18n } from "vue-i18n"; | ||||||
|  |     import DiscussionsSideBar from "@/components/DiscussionsSideBar.vue"; | ||||||
|  | 
 | ||||||
|  |     const { t } = useI18n(); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |     <DiscussionsSideBar></DiscussionsSideBar> | ||||||
|  |     <div> | ||||||
|  |         <p class="no-discussion-tip">{{ t("no-discussion-tip") }}</p> | ||||||
|  |     </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  |     .no-discussion-tip { | ||||||
|  |         display: flex; | ||||||
|  |         justify-content: center; | ||||||
|  |         align-items: center; | ||||||
|  |         height: 100vh; | ||||||
|  |         text-align: center; | ||||||
|  |         font-size: 18px; | ||||||
|  |         color: #666; | ||||||
|  |         padding: 0 20px; | ||||||
|  |     } | ||||||
|  |     .learning-path-title { | ||||||
|  |         white-space: normal; | ||||||
|  |     } | ||||||
|  |     .search-field-container { | ||||||
|  |         min-width: 250px; | ||||||
|  |     } | ||||||
|  |     .control-bar-above-content { | ||||||
|  |         margin-left: 5px; | ||||||
|  |         margin-right: 5px; | ||||||
|  |         margin-bottom: -30px; | ||||||
|  |         display: flex; | ||||||
|  |         justify-content: space-between; | ||||||
|  |     } | ||||||
|  |     .learning-object-view-container { | ||||||
|  |         padding-left: 20px; | ||||||
|  |         padding-right: 20px; | ||||||
|  |         padding-bottom: 20px; | ||||||
|  |     } | ||||||
|  |     .navigation-buttons-container { | ||||||
|  |         padding: 20px; | ||||||
|  |         display: flex; | ||||||
|  |         justify-content: space-between; | ||||||
|  |     } | ||||||
|  |     .assignment-indicator { | ||||||
|  |         position: absolute; | ||||||
|  |         bottom: 10px; | ||||||
|  |         left: 10px; | ||||||
|  |         padding: 4px 12px; | ||||||
|  |         border: 2px solid #f8bcbc; | ||||||
|  |         border-radius: 20px; | ||||||
|  |         color: #f36c6c; | ||||||
|  |         background-color: rgba(248, 188, 188, 0.1); | ||||||
|  |         font-weight: bold; | ||||||
|  |         font-family: Arial, sans-serif; | ||||||
|  |         font-size: 14px; | ||||||
|  |         text-transform: uppercase; | ||||||
|  |         z-index: 2; /* Less than modals/popups */ | ||||||
|  |     } | ||||||
|  |     .question-box { | ||||||
|  |         width: 100%; | ||||||
|  |         max-width: 400px; | ||||||
|  |         margin: 20px auto; | ||||||
|  |         font-family: sans-serif; | ||||||
|  |     } | ||||||
|  |     .input-wrapper { | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |         border: 1px solid #ccc; | ||||||
|  |         border-radius: 999px; | ||||||
|  |         padding: 8px 12px; | ||||||
|  |         box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .question-input { | ||||||
|  |         flex: 1; | ||||||
|  |         border: none; | ||||||
|  |         outline: none; | ||||||
|  |         font-size: 14px; | ||||||
|  |         background-color: transparent; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .question-input::placeholder { | ||||||
|  |         color: #999; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .discussion-link { | ||||||
|  |         margin-top: 8px; | ||||||
|  |         font-size: 13px; | ||||||
|  |         color: #444; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .discussion-link a { | ||||||
|  |         color: #3b82f6; /* blue */ | ||||||
|  |         text-decoration: none; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .discussion-link a:hover { | ||||||
|  |         text-decoration: underline; | ||||||
|  |     } | ||||||
|  | </style> | ||||||
|  | @ -1,7 +1,195 @@ | ||||||
| <script setup lang="ts"></script> | <script setup lang="ts"> | ||||||
|  |     import { Language } from "@/data-objects/language.ts"; | ||||||
|  |     import { computed, type ComputedRef, watch } from "vue"; | ||||||
|  |     import { useRoute } from "vue-router"; | ||||||
|  |     import { useGetLearningPathQuery } from "@/queries/learning-paths.ts"; | ||||||
|  |     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||||
|  |     import { LearningPathNode } from "@/data-objects/learning-paths/learning-path-node.ts"; | ||||||
|  |     import { useQuestionsQuery } from "@/queries/questions"; | ||||||
|  |     import type { QuestionsResponse } from "@/controllers/questions"; | ||||||
|  |     import type { LearningObjectIdentifierDTO } from "@dwengo-1/common/interfaces/learning-content"; | ||||||
|  |     import QandA from "@/components/QandA.vue"; | ||||||
|  |     import type { QuestionDTO } from "@dwengo-1/common/interfaces/question"; | ||||||
|  |     import DiscussionsSideBar from "@/components/DiscussionsSideBar.vue"; | ||||||
|  |     import QuestionBox from "@/components/QuestionBox.vue"; | ||||||
|  |     import { useI18n } from "vue-i18n"; | ||||||
|  | 
 | ||||||
|  |     const { t } = useI18n(); | ||||||
|  | 
 | ||||||
|  |     const route = useRoute(); | ||||||
|  | 
 | ||||||
|  |     const props = defineProps<{ | ||||||
|  |         hruid: string; | ||||||
|  |         language: Language; | ||||||
|  |         learningObjectHruid?: string; | ||||||
|  |     }>(); | ||||||
|  | 
 | ||||||
|  |     interface LearningPathPageQuery { | ||||||
|  |         forGroup?: string; | ||||||
|  |         assignmentNo?: string; | ||||||
|  |         classId?: string; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const query = computed(() => route.query as LearningPathPageQuery); | ||||||
|  | 
 | ||||||
|  |     const forGroup = computed(() => { | ||||||
|  |         if (query.value.forGroup && query.value.assignmentNo && query.value.classId) { | ||||||
|  |             return { | ||||||
|  |                 forGroup: parseInt(query.value.forGroup), | ||||||
|  |                 assignmentNo: parseInt(query.value.assignmentNo), | ||||||
|  |                 classId: query.value.classId, | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  |         return undefined; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const learningPathQueryResult = useGetLearningPathQuery(props.hruid, props.language, forGroup); | ||||||
|  | 
 | ||||||
|  |     const nodesList: ComputedRef<LearningPathNode[] | null> = computed( | ||||||
|  |         () => learningPathQueryResult.data.value?.nodesAsList ?? null, | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     const currentNode = computed(() => { | ||||||
|  |         const currentHruid = props.learningObjectHruid; | ||||||
|  |         return nodesList.value?.find((it) => it.learningobjectHruid === currentHruid); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const getQuestionsQuery = useQuestionsQuery( | ||||||
|  |         computed( | ||||||
|  |             () => | ||||||
|  |                 ({ | ||||||
|  |                     language: currentNode.value?.language, | ||||||
|  |                     hruid: currentNode.value?.learningobjectHruid, | ||||||
|  |                     version: currentNode.value?.version, | ||||||
|  |                 }) as LearningObjectIdentifierDTO, | ||||||
|  |         ), | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     watch( | ||||||
|  |         () => [route.params.hruid, route.params.language, route.params.learningObjectHruid], | ||||||
|  |         () => { | ||||||
|  |             //TODO: moet op een of andere manier createQuestionMutation opnieuw kunnen instellen | ||||||
|  |             //      Momenteel opgelost door de DiscussionsForward page workaround | ||||||
|  |         }, | ||||||
|  |     ); | ||||||
|  | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|     <main></main> |     <DiscussionsSideBar></DiscussionsSideBar> | ||||||
|  |     <div class="discussions-container"> | ||||||
|  |         <QuestionBox | ||||||
|  |             :hruid="props.hruid" | ||||||
|  |             :language="props.language" | ||||||
|  |             :learningObjectHruid="props.learningObjectHruid" | ||||||
|  |             :forGroup="forGroup" | ||||||
|  |             withTitle | ||||||
|  |         /> | ||||||
|  |         <h3>{{ t("questionsCapitalized") }}:</h3> | ||||||
|  |         <using-query-result | ||||||
|  |             :query-result="getQuestionsQuery" | ||||||
|  |             v-slot="questionsResponse: { data: QuestionsResponse }" | ||||||
|  |         > | ||||||
|  |             <QandA :questions="(questionsResponse.data.questions as QuestionDTO[]) ?? []" /> | ||||||
|  |         </using-query-result> | ||||||
|  |     </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped></style> | <style scoped> | ||||||
|  |     .discussions-container { | ||||||
|  |         margin: 20px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .learning-path-title { | ||||||
|  |         white-space: normal; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .search-field-container { | ||||||
|  |         min-width: 250px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .control-bar-above-content { | ||||||
|  |         margin-left: 5px; | ||||||
|  |         margin-right: 5px; | ||||||
|  |         margin-bottom: -30px; | ||||||
|  |         display: flex; | ||||||
|  |         justify-content: space-between; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .learning-object-view-container { | ||||||
|  |         padding-left: 20px; | ||||||
|  |         padding-right: 20px; | ||||||
|  |         padding-bottom: 20px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .navigation-buttons-container { | ||||||
|  |         padding: 20px; | ||||||
|  |         display: flex; | ||||||
|  |         justify-content: space-between; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .assignment-indicator { | ||||||
|  |         position: absolute; | ||||||
|  |         bottom: 10px; | ||||||
|  |         left: 10px; | ||||||
|  |         padding: 4px 12px; | ||||||
|  |         border: 2px solid #f8bcbc; | ||||||
|  |         border-radius: 20px; | ||||||
|  |         color: #f36c6c; | ||||||
|  |         background-color: rgba(248, 188, 188, 0.1); | ||||||
|  |         font-weight: bold; | ||||||
|  |         font-family: Arial, sans-serif; | ||||||
|  |         font-size: 14px; | ||||||
|  |         text-transform: uppercase; | ||||||
|  |         z-index: 2; /* Less than modals/popups */ | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .question-box { | ||||||
|  |         width: 100%; | ||||||
|  |         max-width: 400px; | ||||||
|  |         margin: 20px auto; | ||||||
|  |         font-family: sans-serif; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .input-wrapper { | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |         border: 1px solid #ccc; | ||||||
|  |         border-radius: 999px; | ||||||
|  |         padding: 8px 12px; | ||||||
|  |         box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .question-input { | ||||||
|  |         flex: 1; | ||||||
|  |         border: none; | ||||||
|  |         outline: none; | ||||||
|  |         font-size: 14px; | ||||||
|  |         background-color: transparent; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .question-input::placeholder { | ||||||
|  |         color: #999; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .send-button { | ||||||
|  |         background: none; | ||||||
|  |         border: none; | ||||||
|  |         cursor: pointer; | ||||||
|  |         font-size: 16px; | ||||||
|  |         color: #555; | ||||||
|  |         transition: color 0.2s ease; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .send-button:hover { | ||||||
|  |         color: #000; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .discussion-link a { | ||||||
|  |         color: #3b82f6; /* blue */ | ||||||
|  |         text-decoration: none; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .discussion-link a:hover { | ||||||
|  |         text-decoration: underline; | ||||||
|  |     } | ||||||
|  | </style> | ||||||
|  |  | ||||||
|  | @ -17,11 +17,11 @@ | ||||||
|     import type { QuestionsResponse } from "@/controllers/questions"; |     import type { QuestionsResponse } from "@/controllers/questions"; | ||||||
|     import type { LearningObjectIdentifierDTO } from "@dwengo-1/common/interfaces/learning-content"; |     import type { LearningObjectIdentifierDTO } from "@dwengo-1/common/interfaces/learning-content"; | ||||||
|     import QandA from "@/components/QandA.vue"; |     import QandA from "@/components/QandA.vue"; | ||||||
|     import type { QuestionData, QuestionDTO } from "@dwengo-1/common/interfaces/question"; |     import type { QuestionDTO } from "@dwengo-1/common/interfaces/question"; | ||||||
|     import { useStudentAssignmentsQuery, useStudentGroupsQuery } from "@/queries/students"; |     import { useStudentAssignmentsQuery, useStudentGroupsQuery } from "@/queries/students"; | ||||||
|     import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; |     import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; | ||||||
|     import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; |  | ||||||
|     import QuestionNotification from "@/components/QuestionNotification.vue"; |     import QuestionNotification from "@/components/QuestionNotification.vue"; | ||||||
|  |     import QuestionBox from "@/components/QuestionBox.vue"; | ||||||
|     import { AccountType } from "@dwengo-1/common/util/account-types"; |     import { AccountType } from "@dwengo-1/common/util/account-types"; | ||||||
| 
 | 
 | ||||||
|     const router = useRouter(); |     const router = useRouter(); | ||||||
|  | @ -150,12 +150,6 @@ | ||||||
|     const studentAssignmentsQueryResult = useStudentAssignmentsQuery( |     const studentAssignmentsQueryResult = useStudentAssignmentsQuery( | ||||||
|         authService.authState.user?.profile.preferred_username, |         authService.authState.user?.profile.preferred_username, | ||||||
|     ); |     ); | ||||||
|     const pathIsAssignment = computed(() => { |  | ||||||
|         const assignments = (studentAssignmentsQueryResult.data.value?.assignments as AssignmentDTO[]) || []; |  | ||||||
|         return assignments.some( |  | ||||||
|             (assignment) => assignment.learningPath === props.hruid && assignment.language === props.language, |  | ||||||
|         ); |  | ||||||
|     }); |  | ||||||
| 
 | 
 | ||||||
|     const loID: LearningObjectIdentifierDTO = { |     const loID: LearningObjectIdentifierDTO = { | ||||||
|         hruid: props.learningObjectHruid as string, |         hruid: props.learningObjectHruid as string, | ||||||
|  | @ -166,31 +160,16 @@ | ||||||
| 
 | 
 | ||||||
|     const questionInput = ref(""); |     const questionInput = ref(""); | ||||||
| 
 | 
 | ||||||
|     function submitQuestion(): void { |     const discussionLink = computed( | ||||||
|         const assignments = studentAssignmentsQueryResult.data.value?.assignments as AssignmentDTO[]; |         () => | ||||||
|         const assignment = assignments.find( |             "/discussion" + | ||||||
|             (assignment) => assignment.learningPath === props.hruid && assignment.language === props.language, |             "/" + | ||||||
|         ); |             props.hruid + | ||||||
|         const groups = groupsQueryResult.data.value?.groups as GroupDTO[]; |             "/" + | ||||||
|         const group = groups?.find((group) => group.assignment === assignment?.id) as GroupDTO; |             currentNode.value?.language + | ||||||
|         const questionData: QuestionData = { |             "/" + | ||||||
|             author: authService.authState.user?.profile.preferred_username, |             currentNode.value?.learningobjectHruid, | ||||||
|             content: questionInput.value, |     ); | ||||||
|             inGroup: group, //TODO: POST response zegt dat dit null is??? |  | ||||||
|         }; |  | ||||||
|         if (questionInput.value !== "") { |  | ||||||
|             createQuestionMutation.mutate(questionData, { |  | ||||||
|                 onSuccess: async () => { |  | ||||||
|                     questionInput.value = ""; // Clear the input field after submission |  | ||||||
|                     await getQuestionsQuery.refetch(); // Reload the questions |  | ||||||
|                 }, |  | ||||||
|                 onError: (_) => { |  | ||||||
|                     // TODO Handle error |  | ||||||
|                     // - console.error(e); |  | ||||||
|                 }, |  | ||||||
|             }); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|  | @ -236,7 +215,7 @@ | ||||||
|                         </p> |                         </p> | ||||||
|                     </template> |                     </template> | ||||||
|                 </v-list-item> |                 </v-list-item> | ||||||
|                 <v-list-itemF |                 <v-list-item | ||||||
|                     v-if=" |                     v-if=" | ||||||
|                         query.classId && query.assignmentNo && authService.authState.activeRole === AccountType.Teacher |                         query.classId && query.assignmentNo && authService.authState.activeRole === AccountType.Teacher | ||||||
|                     " |                     " | ||||||
|  | @ -248,7 +227,7 @@ | ||||||
|                             v-model="forGroupQueryParam" |                             v-model="forGroupQueryParam" | ||||||
|                         /> |                         /> | ||||||
|                     </template> |                     </template> | ||||||
|                 </v-list-itemF> |                 </v-list-item> | ||||||
|                 <v-divider></v-divider> |                 <v-divider></v-divider> | ||||||
|                 <div> |                 <div> | ||||||
|                     <using-query-result |                     <using-query-result | ||||||
|  | @ -329,25 +308,6 @@ | ||||||
|                 v-if="currentNode" |                 v-if="currentNode" | ||||||
|             ></learning-object-view> |             ></learning-object-view> | ||||||
|         </div> |         </div> | ||||||
|         <div |  | ||||||
|             v-if="authService.authState.activeRole === AccountType.Student && pathIsAssignment" |  | ||||||
|             class="question-box" |  | ||||||
|         > |  | ||||||
|             <div class="input-wrapper"> |  | ||||||
|                 <input |  | ||||||
|                     type="text" |  | ||||||
|                     placeholder="question : ..." |  | ||||||
|                     class="question-input" |  | ||||||
|                     v-model="questionInput" |  | ||||||
|                 /> |  | ||||||
|                 <button |  | ||||||
|                     @click="submitQuestion" |  | ||||||
|                     class="send-button" |  | ||||||
|                 > |  | ||||||
|                     ▶ |  | ||||||
|                 </button> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|         <div class="navigation-buttons-container"> |         <div class="navigation-buttons-container"> | ||||||
|             <v-btn |             <v-btn | ||||||
|                 prepend-icon="mdi-chevron-left" |                 prepend-icon="mdi-chevron-left" | ||||||
|  | @ -367,15 +327,43 @@ | ||||||
|             </v-btn> |             </v-btn> | ||||||
|         </div> |         </div> | ||||||
|         <using-query-result |         <using-query-result | ||||||
|  |             v-if="forGroup" | ||||||
|             :query-result="getQuestionsQuery" |             :query-result="getQuestionsQuery" | ||||||
|             v-slot="questionsResponse: { data: QuestionsResponse }" |             v-slot="questionsResponse: { data: QuestionsResponse }" | ||||||
|         > |         > | ||||||
|  |             <v-divider :thickness="6"></v-divider> | ||||||
|  |             <div class="question-header"> | ||||||
|  |                 <span class="question-title">{{ t("questions") }}</span> | ||||||
|  |                 <span class="discussion-link-text"> | ||||||
|  |                     {{ t("view-questions") }} | ||||||
|  |                     <router-link :to="discussionLink"> | ||||||
|  |                         {{ t("discussions") }} | ||||||
|  |                     </router-link> | ||||||
|  |                 </span> | ||||||
|  |             </div> | ||||||
|  |             <QuestionBox | ||||||
|  |                 :hruid="props.hruid" | ||||||
|  |                 :language="props.language" | ||||||
|  |                 :learningObjectHruid="props.learningObjectHruid" | ||||||
|  |                 :forGroup="forGroup" | ||||||
|  |             /> | ||||||
|             <QandA :questions="(questionsResponse.data.questions as QuestionDTO[]) ?? []" /> |             <QandA :questions="(questionsResponse.data.questions as QuestionDTO[]) ?? []" /> | ||||||
|         </using-query-result> |         </using-query-result> | ||||||
|     </using-query-result> |     </using-query-result> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
|  |     .question-title { | ||||||
|  |         color: #0e6942; | ||||||
|  |         text-transform: uppercase; | ||||||
|  |         font-weight: bolder; | ||||||
|  |         font-size: 24px; | ||||||
|  |     } | ||||||
|  |     .question-header { | ||||||
|  |         display: flex; | ||||||
|  |         justify-content: space-between; | ||||||
|  |         padding: 10px; | ||||||
|  |     } | ||||||
|     .learning-path-title { |     .learning-path-title { | ||||||
|         white-space: normal; |         white-space: normal; | ||||||
|     } |     } | ||||||
|  | @ -414,45 +402,6 @@ | ||||||
|         text-transform: uppercase; |         text-transform: uppercase; | ||||||
|         z-index: 2; /* Less than modals/popups */ |         z-index: 2; /* Less than modals/popups */ | ||||||
|     } |     } | ||||||
|     .question-box { |  | ||||||
|         width: 100%; |  | ||||||
|         max-width: 400px; |  | ||||||
|         margin: 20px auto; |  | ||||||
|         font-family: sans-serif; |  | ||||||
|     } |  | ||||||
|     .input-wrapper { |  | ||||||
|         display: flex; |  | ||||||
|         align-items: center; |  | ||||||
|         border: 1px solid #ccc; |  | ||||||
|         border-radius: 999px; |  | ||||||
|         padding: 8px 12px; |  | ||||||
|         box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     .question-input { |  | ||||||
|         flex: 1; |  | ||||||
|         border: none; |  | ||||||
|         outline: none; |  | ||||||
|         font-size: 14px; |  | ||||||
|         background-color: transparent; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     .question-input::placeholder { |  | ||||||
|         color: #999; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     .send-button { |  | ||||||
|         background: none; |  | ||||||
|         border: none; |  | ||||||
|         cursor: pointer; |  | ||||||
|         font-size: 16px; |  | ||||||
|         color: #555; |  | ||||||
|         transition: color 0.2s ease; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     .send-button:hover { |  | ||||||
|         color: #000; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     .discussion-link { |     .discussion-link { | ||||||
|         margin-top: 8px; |         margin-top: 8px; | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Timo De Meyst
						Timo De Meyst