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 { FALLBACK_LANG } from '../config.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.
|
||||
*/
|
||||
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;
|
||||
if (admin) {
|
||||
const paths = await learningPathService.getLearningPathsAdministratedBy(admin as string);
|
||||
|
@ -59,6 +59,19 @@ export async function getLearningPaths(req: Request, res: Response): Promise<voi
|
|||
return;
|
||||
} else {
|
||||
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(
|
||||
|
|
|
@ -7,14 +7,20 @@ import { LearningPathTransition } from '../../entities/content/learning-path-tra
|
|||
|
||||
export class LearningPathRepository extends DwengoEntityRepository<LearningPath> {
|
||||
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
|
||||
* 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.
|
||||
*/
|
||||
public async findByQueryStringAndLanguage(query: string, language: Language): Promise<LearningPath[]> {
|
||||
|
|
|
@ -18,13 +18,14 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
|
|||
content: question.content,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
await this.insert(questionEntity);
|
||||
questionEntity.learningObjectHruid = question.loId.hruid;
|
||||
questionEntity.learningObjectLanguage = question.loId.language;
|
||||
questionEntity.learningObjectVersion = question.loId.version;
|
||||
questionEntity.author = question.author;
|
||||
questionEntity.inGroup = question.inGroup;
|
||||
questionEntity.content = question.content;
|
||||
return await this.insert(questionEntity);
|
||||
return questionEntity;
|
||||
}
|
||||
public async findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> {
|
||||
return this.findAll({
|
||||
|
|
|
@ -62,6 +62,7 @@ const dwengoApiLearningPathProvider: LearningPathProvider = {
|
|||
data: learningPaths,
|
||||
};
|
||||
},
|
||||
|
||||
async searchLearningPaths(query: string, language: string, personalizedFor: Group): Promise<LearningPath[]> {
|
||||
const apiUrl = `${DWENGO_API_BASE}/learningPath/search`;
|
||||
const params = { all: query, language };
|
||||
|
@ -75,7 +76,8 @@ const dwengoApiLearningPathProvider: LearningPathProvider = {
|
|||
},
|
||||
|
||||
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,
|
||||
testLearningObjectMultipleChoice,
|
||||
} 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 { getTestGroup01, getTestGroup02 } from '../../test_assets/assignments/groups.testdata';
|
||||
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 { getFooFighters, getLimpBizkit } from '../../test_assets/users/teachers.testdata';
|
||||
import { mapToTeacherDTO } from '../../../src/interfaces/teacher';
|
||||
|
||||
function expectBranchingObjectNode(result: LearningPathResponse): LearningObjectNode {
|
||||
const branchingObjectMatches = result.data![0].nodes.filter((it) => it.learningobject_hruid === testLearningObjectMultipleChoice.hruid);
|
||||
|
@ -33,6 +36,8 @@ describe('DatabaseLearningPathProvider', () => {
|
|||
let finalLearningObject: RequiredEntityData<LearningObject>;
|
||||
let groupA: Group;
|
||||
let groupB: Group;
|
||||
let teacherA: Teacher;
|
||||
let teacherB: Teacher;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupTestApp();
|
||||
|
@ -42,6 +47,8 @@ describe('DatabaseLearningPathProvider', () => {
|
|||
finalLearningObject = testLearningObjectEssayQuestion;
|
||||
groupA = getTestGroup01();
|
||||
groupB = getTestGroup02();
|
||||
teacherA = getFooFighters();
|
||||
teacherB = getLimpBizkit();
|
||||
|
||||
// Place different submissions for group A and B.
|
||||
const submissionRepo = getSubmissionRepository();
|
||||
|
@ -140,4 +147,18 @@ describe('DatabaseLearningPathProvider', () => {
|
|||
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,
|
||||
testLearningObjectPnNotebooks,
|
||||
} from './learning-objects.testdata';
|
||||
import { getLimpBizkit } from '../users/teachers.testdata';
|
||||
|
||||
export function makeTestLearningPaths(_em: EntityManager): LearningPath[] {
|
||||
const learningPath01 = mapToLearningPath(testLearningPath01, []);
|
||||
const learningPath02 = mapToLearningPath(testLearningPath02, []);
|
||||
learningPath02.admins = [getLimpBizkit()];
|
||||
|
||||
const partiallyDatabasePartiallyDwengoApiLearningPath = mapToLearningPath(testPartiallyDatabaseAndPartiallyDwengoApiLearningPath, []);
|
||||
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") }}
|
||||
</v-btn>
|
||||
<!-- TODO Re-enable this button when the discussion page is ready -->
|
||||
<!-- <v-btn-->
|
||||
<!-- class="menu_item"-->
|
||||
<!-- variant="text"-->
|
||||
<!-- to="/user/discussion"-->
|
||||
<!-- >-->
|
||||
<!-- {{ t("discussions") }}-->
|
||||
<!-- </v-btn>-->
|
||||
<v-btn
|
||||
class="menu_item"
|
||||
variant="text"
|
||||
to="/discussion"
|
||||
>
|
||||
{{ t("discussions") }}
|
||||
</v-btn>
|
||||
</v-toolbar-items>
|
||||
<v-menu
|
||||
open-on-hover
|
||||
|
@ -231,7 +230,7 @@
|
|||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
to="/user/discussion"
|
||||
to="/discussion"
|
||||
link
|
||||
>
|
||||
<v-list-item-title class="menu_item">{{ t("discussions") }}</v-list-item-title>
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import type { QuestionDTO } from "@dwengo-1/common/interfaces/question";
|
||||
import SingleQuestion from "./SingleQuestion.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
defineProps<{
|
||||
questions: QuestionDTO[];
|
||||
|
@ -8,13 +11,28 @@
|
|||
</script>
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
v-for="question in questions"
|
||||
:key="(question.sequenceNumber, question.content)"
|
||||
class="border rounded-2xl p-4 shadow-sm bg-white"
|
||||
>
|
||||
<SingleQuestion :question="question"></SingleQuestion>
|
||||
<div v-if="questions.length != 0">
|
||||
<div
|
||||
v-for="question in questions"
|
||||
:key="(question.sequenceNumber, question.content)"
|
||||
>
|
||||
<SingleQuestion :question="question"></SingleQuestion>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p class="no-questions">{{ t("no-questions") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</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 type { AnswersResponse } from "@/controllers/answers";
|
||||
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 { useI18n } from "vue-i18n";
|
||||
import { AccountType } from "@dwengo-1/common/util/account-types";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
question: QuestionDTO;
|
||||
}>();
|
||||
|
||||
const expanded = ref(false);
|
||||
const answersContainer = ref<HTMLElement | null>(null); // Ref for the answers container
|
||||
|
||||
function toggle(): void {
|
||||
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 {
|
||||
|
@ -49,141 +66,121 @@
|
|||
createAnswerMutation.mutate(answerData, {
|
||||
onSuccess: async () => {
|
||||
answer.value = "";
|
||||
expanded.value = true;
|
||||
await answersQuery.refetch();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function displayNameFor(user: UserDTO) {
|
||||
if (user.firstName && user.lastName) {
|
||||
return `${user.firstName} ${user.lastName}`;
|
||||
} else {
|
||||
return user.username;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<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">{{
|
||||
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"
|
||||
<v-card class="question-card">
|
||||
<v-card-title class="author-title">{{ displayNameFor(question.author) }}</v-card-title>
|
||||
<v-card-subtitle>{{ formatDate(question.timestamp) }}</v-card-subtitle>
|
||||
<v-card-text>
|
||||
{{ question.content }}
|
||||
</v-card-text>
|
||||
<template
|
||||
v-slot:actions
|
||||
v-if="
|
||||
authService.authState.activeRole === AccountType.Teacher ||
|
||||
answersQuery.data?.value?.answers?.length > 0
|
||||
"
|
||||
>
|
||||
▶
|
||||
</button>
|
||||
</div>
|
||||
<using-query-result
|
||||
:query-result="answersQuery"
|
||||
v-slot="answersResponse: { data: AnswersResponse }"
|
||||
>
|
||||
<button
|
||||
v-if="answersResponse.data.answers && answersResponse.data.answers.length > 0"
|
||||
@click="toggle()"
|
||||
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;
|
||||
"
|
||||
<div class="question-actions-container">
|
||||
<v-textarea
|
||||
v-if="authService.authState.activeRole === AccountType.Teacher"
|
||||
:label="t('answer-input-placeholder')"
|
||||
v-model="answer"
|
||||
class="answer-field"
|
||||
density="compact"
|
||||
rows="1"
|
||||
variant="outlined"
|
||||
auto-grow
|
||||
>
|
||||
<span class="font-semibold text-lg text-gray-800">{{ answer.author.username }}</span>
|
||||
<span class="text-sm text-gray-500">{{ formatDate(answer.timestamp) }}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-gray-700 mb-3"
|
||||
style="margin-left: 10px"
|
||||
<template v-slot:append-inner>
|
||||
<v-btn
|
||||
icon="mdi mdi-send"
|
||||
size="small"
|
||||
variant="plain"
|
||||
class="answer-button"
|
||||
@click="submitAnswer"
|
||||
/>
|
||||
</template>
|
||||
</v-textarea>
|
||||
<using-query-result
|
||||
:query-result="answersQuery"
|
||||
v-slot="answersResponse: { data: AnswersResponse }"
|
||||
>
|
||||
{{ answer.content }}
|
||||
</div>
|
||||
<v-btn
|
||||
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>
|
||||
</using-query-result>
|
||||
</template>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.answer-input {
|
||||
flex-grow: 1;
|
||||
outline: none;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #374151; /* gray-700 */
|
||||
font-size: 0.875rem; /* smaller font size */
|
||||
.answer-field {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.answer-input::placeholder {
|
||||
color: #9ca3af; /* gray-400 */
|
||||
.answer-button {
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #d1d5db; /* gray-300 */
|
||||
border-radius: 9999px;
|
||||
padding: 0.5rem 1rem;
|
||||
max-width: 28rem;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.question-card {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.question-actions-container {
|
||||
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>
|
||||
|
|
|
@ -181,5 +181,15 @@
|
|||
"current-groups": "Aktuelle Gruppen",
|
||||
"group-size-label": "Gruppengröße",
|
||||
"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",
|
||||
"assignments": "Assignments",
|
||||
"classes": "Classes",
|
||||
"discussions": "discussions",
|
||||
"discussions": "Discussions",
|
||||
"logout": "log out",
|
||||
"error_title": "Error",
|
||||
"previous": "Previous",
|
||||
|
@ -182,5 +182,15 @@
|
|||
"current-groups": "Current groups",
|
||||
"group-size-label": "Group size",
|
||||
"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",
|
||||
"group-size-label": "Taille des groupes",
|
||||
"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",
|
||||
"assignments": "Opdrachten",
|
||||
"classes": "Klassen",
|
||||
"discussions": "discussies",
|
||||
"discussions": "Discussies",
|
||||
"logout": "log uit",
|
||||
"error_title": "Fout",
|
||||
"previous": "Vorige",
|
||||
|
@ -181,5 +181,15 @@
|
|||
"current-groups": "Huidige groepen",
|
||||
"group-size-label": "Grootte van groepen",
|
||||
"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 LearningObjectView from "@/views/learning-paths/learning-object/LearningObjectView.vue";
|
||||
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 { allowRedirect, Redirect } from "@/utils/redirect.ts";
|
||||
|
||||
|
@ -57,12 +59,6 @@ const router = createRouter({
|
|||
name: "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 },
|
||||
},
|
||||
{
|
||||
path: "/discussion/:id",
|
||||
path: "/discussion",
|
||||
name: "Discussions",
|
||||
component: NoDiscussion,
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "/discussion/:hruid/:language/:learningObjectHruid",
|
||||
name: "SingleDiscussion",
|
||||
component: SingleDiscussion,
|
||||
props: true,
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "/discussion-reload/:hruid/:language/:learningObjectHruid",
|
||||
name: "DiscussionForwardWorkaround",
|
||||
component: DiscussionForward,
|
||||
props: 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>
|
||||
<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>
|
||||
|
||||
<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 { LearningObjectIdentifierDTO } from "@dwengo-1/common/interfaces/learning-content";
|
||||
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 type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment";
|
||||
import type { GroupDTO } from "@dwengo-1/common/interfaces/group";
|
||||
import QuestionNotification from "@/components/QuestionNotification.vue";
|
||||
import QuestionBox from "@/components/QuestionBox.vue";
|
||||
import { AccountType } from "@dwengo-1/common/util/account-types";
|
||||
|
||||
const router = useRouter();
|
||||
|
@ -150,12 +150,6 @@
|
|||
const studentAssignmentsQueryResult = useStudentAssignmentsQuery(
|
||||
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 = {
|
||||
hruid: props.learningObjectHruid as string,
|
||||
|
@ -166,31 +160,16 @@
|
|||
|
||||
const questionInput = ref("");
|
||||
|
||||
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, //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);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
const discussionLink = computed(
|
||||
() =>
|
||||
"/discussion" +
|
||||
"/" +
|
||||
props.hruid +
|
||||
"/" +
|
||||
currentNode.value?.language +
|
||||
"/" +
|
||||
currentNode.value?.learningobjectHruid,
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -236,7 +215,7 @@
|
|||
</p>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-itemF
|
||||
<v-list-item
|
||||
v-if="
|
||||
query.classId && query.assignmentNo && authService.authState.activeRole === AccountType.Teacher
|
||||
"
|
||||
|
@ -248,7 +227,7 @@
|
|||
v-model="forGroupQueryParam"
|
||||
/>
|
||||
</template>
|
||||
</v-list-itemF>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
<div>
|
||||
<using-query-result
|
||||
|
@ -329,25 +308,6 @@
|
|||
v-if="currentNode"
|
||||
></learning-object-view>
|
||||
</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">
|
||||
<v-btn
|
||||
prepend-icon="mdi-chevron-left"
|
||||
|
@ -367,15 +327,43 @@
|
|||
</v-btn>
|
||||
</div>
|
||||
<using-query-result
|
||||
v-if="forGroup"
|
||||
:query-result="getQuestionsQuery"
|
||||
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[]) ?? []" />
|
||||
</using-query-result>
|
||||
</using-query-result>
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
white-space: normal;
|
||||
}
|
||||
|
@ -414,45 +402,6 @@
|
|||
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 {
|
||||
margin-top: 8px;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue