From cb29b7b9e6a6d9032cb059c9d72f471c6b2308c4 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 22 Oct 2024 17:46:04 +0200 Subject: [PATCH 01/30] Enable references for FAQ's --- src/main/webapp/app/entities/course.model.ts | 2 + .../course-conversations.component.ts | 1 + .../webapp/app/shared/metis/metis.service.ts | 33 ++++++++- .../webapp/app/shared/metis/metis.util.ts | 1 + .../posting-content.components.ts | 5 +- .../posting-markdown-editor.component.ts | 2 + .../communication/faq-reference.action.ts | 73 +++++++++++++++++++ src/main/webapp/i18n/de/metis.json | 3 +- src/main/webapp/i18n/en/metis.json | 3 +- 9 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 src/main/webapp/app/shared/monaco-editor/model/actions/communication/faq-reference.action.ts diff --git a/src/main/webapp/app/entities/course.model.ts b/src/main/webapp/app/entities/course.model.ts index 6cddcfe61040..093772e76d42 100644 --- a/src/main/webapp/app/entities/course.model.ts +++ b/src/main/webapp/app/entities/course.model.ts @@ -13,6 +13,7 @@ import { TutorialGroup } from 'app/entities/tutorial-group/tutorial-group.model' import { TutorialGroupsConfiguration } from 'app/entities/tutorial-group/tutorial-groups-configuration.model'; import { LearningPath } from 'app/entities/competency/learning-path.model'; import { Prerequisite } from 'app/entities/prerequisite.model'; +import { Faq } from 'app/entities/faq.model'; export enum CourseInformationSharingConfiguration { COMMUNICATION_AND_MESSAGING = 'COMMUNICATION_AND_MESSAGING', @@ -98,6 +99,7 @@ export class Course implements BaseEntity { public exercises?: Exercise[]; public lectures?: Lecture[]; + public faqs?: Faq[]; public competencies?: Competency[]; public prerequisites?: Prerequisite[]; public learningPathsEnabled?: boolean; diff --git a/src/main/webapp/app/overview/course-conversations/course-conversations.component.ts b/src/main/webapp/app/overview/course-conversations/course-conversations.component.ts index 742999cf7aa7..9293d013bafc 100644 --- a/src/main/webapp/app/overview/course-conversations/course-conversations.component.ts +++ b/src/main/webapp/app/overview/course-conversations/course-conversations.component.ts @@ -153,6 +153,7 @@ export class CourseConversationsComponent implements OnInit, OnDestroy { private setupMetis() { this.metisService.setPageType(PageType.OVERVIEW); this.metisService.setCourse(this.course!); + this.metisService.setFaqs(this.course); } ngOnInit(): void { diff --git a/src/main/webapp/app/shared/metis/metis.service.ts b/src/main/webapp/app/shared/metis/metis.service.ts index a2badd9dba73..f15e852d2d94 100644 --- a/src/main/webapp/app/shared/metis/metis.service.ts +++ b/src/main/webapp/app/shared/metis/metis.service.ts @@ -6,7 +6,7 @@ import { User } from 'app/core/user/user.model'; import { AccountService } from 'app/core/auth/account.service'; import { Course } from 'app/entities/course.model'; import { Posting } from 'app/entities/metis/posting.model'; -import { Injectable, OnDestroy } from '@angular/core'; +import { Injectable, OnDestroy, inject } from '@angular/core'; import { AnswerPostService } from 'app/shared/metis/answer-post.service'; import { AnswerPost } from 'app/entities/metis/answer-post.model'; import { Reaction } from 'app/entities/metis/reaction.model'; @@ -31,6 +31,8 @@ import { Conversation, ConversationDTO } from 'app/entities/metis/conversation/c import { ChannelDTO, ChannelSubType, getAsChannelDTO } from 'app/entities/metis/conversation/channel.model'; import { ConversationService } from 'app/shared/metis/conversations/conversation.service'; import { NotificationService } from 'app/shared/notification/notification.service'; +import { FaqService } from 'app/faq/faq.service'; +import { Faq } from 'app/entities/faq.model'; @Injectable() export class MetisService implements OnDestroy { @@ -50,6 +52,8 @@ export class MetisService implements OnDestroy { private courseWideTopicSubscription: Subscription; + private faqService = inject(FaqService); + constructor( protected postService: PostService, protected answerPostService: AnswerPostService, @@ -145,6 +149,23 @@ export class MetisService implements OnDestroy { } } + /** + * set course property before using metis service + * @param {Course} course in which the metis service is used + */ + setFaqs(course: Course | undefined): void { + if (course) { + this.faqService + .findAllByCourseId(this.courseId) + .pipe(map((res: HttpResponse) => res.body)) + .subscribe({ + next: (res: Faq[]) => { + course.faqs = res; + }, + }); + } + } + /** * to be used to set posts from outside * @param {Post[]} posts that are managed by metis service @@ -494,6 +515,16 @@ export class MetisService implements OnDestroy { } } + /** + * returns the router link required for navigating to the exercise referenced within a faq + * @param {string} faqId ID of the faq be navigated to + * @return {string} router link of the exercise + */ + getLinkForFaq(faqId: string): string { + console.log(faqId); + return `/courses/${this.getCourse().id}/faq`; + } + /** * determines the routing params required for navigating to the detail view of the given post * @param {Post} post to be navigated to diff --git a/src/main/webapp/app/shared/metis/metis.util.ts b/src/main/webapp/app/shared/metis/metis.util.ts index 05a020433349..2b2036b3d7ff 100644 --- a/src/main/webapp/app/shared/metis/metis.util.ts +++ b/src/main/webapp/app/shared/metis/metis.util.ts @@ -105,6 +105,7 @@ export enum ReferenceType { FILE_UPLOAD = 'file-upload', USER = 'USER', CHANNEL = 'CHANNEL', + FAQ = 'FAQ', } export enum UserRole { diff --git a/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts b/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts index 836f1fad6390..f9e751eb7321 100644 --- a/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts +++ b/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts @@ -108,7 +108,8 @@ export class PostingContentComponent implements OnInit, OnChanges, OnDestroy { ReferenceType.MODELING === referenceType || ReferenceType.QUIZ === referenceType || ReferenceType.TEXT === referenceType || - ReferenceType.FILE_UPLOAD === referenceType + ReferenceType.FILE_UPLOAD === referenceType || + ReferenceType.FAQ == referenceType ) { // reference opening tag: [{referenceType}] (wrapped between 2 characters) // reference closing tag: [/referenceType] (wrapped between 3 characters) @@ -201,7 +202,7 @@ export class PostingContentComponent implements OnInit, OnChanges, OnDestroy { // Group 9: reference pattern for Users // globally searched for, i.e. no return after first match const pattern = - /(?#\d+)|(?\[programming].*?\[\/programming])|(?\[modeling].*?\[\/modeling])|(?\[quiz].*?\[\/quiz])|(?\[text].*?\[\/text])|(?\[file-upload].*?\[\/file-upload])|(?\[lecture].*?\[\/lecture])|(?\[attachment].*?\[\/attachment])|(?\[lecture-unit].*?\[\/lecture-unit])|(?\[slide].*?\[\/slide])|(?\[user].*?\[\/user])|(?\[channel].*?\[\/channel])/g; + /(?#\d+)|(?\[programming].*?\[\/programming])|(?\[modeling].*?\[\/modeling])|(?\[quiz].*?\[\/quiz])|(?\[text].*?\[\/text])|(?\[file-upload].*?\[\/file-upload])|(?\[lecture].*?\[\/lecture])|(?\[attachment].*?\[\/attachment])|(?\[lecture-unit].*?\[\/lecture-unit])|(?\[slide].*?\[\/slide])|(?\[user].*?\[\/user])|(?\[channel].*?\[\/channel])|(?\[faq].*?\[\/faq])/g; // array with PatternMatch objects per reference found in the posting content const patternMatches: PatternMatch[] = []; diff --git a/src/main/webapp/app/shared/metis/posting-markdown-editor/posting-markdown-editor.component.ts b/src/main/webapp/app/shared/metis/posting-markdown-editor/posting-markdown-editor.component.ts index 2ccacb086850..ace2400feda9 100644 --- a/src/main/webapp/app/shared/metis/posting-markdown-editor/posting-markdown-editor.component.ts +++ b/src/main/webapp/app/shared/metis/posting-markdown-editor/posting-markdown-editor.component.ts @@ -30,6 +30,7 @@ import { ChannelReferenceAction } from 'app/shared/monaco-editor/model/actions/c import { UserMentionAction } from 'app/shared/monaco-editor/model/actions/communication/user-mention.action'; import { ExerciseReferenceAction } from 'app/shared/monaco-editor/model/actions/communication/exercise-reference.action'; import { LectureAttachmentReferenceAction } from 'app/shared/monaco-editor/model/actions/communication/lecture-attachment-reference.action'; +import { FaqReferenceAction } from 'app/shared/monaco-editor/model/actions/communication/faq-reference.action'; @Component({ selector: 'jhi-posting-markdown-editor', @@ -84,6 +85,7 @@ export class PostingMarkdownEditorComponent implements OnInit, ControlValueAcces new CodeBlockAction(), ...messagingOnlyActions, new ExerciseReferenceAction(this.metisService), + new FaqReferenceAction(this.metisService), ]; this.lectureAttachmentReferenceAction = new LectureAttachmentReferenceAction(this.metisService, this.lectureService); diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/communication/faq-reference.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/communication/faq-reference.action.ts new file mode 100644 index 000000000000..08237f3449e9 --- /dev/null +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/communication/faq-reference.action.ts @@ -0,0 +1,73 @@ +import { TranslateService } from '@ngx-translate/core'; +import { MetisService } from 'app/shared/metis/metis.service'; +import { TextEditorDomainActionWithOptions } from 'app/shared/monaco-editor/model/actions/text-editor-domain-action-with-options.model'; +import { ValueItem } from 'app/shared/markdown-editor/value-item.model'; +import { Disposable } from 'app/shared/monaco-editor/model/actions/monaco-editor.util'; +import { TextEditor } from 'app/shared/monaco-editor/model/actions/adapter/text-editor.interface'; +import { TextEditorCompletionItem, TextEditorCompletionItemKind } from 'app/shared/monaco-editor/model/actions/adapter/text-editor-completion-item.model'; +import { TextEditorRange } from 'app/shared/monaco-editor/model/actions/adapter/text-editor-range.model'; + +/** + * Action to insert a reference to a faq into the editor. Users that type a / will see a list of available faqs to reference. + */ +export class FaqReferenceAction extends TextEditorDomainActionWithOptions { + static readonly ID = 'faq-reference.action'; + static readonly DEFAULT_INSERT_TEXT = '/faq'; + + disposableCompletionProvider?: Disposable; + + constructor(private readonly metisService: MetisService) { + super(FaqReferenceAction.ID, 'artemisApp.metis.editor.faq'); + } + + /** + * Registers this action in the provided editor. This will register a completion provider that shows the available faqs. + * @param editor The editor to register the completion provider for. + * @param translateService The translate service to use for translations. + */ + register(editor: TextEditor, translateService: TranslateService): void { + super.register(editor, translateService); + const faqs = this.metisService.getCourse().faqs ?? []; + console.log(this.metisService.getCourse().faqs); + this.setValues( + faqs.map((faq) => ({ + id: faq.id!.toString(), + value: faq.questionTitle!, + type: 'faq', + })), + ); + + this.disposableCompletionProvider = this.registerCompletionProviderForCurrentModel( + editor, + () => Promise.resolve(this.getValues()), + (item: ValueItem, range: TextEditorRange) => + new TextEditorCompletionItem( + `/faq ${item.value}`, + item.type, + `[${item.type}]${item.value}(${this.metisService.getLinkForFaq(item.id)})[/${item.type}]`, + TextEditorCompletionItemKind.Default, + range, + ), + '/', + ); + } + + /** + * Inserts the text '/faq' into the editor and focuses it. This method will trigger the completion provider to show the available exercises. + * @param editor The editor to insert the text into. + */ + run(editor: TextEditor): void { + this.replaceTextAtCurrentSelection(editor, FaqReferenceAction.DEFAULT_INSERT_TEXT); + editor.triggerCompletion(); + editor.focus(); + } + + dispose(): void { + super.dispose(); + this.disposableCompletionProvider?.dispose(); + } + + getOpeningIdentifier(): string { + return '[faq]'; + } +} diff --git a/src/main/webapp/i18n/de/metis.json b/src/main/webapp/i18n/de/metis.json index 8a669a9c8c8c..27a983fe0543 100644 --- a/src/main/webapp/i18n/de/metis.json +++ b/src/main/webapp/i18n/de/metis.json @@ -46,7 +46,8 @@ "exercise": "Aufgabe", "lecture": "Vortrag", "channel": "Kanal", - "user": "Benutzer" + "user": "Benutzer", + "faq": "FAQ" }, "channel": { "noChannel": "Es ist kein Kanal verfügbar.", diff --git a/src/main/webapp/i18n/en/metis.json b/src/main/webapp/i18n/en/metis.json index 9a3ff0977f2b..736c5c2f7e0d 100644 --- a/src/main/webapp/i18n/en/metis.json +++ b/src/main/webapp/i18n/en/metis.json @@ -46,7 +46,8 @@ "exercise": "Exercise", "lecture": "Lecture", "channel": "Channel", - "user": "User" + "user": "User", + "faq": "FAQ" }, "channel": { "noChannel": "There is no channel available.", From 066059947ea95870e174ab5364170e5547ca87e1 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 22 Oct 2024 17:56:52 +0200 Subject: [PATCH 02/30] sort faqs by id --- .../app/overview/course-faq/course-faq.component.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.ts b/src/main/webapp/app/overview/course-faq/course-faq.component.ts index db5a91e2c3d7..5fa08d5d6835 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq.component.ts +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.ts @@ -17,6 +17,7 @@ import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-catego import { onError } from 'app/shared/util/global.utils'; import { SearchFilterComponent } from 'app/shared/search-filter/search-filter.component'; import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; +import { SortService } from 'app/shared/service/sort.service'; @Component({ selector: 'jhi-course-faq', @@ -51,6 +52,7 @@ export class CourseFaqComponent implements OnInit, OnDestroy { private faqService = inject(FaqService); private alertService = inject(AlertService); + private sortService = inject(SortService); ngOnInit(): void { this.parentParamSubscription = this.route.parent!.params.subscribe((params) => { @@ -78,6 +80,7 @@ export class CourseFaqComponent implements OnInit, OnDestroy { next: (res: Faq[]) => { this.faqs = res; this.applyFilters(); + this.sortFaqs(); }, error: (res: HttpErrorResponse) => onError(this.alertService, res), }); @@ -113,4 +116,9 @@ export class CourseFaqComponent implements OnInit, OnDestroy { this.applyFilters(); this.applySearch(searchTerm); } + + sortFaqs() { + this.sortService.sortByProperty(this.filteredFaqs, 'id', true); + console.log(this.filteredFaqs); + } } From f91168de7ae8b1124a02f39020c8ab2b9e4c61ac Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Wed, 23 Oct 2024 08:52:51 +0200 Subject: [PATCH 03/30] Scroll to FAQ on reference --- .../course-faq/course-faq.component.html | 4 ++- .../course-faq/course-faq.component.ts | 30 +++++++++++++++++-- .../webapp/app/shared/metis/metis.service.ts | 6 ++-- .../posting-content.components.ts | 7 +++-- .../communication/faq-reference.action.ts | 3 +- 5 files changed, 38 insertions(+), 12 deletions(-) diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.html b/src/main/webapp/app/overview/course-faq/course-faq.component.html index 67cc28d2f00f..fd6093d52630 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq.component.html +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.html @@ -32,7 +32,9 @@

}
@for (faq of this.filteredFaqs; track faq) { - +
+ +
}
@if (filteredFaqs?.length === 0 && faqs.length > 0) { diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.ts b/src/main/webapp/app/overview/course-faq/course-faq.component.ts index 5fa08d5d6835..ed5208d7de1f 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq.component.ts +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit, ViewEncapsulation, inject } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChildren, ViewEncapsulation, inject } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { debounceTime, map } from 'rxjs/operators'; import { BehaviorSubject, Subject, Subscription } from 'rxjs'; @@ -27,7 +27,8 @@ import { SortService } from 'app/shared/service/sort.service'; standalone: true, imports: [ArtemisSharedComponentModule, ArtemisSharedModule, CourseFaqAccordionComponent, CustomExerciseCategoryBadgeComponent, SearchFilterComponent, ArtemisMarkdownModule], }) -export class CourseFaqComponent implements OnInit, OnDestroy { +export class CourseFaqComponent implements OnInit, OnDestroy, AfterViewInit { + @ViewChildren('faqElement') faqElements!: QueryList; private ngUnsubscribe = new Subject(); private parentParamSubscription: Subscription; @@ -60,11 +61,18 @@ export class CourseFaqComponent implements OnInit, OnDestroy { this.loadFaqs(); this.loadCourseExerciseCategories(this.courseId); }); + this.searchInput.pipe(debounceTime(300)).subscribe((searchTerm: string) => { this.refreshFaqList(searchTerm); }); } + ngAfterViewInit(): void { + this.faqElements.changes.subscribe(() => { + this.goToFaq(); + }); + } + private loadCourseExerciseCategories(courseId: number) { loadCourseFaqCategories(courseId, this.alertService, this.faqService).subscribe((existingCategories) => { this.existingCategories = existingCategories; @@ -119,6 +127,22 @@ export class CourseFaqComponent implements OnInit, OnDestroy { sortFaqs() { this.sortService.sortByProperty(this.filteredFaqs, 'id', true); - console.log(this.filteredFaqs); + } + + goToFaq() { + this.route.queryParams.subscribe((params) => { + const faqId = params['faqId']; + if (faqId) { + this.scrollToFaq(faqId); + } + }); + } + + scrollToFaq(faqId: number): void { + const faqElement = this.faqElements.find((faq) => faq.nativeElement.id === 'faq-' + String(faqId)); + if (faqElement) { + faqElement.nativeElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); + faqElement.nativeElement.focus(); + } } } diff --git a/src/main/webapp/app/shared/metis/metis.service.ts b/src/main/webapp/app/shared/metis/metis.service.ts index f15e852d2d94..9d3ab29e8e9c 100644 --- a/src/main/webapp/app/shared/metis/metis.service.ts +++ b/src/main/webapp/app/shared/metis/metis.service.ts @@ -517,11 +517,9 @@ export class MetisService implements OnDestroy { /** * returns the router link required for navigating to the exercise referenced within a faq - * @param {string} faqId ID of the faq be navigated to - * @return {string} router link of the exercise + * @return {string} router link of the faq */ - getLinkForFaq(faqId: string): string { - console.log(faqId); + getLinkForFaq(): string { return `/courses/${this.getCourse().id}/faq`; } diff --git a/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts b/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts index f9e751eb7321..533a1a33bb0f 100644 --- a/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts +++ b/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts @@ -108,8 +108,7 @@ export class PostingContentComponent implements OnInit, OnChanges, OnDestroy { ReferenceType.MODELING === referenceType || ReferenceType.QUIZ === referenceType || ReferenceType.TEXT === referenceType || - ReferenceType.FILE_UPLOAD === referenceType || - ReferenceType.FAQ == referenceType + ReferenceType.FILE_UPLOAD === referenceType ) { // reference opening tag: [{referenceType}] (wrapped between 2 characters) // reference closing tag: [/referenceType] (wrapped between 3 characters) @@ -117,6 +116,10 @@ export class PostingContentComponent implements OnInit, OnChanges, OnDestroy { // linkToReference: link to be navigated to on reference click referenceStr = this.content.substring(this.content.indexOf(']', patternMatch.startIndex)! + 1, this.content.indexOf('(', patternMatch.startIndex)!); linkToReference = [this.content.substring(this.content.indexOf('(', patternMatch.startIndex)! + 1, this.content.indexOf(')', patternMatch.startIndex))]; + } else if (ReferenceType.FAQ == referenceType) { + referenceStr = this.content.substring(this.content.indexOf(']', patternMatch.startIndex)! + 1, this.content.indexOf('(', patternMatch.startIndex)!); + linkToReference = [this.content.substring(this.content.indexOf('(', patternMatch.startIndex)! + 1, this.content.indexOf('?', patternMatch.startIndex))]; + queryParams = { faqId: this.content.substring(this.content.indexOf('=') + 1, this.content.indexOf(')')) } as Params; } else if (ReferenceType.ATTACHMENT === referenceType || ReferenceType.ATTACHMENT_UNITS === referenceType) { // referenceStr: string to be displayed for the reference // attachmentToReference: location of attachment to be opened on reference click diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/communication/faq-reference.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/communication/faq-reference.action.ts index 08237f3449e9..ce5013037c94 100644 --- a/src/main/webapp/app/shared/monaco-editor/model/actions/communication/faq-reference.action.ts +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/communication/faq-reference.action.ts @@ -28,7 +28,6 @@ export class FaqReferenceAction extends TextEditorDomainActionWithOptions { register(editor: TextEditor, translateService: TranslateService): void { super.register(editor, translateService); const faqs = this.metisService.getCourse().faqs ?? []; - console.log(this.metisService.getCourse().faqs); this.setValues( faqs.map((faq) => ({ id: faq.id!.toString(), @@ -44,7 +43,7 @@ export class FaqReferenceAction extends TextEditorDomainActionWithOptions { new TextEditorCompletionItem( `/faq ${item.value}`, item.type, - `[${item.type}]${item.value}(${this.metisService.getLinkForFaq(item.id)})[/${item.type}]`, + `[${item.type}]${item.value}(${this.metisService.getLinkForFaq()}?faqId=${item.id})[/${item.type}]`, TextEditorCompletionItemKind.Default, range, ), From 61ea920ae0e5575d9904f571fe77255cd672c2cf Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Wed, 23 Oct 2024 09:01:51 +0200 Subject: [PATCH 04/30] Fixed tests --- .../spec/helpers/mocks/service/mock-metis-service.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/javascript/spec/helpers/mocks/service/mock-metis-service.service.ts b/src/test/javascript/spec/helpers/mocks/service/mock-metis-service.service.ts index b96cb2441e8c..8b5772207062 100644 --- a/src/test/javascript/spec/helpers/mocks/service/mock-metis-service.service.ts +++ b/src/test/javascript/spec/helpers/mocks/service/mock-metis-service.service.ts @@ -130,4 +130,6 @@ export class MockMetisService { } setCourse(course: Course | undefined): void {} + + setFaqs(course: Course | undefined): void {} } From dc8c74ded45e3b2c6d8762f3733e55b5d57546a8 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Wed, 23 Oct 2024 09:43:04 +0200 Subject: [PATCH 05/30] Fixed tests --- src/main/webapp/app/entities/course.model.ts | 7 ++++ .../posting-markdown-editor.component.ts | 6 ++- ...postings-markdown-editor.component.spec.ts | 40 +++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/entities/course.model.ts b/src/main/webapp/app/entities/course.model.ts index 093772e76d42..d31f0792a6fe 100644 --- a/src/main/webapp/app/entities/course.model.ts +++ b/src/main/webapp/app/entities/course.model.ts @@ -29,6 +29,13 @@ export function isCommunicationEnabled(course: Course | undefined) { return config === CourseInformationSharingConfiguration.COMMUNICATION_AND_MESSAGING || config === CourseInformationSharingConfiguration.COMMUNICATION_ONLY; } +/** + * Note: Keep in sync with method in CourseRepository.java + */ +export function isFaqEnabled(course: Course | undefined) { + return course?.faqEnabled; +} + /** * Note: Keep in sync with method in CourseRepository.java */ diff --git a/src/main/webapp/app/shared/metis/posting-markdown-editor/posting-markdown-editor.component.ts b/src/main/webapp/app/shared/metis/posting-markdown-editor/posting-markdown-editor.component.ts index ace2400feda9..3d1776162a06 100644 --- a/src/main/webapp/app/shared/metis/posting-markdown-editor/posting-markdown-editor.component.ts +++ b/src/main/webapp/app/shared/metis/posting-markdown-editor/posting-markdown-editor.component.ts @@ -17,7 +17,7 @@ import { MetisService } from 'app/shared/metis/metis.service'; import { LectureService } from 'app/lecture/lecture.service'; import { CourseManagementService } from 'app/course/manage/course-management.service'; import { ChannelService } from 'app/shared/metis/conversations/channel.service'; -import { isCommunicationEnabled } from 'app/entities/course.model'; +import { isCommunicationEnabled, isFaqEnabled } from 'app/entities/course.model'; import { TextEditorAction } from 'app/shared/monaco-editor/model/actions/text-editor-action.model'; import { BoldAction } from 'app/shared/monaco-editor/model/actions/bold.action'; import { ItalicAction } from 'app/shared/monaco-editor/model/actions/italic.action'; @@ -76,6 +76,8 @@ export class PostingMarkdownEditorComponent implements OnInit, ControlValueAcces ? [new UserMentionAction(this.courseManagementService, this.metisService), new ChannelReferenceAction(this.metisService, this.channelService)] : []; + const faqAction = isFaqEnabled(this.metisService.getCourse()) ? [new FaqReferenceAction(this.metisService)] : []; + this.defaultActions = [ new BoldAction(), new ItalicAction(), @@ -85,7 +87,7 @@ export class PostingMarkdownEditorComponent implements OnInit, ControlValueAcces new CodeBlockAction(), ...messagingOnlyActions, new ExerciseReferenceAction(this.metisService), - new FaqReferenceAction(this.metisService), + ...faqAction, ]; this.lectureAttachmentReferenceAction = new LectureAttachmentReferenceAction(this.metisService, this.lectureService); diff --git a/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts b/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts index d599aac5a13a..58d0ae51dcf3 100644 --- a/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts @@ -24,6 +24,7 @@ import { CodeAction } from 'app/shared/monaco-editor/model/actions/code.action'; import { CodeBlockAction } from 'app/shared/monaco-editor/model/actions/code-block.action'; import { ExerciseReferenceAction } from 'app/shared/monaco-editor/model/actions/communication/exercise-reference.action'; import { LectureAttachmentReferenceAction } from 'app/shared/monaco-editor/model/actions/communication/lecture-attachment-reference.action'; +import { FaqReferenceAction } from 'app/shared/monaco-editor/model/actions/communication/faq-reference.action'; describe('PostingsMarkdownEditor', () => { let component: PostingMarkdownEditorComponent; @@ -95,6 +96,45 @@ describe('PostingsMarkdownEditor', () => { expect(component.lectureAttachmentReferenceAction).toEqual(new LectureAttachmentReferenceAction(metisService, lectureService)); }); + it('should have set the correct default commands on init if faq is disabled', () => { + jest.spyOn(CourseModel, 'isFaqEnabled').mockReturnValueOnce(false); + component.ngOnInit(); + + expect(component.defaultActions).toEqual([ + new BoldAction(), + new ItalicAction(), + new UnderlineAction(), + new QuoteAction(), + new CodeAction(), + new CodeBlockAction(), + new UserMentionAction(courseManagementService, metisService), + new ChannelReferenceAction(metisService, channelService), + new ExerciseReferenceAction(metisService), + ]); + + expect(component.lectureAttachmentReferenceAction).toEqual(new LectureAttachmentReferenceAction(metisService, lectureService)); + }); + + it('should have set the correct default commands on init if faq is enabled', () => { + jest.spyOn(CourseModel, 'isFaqEnabled').mockReturnValueOnce(true); + component.ngOnInit(); + + expect(component.defaultActions).toEqual([ + new BoldAction(), + new ItalicAction(), + new UnderlineAction(), + new QuoteAction(), + new CodeAction(), + new CodeBlockAction(), + new UserMentionAction(courseManagementService, metisService), + new ChannelReferenceAction(metisService, channelService), + new ExerciseReferenceAction(metisService), + new FaqReferenceAction(metisService), + ]); + + expect(component.lectureAttachmentReferenceAction).toEqual(new LectureAttachmentReferenceAction(metisService, lectureService)); + }); + it('should show the correct amount of characters below the markdown input', () => { component.maxContentLength = 200; fixture.detectChanges(); From 6518cf599b566d34bc847ef5385e8392bdefe6ab Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Wed, 23 Oct 2024 10:24:51 +0200 Subject: [PATCH 06/30] Added FAQ's to initial CourseOverviewComponents --- .../course-conversations.component.ts | 1 - .../app/overview/course-overview.component.ts | 23 +++++++++++++++++++ .../webapp/app/shared/metis/metis.service.ts | 23 +------------------ .../service/mock-metis-service.service.ts | 2 -- 4 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/main/webapp/app/overview/course-conversations/course-conversations.component.ts b/src/main/webapp/app/overview/course-conversations/course-conversations.component.ts index 9293d013bafc..742999cf7aa7 100644 --- a/src/main/webapp/app/overview/course-conversations/course-conversations.component.ts +++ b/src/main/webapp/app/overview/course-conversations/course-conversations.component.ts @@ -153,7 +153,6 @@ export class CourseConversationsComponent implements OnInit, OnDestroy { private setupMetis() { this.metisService.setPageType(PageType.OVERVIEW); this.metisService.setCourse(this.course!); - this.metisService.setFaqs(this.course); } ngOnInit(): void { diff --git a/src/main/webapp/app/overview/course-overview.component.ts b/src/main/webapp/app/overview/course-overview.component.ts index 86d8e31f26d3..d57752033d8a 100644 --- a/src/main/webapp/app/overview/course-overview.component.ts +++ b/src/main/webapp/app/overview/course-overview.component.ts @@ -12,6 +12,7 @@ import { ViewChild, ViewChildren, ViewContainerRef, + inject, } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { @@ -67,6 +68,8 @@ import { CourseConversationsComponent } from 'app/overview/course-conversations/ import { sortCourses } from 'app/shared/util/course.util'; import { CourseUnenrollmentModalComponent } from './course-unenrollment-modal.component'; import { LtiService } from 'app/shared/service/lti.service'; +import { Faq } from 'app/entities/faq.model'; +import { FaqService } from 'app/faq/faq.service'; interface CourseActionItem { title: string; @@ -186,6 +189,8 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit readonly isMessagingEnabled = isMessagingEnabled; readonly isCommunicationEnabled = isCommunicationEnabled; + private faqService = inject(FaqService); + constructor( private courseService: CourseManagementService, private courseExerciseService: CourseExerciseService, @@ -695,6 +700,7 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit map((res: HttpResponse) => { if (res.body) { this.course = res.body; + this.setFaqs(this.course); } if (refresh) { @@ -733,6 +739,23 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit return observable; } + /** + * set course property before using metis service + * @param {Course} course in which the metis service is used + */ + setFaqs(course: Course | undefined): void { + if (course) { + this.faqService + .findAllByCourseId(this.courseId) + .pipe(map((res: HttpResponse) => res.body)) + .subscribe({ + next: (res: Faq[]) => { + course.faqs = res; + }, + }); + } + } + ngOnDestroy() { if (this.teamAssignmentUpdateListener) { this.teamAssignmentUpdateListener.unsubscribe(); diff --git a/src/main/webapp/app/shared/metis/metis.service.ts b/src/main/webapp/app/shared/metis/metis.service.ts index 9d3ab29e8e9c..0fb2ed3bf277 100644 --- a/src/main/webapp/app/shared/metis/metis.service.ts +++ b/src/main/webapp/app/shared/metis/metis.service.ts @@ -6,7 +6,7 @@ import { User } from 'app/core/user/user.model'; import { AccountService } from 'app/core/auth/account.service'; import { Course } from 'app/entities/course.model'; import { Posting } from 'app/entities/metis/posting.model'; -import { Injectable, OnDestroy, inject } from '@angular/core'; +import { Injectable, OnDestroy } from '@angular/core'; import { AnswerPostService } from 'app/shared/metis/answer-post.service'; import { AnswerPost } from 'app/entities/metis/answer-post.model'; import { Reaction } from 'app/entities/metis/reaction.model'; @@ -31,8 +31,6 @@ import { Conversation, ConversationDTO } from 'app/entities/metis/conversation/c import { ChannelDTO, ChannelSubType, getAsChannelDTO } from 'app/entities/metis/conversation/channel.model'; import { ConversationService } from 'app/shared/metis/conversations/conversation.service'; import { NotificationService } from 'app/shared/notification/notification.service'; -import { FaqService } from 'app/faq/faq.service'; -import { Faq } from 'app/entities/faq.model'; @Injectable() export class MetisService implements OnDestroy { @@ -52,8 +50,6 @@ export class MetisService implements OnDestroy { private courseWideTopicSubscription: Subscription; - private faqService = inject(FaqService); - constructor( protected postService: PostService, protected answerPostService: AnswerPostService, @@ -149,23 +145,6 @@ export class MetisService implements OnDestroy { } } - /** - * set course property before using metis service - * @param {Course} course in which the metis service is used - */ - setFaqs(course: Course | undefined): void { - if (course) { - this.faqService - .findAllByCourseId(this.courseId) - .pipe(map((res: HttpResponse) => res.body)) - .subscribe({ - next: (res: Faq[]) => { - course.faqs = res; - }, - }); - } - } - /** * to be used to set posts from outside * @param {Post[]} posts that are managed by metis service diff --git a/src/test/javascript/spec/helpers/mocks/service/mock-metis-service.service.ts b/src/test/javascript/spec/helpers/mocks/service/mock-metis-service.service.ts index 8b5772207062..b96cb2441e8c 100644 --- a/src/test/javascript/spec/helpers/mocks/service/mock-metis-service.service.ts +++ b/src/test/javascript/spec/helpers/mocks/service/mock-metis-service.service.ts @@ -130,6 +130,4 @@ export class MockMetisService { } setCourse(course: Course | undefined): void {} - - setFaqs(course: Course | undefined): void {} } From a3f8ba3c01f6c7668e180e283c0cd90676602613 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Wed, 23 Oct 2024 10:55:06 +0200 Subject: [PATCH 07/30] Add test for postingContent --- .../posting-content.component.spec.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/test/javascript/spec/component/shared/metis/posting-content/posting-content.component.spec.ts b/src/test/javascript/spec/component/shared/metis/posting-content/posting-content.component.spec.ts index d3b874b1c8d4..1e475bdf8abb 100644 --- a/src/test/javascript/spec/component/shared/metis/posting-content/posting-content.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/posting-content/posting-content.component.spec.ts @@ -492,6 +492,22 @@ describe('PostingContentComponent', () => { ]); })); + it('should compute parts when referencing a faq', fakeAsync(() => { + component.content = `I want to reference [faq]faq(/courses/1/faq?faqId=45)[/faq].`; + const matches = component.getPatternMatches(); + component.computePostingContentParts(matches); + expect(component.postingContentParts).toEqual([ + { + contentBeforeReference: 'I want to reference ', + linkToReference: ['/courses/1/faq'], + referenceStr: `faq`, + referenceType: ReferenceType.FAQ, + contentAfterReference: '.', + queryParams: { faqId: '45' }, + } as PostingContentPart, + ]); + })); + it('should compute parts when referencing a channel', fakeAsync(() => { component.content = `This topic belongs to [channel]test(1)[/channel].`; const matches = component.getPatternMatches(); From c617c83b6c89421f4422c360ab482d8a0b0886ad Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Wed, 23 Oct 2024 11:27:47 +0200 Subject: [PATCH 08/30] Add tests --- .../course-faq/course-faq.component.spec.ts | 39 ++++++++++++++++++- .../spec/service/metis/metis.service.spec.ts | 5 +++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts b/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts index f7795a433603..362f9428c337 100644 --- a/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts @@ -19,6 +19,8 @@ import { CourseFaqAccordionComponent } from 'app/overview/course-faq/course-faq- import { Faq } from 'app/entities/faq.model'; import { FaqCategory } from 'app/entities/faq-category.model'; import { SearchFilterComponent } from 'app/shared/search-filter/search-filter.component'; +import { SortService } from 'app/shared/service/sort.service'; +import { ElementRef, QueryList } from '@angular/core'; function createFaq(id: number, category: string, color: string): Faq { const faq = new Faq(); @@ -36,6 +38,7 @@ describe('CourseFaqs', () => { let faqService: FaqService; let alertServiceStub: jest.SpyInstance; let alertService: AlertService; + let sortService: SortService; let faq1: Faq; let faq2: Faq; @@ -104,6 +107,7 @@ describe('CourseFaqs', () => { faqService = TestBed.inject(FaqService); alertService = TestBed.inject(AlertService); + sortService = TestBed.inject(SortService); }); }); @@ -119,7 +123,6 @@ describe('CourseFaqs', () => { it('should fetch faqs when initialized', () => { const findAllSpy = jest.spyOn(faqService, 'findAllByCourseId'); - courseFaqComponentFixture.detectChanges(); expect(findAllSpy).toHaveBeenCalledExactlyOnceWith(1); expect(courseFaqComponent.faqs).toHaveLength(3); @@ -156,4 +159,38 @@ describe('CourseFaqs', () => { courseFaqComponentFixture.detectChanges(); expect(alertServiceStub).toHaveBeenCalledOnce(); }); + + it('should call sortService when sortRows is called', () => { + jest.spyOn(sortService, 'sortByProperty').mockReturnValue([]); + courseFaqComponent.sortFaqs(); + expect(sortService.sortByProperty).toHaveBeenCalledOnce(); + }); + + it('should call scrollToFaq when faqId is in query params', () => { + const scrollToFaqSpy = jest.spyOn(courseFaqComponent, 'scrollToFaq'); + + const route = TestBed.inject(ActivatedRoute); + (route.queryParams as any) = of({ faqId: '1' }); + + courseFaqComponent.goToFaq(); + + expect(scrollToFaqSpy).toHaveBeenCalledOnce(); + expect(scrollToFaqSpy).toHaveBeenCalledWith('1'); + }); + + it('should scroll and focus on the faq element with given id', () => { + const nativeElement = { + id: 'faq-1', + scrollIntoView: jest.fn(), + focus: jest.fn(), + }; + const elementRef = new ElementRef(nativeElement); + const queryList = new QueryList(); + queryList.reset([elementRef]); + courseFaqComponent.faqElements = queryList; + courseFaqComponent.scrollToFaq(1); + + expect(nativeElement.scrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth', block: 'start' }); + expect(nativeElement.focus).toHaveBeenCalled(); + }); }); diff --git a/src/test/javascript/spec/service/metis/metis.service.spec.ts b/src/test/javascript/spec/service/metis/metis.service.spec.ts index 7b61efbc958e..161756e7f36b 100644 --- a/src/test/javascript/spec/service/metis/metis.service.spec.ts +++ b/src/test/javascript/spec/service/metis/metis.service.spec.ts @@ -330,6 +330,11 @@ describe('Metis Service', () => { expect(referenceRouterLink).toBe(`/courses/${metisCourse.id}/exams/${metisExam.id!.toString()}`); }); + it('should determine the router link required for referencing a faq', () => { + const link = metisService.getLinkForFaq(); + expect(link).toBe(`/courses/${metisCourse.id}/faq`); + }); + it('should determine the router link required for navigation based on the channel subtype', () => { metisService.setCourse(course); const channelDTO = new ChannelDTO(); From f7683a0b0c699303ab109dbf4f1be84103adfadb Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Wed, 23 Oct 2024 12:18:21 +0200 Subject: [PATCH 09/30] Adressed coderabit --- .../course-faq/course-faq.component.ts | 29 +++++++++---------- .../posting-content-part.components.ts | 4 +++ .../course-faq/course-faq.component.spec.ts | 12 -------- 3 files changed, 18 insertions(+), 27 deletions(-) diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.ts b/src/main/webapp/app/overview/course-faq/course-faq.component.ts index ed5208d7de1f..1bba3f75e052 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq.component.ts +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.ts @@ -1,6 +1,6 @@ import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChildren, ViewEncapsulation, inject } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { debounceTime, map } from 'rxjs/operators'; +import { debounceTime, map, takeUntil } from 'rxjs/operators'; import { BehaviorSubject, Subject, Subscription } from 'rxjs'; import { faFilter } from '@fortawesome/free-solid-svg-icons'; import { ButtonType } from 'app/shared/components/button.component'; @@ -18,6 +18,7 @@ import { onError } from 'app/shared/util/global.utils'; import { SearchFilterComponent } from 'app/shared/search-filter/search-filter.component'; import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; import { SortService } from 'app/shared/service/sort.service'; +import { Renderer2 } from '@angular/core'; @Component({ selector: 'jhi-course-faq', @@ -33,6 +34,7 @@ export class CourseFaqComponent implements OnInit, OnDestroy, AfterViewInit { private parentParamSubscription: Subscription; courseId: number; + faqId: number; faqs: Faq[]; filteredFaqs: Faq[]; @@ -54,6 +56,7 @@ export class CourseFaqComponent implements OnInit, OnDestroy, AfterViewInit { private faqService = inject(FaqService); private alertService = inject(AlertService); private sortService = inject(SortService); + private renderer = inject(Renderer2); ngOnInit(): void { this.parentParamSubscription = this.route.parent!.params.subscribe((params) => { @@ -62,14 +65,19 @@ export class CourseFaqComponent implements OnInit, OnDestroy, AfterViewInit { this.loadCourseExerciseCategories(this.courseId); }); + this.route.queryParams.subscribe((params) => { + this.faqId = params['faqId']; + }); + this.searchInput.pipe(debounceTime(300)).subscribe((searchTerm: string) => { this.refreshFaqList(searchTerm); }); } - ngAfterViewInit(): void { - this.faqElements.changes.subscribe(() => { - this.goToFaq(); + this.faqElements.changes.pipe(takeUntil(this.ngUnsubscribe)).subscribe(() => { + if (this.faqId) { + this.scrollToFaq(this.faqId); + } }); } @@ -129,20 +137,11 @@ export class CourseFaqComponent implements OnInit, OnDestroy, AfterViewInit { this.sortService.sortByProperty(this.filteredFaqs, 'id', true); } - goToFaq() { - this.route.queryParams.subscribe((params) => { - const faqId = params['faqId']; - if (faqId) { - this.scrollToFaq(faqId); - } - }); - } - scrollToFaq(faqId: number): void { const faqElement = this.faqElements.find((faq) => faq.nativeElement.id === 'faq-' + String(faqId)); if (faqElement) { - faqElement.nativeElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); - faqElement.nativeElement.focus(); + this.renderer.selectRootElement(faqElement.nativeElement).scrollIntoView({ behavior: 'smooth', block: 'start' }); + this.renderer.selectRootElement(faqElement.nativeElement).focus(); } } } diff --git a/src/main/webapp/app/shared/metis/posting-content/posting-content-part/posting-content-part.components.ts b/src/main/webapp/app/shared/metis/posting-content/posting-content-part/posting-content-part.components.ts index 8f7d7038a29d..cad9f98c907a 100644 --- a/src/main/webapp/app/shared/metis/posting-content/posting-content-part/posting-content-part.components.ts +++ b/src/main/webapp/app/shared/metis/posting-content/posting-content-part/posting-content-part.components.ts @@ -15,6 +15,7 @@ import { faMessage, faPaperclip, faProjectDiagram, + faQuestion, } from '@fortawesome/free-solid-svg-icons'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { EnlargeSlideImageComponent } from 'app/shared/metis/posting-content/enlarge-slide-image/enlarge-slide-image.component'; @@ -43,6 +44,7 @@ export class PostingContentPartComponent { protected readonly faBan = faBan; protected readonly faAt = faAt; protected readonly faHashtag = faHashtag; + protected readonly faQuestion = faQuestion; protected readonly ReferenceType = ReferenceType; @@ -98,6 +100,8 @@ export class PostingContentPartComponent { return faFileUpload; case ReferenceType.SLIDE: return faFile; + case ReferenceType.FAQ: + return faQuestion; default: return faPaperclip; } diff --git a/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts b/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts index 362f9428c337..47fe075cfac7 100644 --- a/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts @@ -166,18 +166,6 @@ describe('CourseFaqs', () => { expect(sortService.sortByProperty).toHaveBeenCalledOnce(); }); - it('should call scrollToFaq when faqId is in query params', () => { - const scrollToFaqSpy = jest.spyOn(courseFaqComponent, 'scrollToFaq'); - - const route = TestBed.inject(ActivatedRoute); - (route.queryParams as any) = of({ faqId: '1' }); - - courseFaqComponent.goToFaq(); - - expect(scrollToFaqSpy).toHaveBeenCalledOnce(); - expect(scrollToFaqSpy).toHaveBeenCalledWith('1'); - }); - it('should scroll and focus on the faq element with given id', () => { const nativeElement = { id: 'faq-1', From 54fc1158b9c28ca1022b3bb96017af1dfed657c2 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Thu, 24 Oct 2024 10:18:43 +0200 Subject: [PATCH 10/30] Added testcases --- .../course-faq/course-faq.component.html | 2 +- .../course-faq/course-faq.component.ts | 23 +++++------ .../posting-content.components.ts | 1 + .../course-faq/course-faq.component.spec.ts | 23 +++++------ .../posting-content.component.spec.ts | 4 +- ...r-communication-action.integration.spec.ts | 41 ++++++++++++++++++- .../service/mock-metis-service.service.ts | 4 ++ .../spec/helpers/sample/metis-sample-data.ts | 5 +++ 8 files changed, 75 insertions(+), 28 deletions(-) diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.html b/src/main/webapp/app/overview/course-faq/course-faq.component.html index fd6093d52630..cda6244769cd 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq.component.html +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.html @@ -26,7 +26,7 @@ } -
+
@if (faqs?.length === 0) {

} diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.ts b/src/main/webapp/app/overview/course-faq/course-faq.component.ts index 1bba3f75e052..e53080f79856 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq.component.ts +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.ts @@ -1,6 +1,6 @@ -import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChildren, ViewEncapsulation, inject } from '@angular/core'; +import { Component, ElementRef, OnDestroy, OnInit, ViewEncapsulation, effect, inject, viewChildren } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { debounceTime, map, takeUntil } from 'rxjs/operators'; +import { debounceTime, map } from 'rxjs/operators'; import { BehaviorSubject, Subject, Subscription } from 'rxjs'; import { faFilter } from '@fortawesome/free-solid-svg-icons'; import { ButtonType } from 'app/shared/components/button.component'; @@ -28,8 +28,8 @@ import { Renderer2 } from '@angular/core'; standalone: true, imports: [ArtemisSharedComponentModule, ArtemisSharedModule, CourseFaqAccordionComponent, CustomExerciseCategoryBadgeComponent, SearchFilterComponent, ArtemisMarkdownModule], }) -export class CourseFaqComponent implements OnInit, OnDestroy, AfterViewInit { - @ViewChildren('faqElement') faqElements!: QueryList; +export class CourseFaqComponent implements OnInit, OnDestroy { + faqElements = viewChildren('faqElement'); private ngUnsubscribe = new Subject(); private parentParamSubscription: Subscription; @@ -58,6 +58,12 @@ export class CourseFaqComponent implements OnInit, OnDestroy, AfterViewInit { private sortService = inject(SortService); private renderer = inject(Renderer2); + constructor() { + effect(() => { + this.scrollToFaq(this.faqId); + }); + } + ngOnInit(): void { this.parentParamSubscription = this.route.parent!.params.subscribe((params) => { this.courseId = Number(params.courseId); @@ -73,13 +79,6 @@ export class CourseFaqComponent implements OnInit, OnDestroy, AfterViewInit { this.refreshFaqList(searchTerm); }); } - ngAfterViewInit(): void { - this.faqElements.changes.pipe(takeUntil(this.ngUnsubscribe)).subscribe(() => { - if (this.faqId) { - this.scrollToFaq(this.faqId); - } - }); - } private loadCourseExerciseCategories(courseId: number) { loadCourseFaqCategories(courseId, this.alertService, this.faqService).subscribe((existingCategories) => { @@ -138,7 +137,7 @@ export class CourseFaqComponent implements OnInit, OnDestroy, AfterViewInit { } scrollToFaq(faqId: number): void { - const faqElement = this.faqElements.find((faq) => faq.nativeElement.id === 'faq-' + String(faqId)); + const faqElement = this.faqElements().find((faq) => faq.nativeElement.id === 'faq-' + String(faqId)); if (faqElement) { this.renderer.selectRootElement(faqElement.nativeElement).scrollIntoView({ behavior: 'smooth', block: 'start' }); this.renderer.selectRootElement(faqElement.nativeElement).focus(); diff --git a/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts b/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts index 533a1a33bb0f..462d5103597f 100644 --- a/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts +++ b/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts @@ -203,6 +203,7 @@ export class PostingContentComponent implements OnInit, OnChanges, OnDestroy { // Group 7: reference pattern for Lecture Attachments // Group 8: reference pattern for Lecture Units // Group 9: reference pattern for Users + // Group 10: reference pattern for FAQ // globally searched for, i.e. no return after first match const pattern = /(?#\d+)|(?\[programming].*?\[\/programming])|(?\[modeling].*?\[\/modeling])|(?\[quiz].*?\[\/quiz])|(?\[text].*?\[\/text])|(?\[file-upload].*?\[\/file-upload])|(?\[lecture].*?\[\/lecture])|(?\[attachment].*?\[\/attachment])|(?\[lecture-unit].*?\[\/lecture-unit])|(?\[slide].*?\[\/slide])|(?\[user].*?\[\/user])|(?\[channel].*?\[\/channel])|(?\[faq].*?\[\/faq])/g; diff --git a/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts b/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts index 47fe075cfac7..aace6ef689b0 100644 --- a/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts @@ -20,7 +20,7 @@ import { Faq } from 'app/entities/faq.model'; import { FaqCategory } from 'app/entities/faq-category.model'; import { SearchFilterComponent } from 'app/shared/search-filter/search-filter.component'; import { SortService } from 'app/shared/service/sort.service'; -import { ElementRef, QueryList } from '@angular/core'; +import { ElementRef, signal } from '@angular/core'; function createFaq(id: number, category: string, color: string): Faq { const faq = new Faq(); @@ -69,6 +69,7 @@ describe('CourseFaqs', () => { parent: { params: of({ courseId: '1' }), }, + queryParams: of({ faqId: '1' }), }, }, MockProvider(FaqService, { @@ -167,18 +168,16 @@ describe('CourseFaqs', () => { }); it('should scroll and focus on the faq element with given id', () => { - const nativeElement = { - id: 'faq-1', - scrollIntoView: jest.fn(), - focus: jest.fn(), - }; - const elementRef = new ElementRef(nativeElement); - const queryList = new QueryList(); - queryList.reset([elementRef]); - courseFaqComponent.faqElements = queryList; + const nativeElement1 = { id: 'faq-1', scrollIntoView: jest.fn(), focus: jest.fn() }; + const nativeElement2 = { id: 'faq-2', scrollIntoView: jest.fn(), focus: jest.fn() }; + + const elementRef1 = new ElementRef(nativeElement1); + const elementRef2 = new ElementRef(nativeElement2); + + courseFaqComponent.faqElements = signal([elementRef1, elementRef2]); + courseFaqComponent.scrollToFaq(1); - expect(nativeElement.scrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth', block: 'start' }); - expect(nativeElement.focus).toHaveBeenCalled(); + expect(nativeElement1.scrollIntoView).toHaveBeenCalledExactlyOnceWith({ behavior: 'smooth', block: 'start' }); }); }); diff --git a/src/test/javascript/spec/component/shared/metis/posting-content/posting-content.component.spec.ts b/src/test/javascript/spec/component/shared/metis/posting-content/posting-content.component.spec.ts index 1e475bdf8abb..46f1a8bc0308 100644 --- a/src/test/javascript/spec/component/shared/metis/posting-content/posting-content.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/posting-content/posting-content.component.spec.ts @@ -492,7 +492,7 @@ describe('PostingContentComponent', () => { ]); })); - it('should compute parts when referencing a faq', fakeAsync(() => { + it('should compute parts when referencing a faq', () => { component.content = `I want to reference [faq]faq(/courses/1/faq?faqId=45)[/faq].`; const matches = component.getPatternMatches(); component.computePostingContentParts(matches); @@ -506,7 +506,7 @@ describe('PostingContentComponent', () => { queryParams: { faqId: '45' }, } as PostingContentPart, ]); - })); + }); it('should compute parts when referencing a channel', fakeAsync(() => { component.content = `This topic belongs to [channel]test(1)[/channel].`; diff --git a/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor-communication-action.integration.spec.ts b/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor-communication-action.integration.spec.ts index fd1179a35c4b..3d95a49b9cc0 100644 --- a/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor-communication-action.integration.spec.ts +++ b/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor-communication-action.integration.spec.ts @@ -28,6 +28,8 @@ import { LectureUnitType } from 'app/entities/lecture-unit/lectureUnit.model'; import { ReferenceType } from 'app/shared/metis/metis.util'; import { Attachment } from 'app/entities/attachment.model'; import dayjs from 'dayjs/esm'; +import { FaqReferenceAction } from 'app/shared/monaco-editor/model/actions/communication/faq-reference.action'; +import { Faq } from 'app/entities/faq.model'; describe('MonacoEditorCommunicationActionIntegration', () => { let comp: MonacoEditorComponent; @@ -42,6 +44,7 @@ describe('MonacoEditorCommunicationActionIntegration', () => { let channelReferenceAction: ChannelReferenceAction; let userMentionAction: UserMentionAction; let exerciseReferenceAction: ExerciseReferenceAction; + let faqReferenceAction: FaqReferenceAction; beforeEach(() => { return TestBed.configureTestingModule({ @@ -69,6 +72,7 @@ describe('MonacoEditorCommunicationActionIntegration', () => { channelReferenceAction = new ChannelReferenceAction(metisService, channelService); userMentionAction = new UserMentionAction(courseManagementService, metisService); exerciseReferenceAction = new ExerciseReferenceAction(metisService); + faqReferenceAction = new FaqReferenceAction(metisService); }); }); @@ -92,11 +96,13 @@ describe('MonacoEditorCommunicationActionIntegration', () => { { actionId: ChannelReferenceAction.ID, defaultInsertText: '#', triggerCharacter: '#' }, { actionId: UserMentionAction.ID, defaultInsertText: '@', triggerCharacter: '@' }, { actionId: ExerciseReferenceAction.ID, defaultInsertText: '/exercise', triggerCharacter: '/' }, + { actionId: FaqReferenceAction.ID, defaultInsertText: '/faq', triggerCharacter: '/' }, ])('Suggestions and default behavior for $actionId', ({ actionId, defaultInsertText, triggerCharacter }) => { - let action: ChannelReferenceAction | UserMentionAction | ExerciseReferenceAction; + let action: ChannelReferenceAction | UserMentionAction | ExerciseReferenceAction | FaqReferenceAction; let channels: ChannelIdAndNameDTO[]; let users: User[]; let exercises: Exercise[]; + let faqs: Faq[]; beforeEach(() => { fixture.detectChanges(); @@ -106,6 +112,7 @@ describe('MonacoEditorCommunicationActionIntegration', () => { users = [metisUser1, metisUser2, metisTutor]; jest.spyOn(courseManagementService, 'searchMembersForUserMentions').mockReturnValue(of(new HttpResponse({ body: users, status: 200 }))); exercises = metisService.getCourse().exercises!; + faqs = metisService.getCourse().faqs!; switch (actionId) { case ChannelReferenceAction.ID: @@ -117,6 +124,9 @@ describe('MonacoEditorCommunicationActionIntegration', () => { case ExerciseReferenceAction.ID: action = exerciseReferenceAction; break; + case FaqReferenceAction.ID: + action = faqReferenceAction; + break; } }); @@ -173,6 +183,15 @@ describe('MonacoEditorCommunicationActionIntegration', () => { }); }; + const checkFaqSuggestions = (suggestions: monaco.languages.CompletionItem[], faqs: Faq[]) => { + expect(suggestions).toHaveLength(faqs.length); + suggestions.forEach((suggestion, index) => { + expect(suggestion.label).toBe(`/faq ${faqs[index].questionTitle}`); + expect(suggestion.insertText).toBe(`[faq]${faqs[index].questionTitle}(${metisService.getLinkForFaq()}?faqId=${faqs[index].id})[/faq]`); + expect(suggestion.detail).toBe('faq'); + }); + }; + it.each(['', 'ex'])('should suggest the correct values if the user is typing a reference (suffix "%s")', async (referenceSuffix: string) => { const reference = triggerCharacter + referenceSuffix; comp.setText(reference); @@ -192,6 +211,9 @@ describe('MonacoEditorCommunicationActionIntegration', () => { case ExerciseReferenceAction.ID: checkExerciseSuggestions(suggestions, exercises); break; + case FaqReferenceAction.ID: + checkFaqSuggestions(suggestions, faqs); + break; } }); }); @@ -232,6 +254,23 @@ describe('MonacoEditorCommunicationActionIntegration', () => { comp.registerAction(exerciseReferenceAction); expect(exerciseReferenceAction.getValues()).toEqual([]); }); + + it('should insert / for faq references', () => { + fixture.detectChanges(); + comp.registerAction(faqReferenceAction); + faqReferenceAction.executeInCurrentEditor(); + expect(comp.getText()).toBe('/faq'); + }); + }); + + describe('FaqReferenceAction (', () => { + it('should initialize with empty values if faqs are not available', () => { + jest.spyOn(metisService, 'getCourse').mockReturnValue({ faqs: undefined } as any); + + fixture.detectChanges(); + comp.registerAction(faqReferenceAction); + expect(faqReferenceAction.getValues()).toEqual([]); + }); }); describe('LectureAttachmentReferenceAction', () => { diff --git a/src/test/javascript/spec/helpers/mocks/service/mock-metis-service.service.ts b/src/test/javascript/spec/helpers/mocks/service/mock-metis-service.service.ts index b96cb2441e8c..b3fd96510999 100644 --- a/src/test/javascript/spec/helpers/mocks/service/mock-metis-service.service.ts +++ b/src/test/javascript/spec/helpers/mocks/service/mock-metis-service.service.ts @@ -94,6 +94,10 @@ export class MockMetisService { return '/courses/' + metisCourse.id + '/exams/' + examId; } + getLinkForFaq(examId: string): string { + return `/courses/${this.getCourse().id}/faq`; + } + getLinkForChannelSubType(channel?: ChannelDTO): string | undefined { const referenceId = channel?.subTypeReferenceId?.toString(); if (!referenceId) { diff --git a/src/test/javascript/spec/helpers/sample/metis-sample-data.ts b/src/test/javascript/spec/helpers/sample/metis-sample-data.ts index ce2136c46028..02601e3a181d 100644 --- a/src/test/javascript/spec/helpers/sample/metis-sample-data.ts +++ b/src/test/javascript/spec/helpers/sample/metis-sample-data.ts @@ -39,6 +39,10 @@ export const metisUpVoteReactionUser1 = { id: 1, user: metisUser1, emojiId: VOTE export const metisReactionUser2 = { id: 2, user: metisUser2, emojiId: 'smile', creationDate: undefined } as Reaction; export const metisReactionToCreate = { emojiId: 'cheerio', creationDate: undefined } as Reaction; +export const metisFaq1 = { id: 1, questionTitle: 'title', questionAnswer: 'answer' }; +export const metisFaq2 = { id: 2, questionTitle: 'title', questionAnswer: 'answer' }; +export const metisFaq3 = { id: 3, questionTitle: 'title', questionAnswer: 'answer' }; + export const metisCourse = { id: 1, title: 'Metis Course', @@ -46,6 +50,7 @@ export const metisCourse = { lectures: [metisLecture, metisLecture2, metisLecture3], courseInformationSharingConfiguration: CourseInformationSharingConfiguration.COMMUNICATION_AND_MESSAGING, groups: ['metisTutors', 'metisStudents', 'metisInstructors'], + faqs: [metisFaq1, metisFaq2, metisFaq3], } as Course; export const metisResolvingAnswerPostUser1 = { From f1f99f9e904335377bbcc82a86242dd33f46de35 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Thu, 24 Oct 2024 10:26:38 +0200 Subject: [PATCH 11/30] You should only be able to reference accepted FAQs --- src/main/webapp/app/overview/course-overview.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/overview/course-overview.component.ts b/src/main/webapp/app/overview/course-overview.component.ts index f968384e1fcf..b3ab80078bff 100644 --- a/src/main/webapp/app/overview/course-overview.component.ts +++ b/src/main/webapp/app/overview/course-overview.component.ts @@ -68,7 +68,7 @@ import { CourseConversationsComponent } from 'app/overview/course-conversations/ import { sortCourses } from 'app/shared/util/course.util'; import { CourseUnenrollmentModalComponent } from './course-unenrollment-modal.component'; import { LtiService } from 'app/shared/service/lti.service'; -import { Faq } from 'app/entities/faq.model'; +import { Faq, FaqState } from 'app/entities/faq.model'; import { FaqService } from 'app/faq/faq.service'; interface CourseActionItem { @@ -750,7 +750,7 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit setFaqs(course: Course | undefined): void { if (course) { this.faqService - .findAllByCourseId(this.courseId) + .findAllByCourseIdAndState(this.courseId, FaqState.ACCEPTED) .pipe(map((res: HttpResponse) => res.body)) .subscribe({ next: (res: Faq[]) => { From 5860f859de55f7ac49732a844b1211aa270b2f9e Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Thu, 24 Oct 2024 10:31:44 +0200 Subject: [PATCH 12/30] Removed uneccessary attribute --- .../spec/helpers/mocks/service/mock-metis-service.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/javascript/spec/helpers/mocks/service/mock-metis-service.service.ts b/src/test/javascript/spec/helpers/mocks/service/mock-metis-service.service.ts index b3fd96510999..1ff172fc7f65 100644 --- a/src/test/javascript/spec/helpers/mocks/service/mock-metis-service.service.ts +++ b/src/test/javascript/spec/helpers/mocks/service/mock-metis-service.service.ts @@ -94,7 +94,7 @@ export class MockMetisService { return '/courses/' + metisCourse.id + '/exams/' + examId; } - getLinkForFaq(examId: string): string { + getLinkForFaq(): string { return `/courses/${this.getCourse().id}/faq`; } From fe73a9afcbbd1d6d95766ab16f3b9f2e659c653a Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Thu, 24 Oct 2024 10:33:30 +0200 Subject: [PATCH 13/30] Resolved failing test --- .../component/overview/course-faq/course-faq.component.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts b/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts index 3496e2b4ea2c..fe654a94a121 100644 --- a/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts @@ -108,6 +108,7 @@ describe('CourseFaqs', () => { faqService = TestBed.inject(FaqService); alertService = TestBed.inject(AlertService); + sortService = TestBed.inject(SortService); }); }); From b9992320a920b8f69cb5b6d5a1a4a3e4b3f0050d Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Thu, 24 Oct 2024 10:35:51 +0200 Subject: [PATCH 14/30] Margin to make ui cleaner --- .../webapp/app/overview/course-faq/course-faq.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.html b/src/main/webapp/app/overview/course-faq/course-faq.component.html index cda6244769cd..02e740d75204 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq.component.html +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.html @@ -26,7 +26,7 @@ } -
+
@if (faqs?.length === 0) {

} From 81857de5fae70dd714e97de010c8967a9922599a Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Thu, 24 Oct 2024 10:51:37 +0200 Subject: [PATCH 15/30] Remove console error --- .../app/overview/course-faq/course-faq.component.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.ts b/src/main/webapp/app/overview/course-faq/course-faq.component.ts index c708da3866f7..3b00f6d73063 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq.component.ts +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.ts @@ -1,6 +1,6 @@ import { Component, ElementRef, OnDestroy, OnInit, ViewEncapsulation, effect, inject, viewChildren } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { debounceTime, map } from 'rxjs/operators'; +import { debounceTime, map, takeUntil } from 'rxjs/operators'; import { BehaviorSubject, Subject, Subscription } from 'rxjs'; import { faFilter } from '@fortawesome/free-solid-svg-icons'; import { ButtonType } from 'app/shared/components/button.component'; @@ -60,7 +60,9 @@ export class CourseFaqComponent implements OnInit, OnDestroy { constructor() { effect(() => { - this.scrollToFaq(this.faqId); + if (this.faqId) { + this.scrollToFaq(this.faqId); + } }); } @@ -71,7 +73,7 @@ export class CourseFaqComponent implements OnInit, OnDestroy { this.loadCourseExerciseCategories(this.courseId); }); - this.route.queryParams.subscribe((params) => { + this.route.queryParams.pipe(takeUntil(this.ngUnsubscribe)).subscribe((params) => { this.faqId = params['faqId']; }); From 436aa5a6cc41bbcc4c4b46060802d0ea735eb760 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Thu, 24 Oct 2024 10:55:11 +0200 Subject: [PATCH 16/30] fixed test --- src/test/javascript/spec/service/metis/metis.service.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/javascript/spec/service/metis/metis.service.spec.ts b/src/test/javascript/spec/service/metis/metis.service.spec.ts index 161756e7f36b..cfbd0b7b7825 100644 --- a/src/test/javascript/spec/service/metis/metis.service.spec.ts +++ b/src/test/javascript/spec/service/metis/metis.service.spec.ts @@ -331,6 +331,7 @@ describe('Metis Service', () => { }); it('should determine the router link required for referencing a faq', () => { + metisService.setCourse(course); const link = metisService.getLinkForFaq(); expect(link).toBe(`/courses/${metisCourse.id}/faq`); }); From 2c006009769d77b0454f2858139d682b57584449 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Thu, 24 Oct 2024 13:31:31 +0200 Subject: [PATCH 17/30] Changes from patrik --- .../shared/metis/posting-content/posting-content.components.ts | 2 +- .../model/actions/communication/faq-reference.action.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts b/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts index 462d5103597f..f552d398d457 100644 --- a/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts +++ b/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts @@ -116,7 +116,7 @@ export class PostingContentComponent implements OnInit, OnChanges, OnDestroy { // linkToReference: link to be navigated to on reference click referenceStr = this.content.substring(this.content.indexOf(']', patternMatch.startIndex)! + 1, this.content.indexOf('(', patternMatch.startIndex)!); linkToReference = [this.content.substring(this.content.indexOf('(', patternMatch.startIndex)! + 1, this.content.indexOf(')', patternMatch.startIndex))]; - } else if (ReferenceType.FAQ == referenceType) { + } else if (ReferenceType.FAQ === referenceType) { referenceStr = this.content.substring(this.content.indexOf(']', patternMatch.startIndex)! + 1, this.content.indexOf('(', patternMatch.startIndex)!); linkToReference = [this.content.substring(this.content.indexOf('(', patternMatch.startIndex)! + 1, this.content.indexOf('?', patternMatch.startIndex))]; queryParams = { faqId: this.content.substring(this.content.indexOf('=') + 1, this.content.indexOf(')')) } as Params; diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/communication/faq-reference.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/communication/faq-reference.action.ts index ce5013037c94..b53d7b5960fa 100644 --- a/src/main/webapp/app/shared/monaco-editor/model/actions/communication/faq-reference.action.ts +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/communication/faq-reference.action.ts @@ -52,7 +52,7 @@ export class FaqReferenceAction extends TextEditorDomainActionWithOptions { } /** - * Inserts the text '/faq' into the editor and focuses it. This method will trigger the completion provider to show the available exercises. + * Inserts the text '/faq' into the editor and focuses it. This method will trigger the completion provider to show the available faqs. * @param editor The editor to insert the text into. */ run(editor: TextEditor): void { From 0f42275d06f449b58f8eef490b864c6484bb1318 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Thu, 24 Oct 2024 17:39:24 +0200 Subject: [PATCH 18/30] render selected faq --- .../webapp/app/overview/course-faq/course-faq.component.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.ts b/src/main/webapp/app/overview/course-faq/course-faq.component.ts index 3b00f6d73063..b96cf8a62742 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq.component.ts +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.ts @@ -141,8 +141,7 @@ export class CourseFaqComponent implements OnInit, OnDestroy { scrollToFaq(faqId: number): void { const faqElement = this.faqElements().find((faq) => faq.nativeElement.id === 'faq-' + String(faqId)); if (faqElement) { - this.renderer.selectRootElement(faqElement.nativeElement).scrollIntoView({ behavior: 'smooth', block: 'start' }); - this.renderer.selectRootElement(faqElement.nativeElement).focus(); + this.renderer.selectRootElement(faqElement.nativeElement, true).scrollIntoView({ behavior: 'smooth', block: 'start' }); } } } From 68858147f5ddfbef68fd5c581647ed80481ddd3b Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Thu, 24 Oct 2024 19:30:49 +0200 Subject: [PATCH 19/30] fixed query issues with ? --- .../metis/posting-content/posting-content.components.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts b/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts index f552d398d457..1584bb5e82b2 100644 --- a/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts +++ b/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts @@ -118,7 +118,9 @@ export class PostingContentComponent implements OnInit, OnChanges, OnDestroy { linkToReference = [this.content.substring(this.content.indexOf('(', patternMatch.startIndex)! + 1, this.content.indexOf(')', patternMatch.startIndex))]; } else if (ReferenceType.FAQ === referenceType) { referenceStr = this.content.substring(this.content.indexOf(']', patternMatch.startIndex)! + 1, this.content.indexOf('(', patternMatch.startIndex)!); - linkToReference = [this.content.substring(this.content.indexOf('(', patternMatch.startIndex)! + 1, this.content.indexOf('?', patternMatch.startIndex))]; + linkToReference = [ + this.content.substring(this.content.indexOf('(/courses', patternMatch.startIndex)! + 1, this.content.indexOf('?faqId', patternMatch.startIndex)), + ]; queryParams = { faqId: this.content.substring(this.content.indexOf('=') + 1, this.content.indexOf(')')) } as Params; } else if (ReferenceType.ATTACHMENT === referenceType || ReferenceType.ATTACHMENT_UNITS === referenceType) { // referenceStr: string to be displayed for the reference From 23d963308bc16f5f543617d78982d9a4c8e63237 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Sat, 26 Oct 2024 22:19:52 +0200 Subject: [PATCH 20/30] resolved merge conflict --- .../metis/posting-content/posting-content.components.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts b/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts index 31bf725ea223..2f1cd4eb4484 100644 --- a/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts +++ b/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts @@ -211,17 +211,12 @@ export class PostingContentComponent implements OnInit, OnChanges, OnDestroy { // Group 7: reference pattern for Lecture Attachments // Group 8: reference pattern for Lecture Units // Group 9: reference pattern for Users - // Group 10: reference pattern for FAQ - // globally searched for, i.e. no return after first match - const pattern = - /(?#\d+)|(?\[programming].*?\[\/programming])|(?\[modeling].*?\[\/modeling])|(?\[quiz].*?\[\/quiz])|(?\[text].*?\[\/text])|(?\[file-upload].*?\[\/file-upload])|(?\[lecture].*?\[\/lecture])|(?\[attachment].*?\[\/attachment])|(?\[lecture-unit].*?\[\/lecture-unit])|(?\[slide].*?\[\/slide])|(?\[user].*?\[\/user])|(?\[channel].*?\[\/channel])|(?\[faq].*?\[\/faq])/g; - // Group 10: pattern for embedded images // globally searched for, i.e. no return after first match + // Group 11: reference pattern for FAQ const pattern = /(?#\d+)|(?\[programming].*?\[\/programming])|(?\[modeling].*?\[\/modeling])|(?\[quiz].*?\[\/quiz])|(?\[text].*?\[\/text])|(?\[file-upload].*?\[\/file-upload])|(?\[lecture].*?\[\/lecture])|(?\[attachment].*?\[\/attachment])|(?\[lecture-unit].*?\[\/lecture-unit])|(?\[slide].*?\[\/slide])|(?\[user].*?\[\/user])|(?\[channel].*?\[\/channel])|(?!\[.*?]\(.*?\))|(?\[faq].*?\[\/faq])/g; - // array with PatternMatch objects per reference found in the posting content const patternMatches: PatternMatch[] = []; From 6f532844f36299ae87caf67b9085c47aa24203e4 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Sat, 26 Oct 2024 22:49:16 +0200 Subject: [PATCH 21/30] fixed style --- src/main/webapp/app/shared/metis/metis.util.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/webapp/app/shared/metis/metis.util.ts b/src/main/webapp/app/shared/metis/metis.util.ts index 660886857ad0..535608cd0907 100644 --- a/src/main/webapp/app/shared/metis/metis.util.ts +++ b/src/main/webapp/app/shared/metis/metis.util.ts @@ -108,7 +108,6 @@ export enum ReferenceType { CHANNEL = 'CHANNEL', FAQ = 'FAQ', IMAGE = 'IMAGE', - } export enum UserRole { From 89fe6a264ba0e1f4531ce378f0f6c6bcb43f6bcf Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Sun, 27 Oct 2024 16:55:27 +0100 Subject: [PATCH 22/30] fixed failing tests --- .../metis/posting-content/posting-content.components.ts | 2 +- .../metis/posting-content/posting-content.component.spec.ts | 2 +- .../postings-markdown-editor.component.spec.ts | 4 ++++ .../monaco-editor-communication-action.integration.spec.ts | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts b/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts index 2f1cd4eb4484..f2ca51b0c8c6 100644 --- a/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts +++ b/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts @@ -212,8 +212,8 @@ export class PostingContentComponent implements OnInit, OnChanges, OnDestroy { // Group 8: reference pattern for Lecture Units // Group 9: reference pattern for Users // Group 10: pattern for embedded images - // globally searched for, i.e. no return after first match // Group 11: reference pattern for FAQ + // globally searched for, i.e. no return after first match const pattern = /(?#\d+)|(?\[programming].*?\[\/programming])|(?\[modeling].*?\[\/modeling])|(?\[quiz].*?\[\/quiz])|(?\[text].*?\[\/text])|(?\[file-upload].*?\[\/file-upload])|(?\[lecture].*?\[\/lecture])|(?\[attachment].*?\[\/attachment])|(?\[lecture-unit].*?\[\/lecture-unit])|(?\[slide].*?\[\/slide])|(?\[user].*?\[\/user])|(?\[channel].*?\[\/channel])|(?!\[.*?]\(.*?\))|(?\[faq].*?\[\/faq])/g; diff --git a/src/test/javascript/spec/component/shared/metis/posting-content/posting-content.component.spec.ts b/src/test/javascript/spec/component/shared/metis/posting-content/posting-content.component.spec.ts index b944feae6e0b..806eade4619a 100644 --- a/src/test/javascript/spec/component/shared/metis/posting-content/posting-content.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/posting-content/posting-content.component.spec.ts @@ -496,7 +496,7 @@ describe('PostingContentComponent', () => { component.content = `I want to reference [faq]faq(/courses/1/faq?faqId=45)[/faq].`; const matches = component.getPatternMatches(); component.computePostingContentParts(matches); - expect(component.postingContentParts).toEqual([ + expect(component.postingContentParts()).toEqual([ { contentBeforeReference: 'I want to reference ', linkToReference: ['/courses/1/faq'], diff --git a/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts b/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts index 5a71653328fe..07521e4ec3dc 100644 --- a/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts @@ -113,6 +113,8 @@ describe('PostingsMarkdownEditor', () => { new QuoteAction(), new CodeAction(), new CodeBlockAction(), + new UrlAction(), + new AttachmentAction(), new UserMentionAction(courseManagementService, metisService), new ChannelReferenceAction(metisService, channelService), new ExerciseReferenceAction(metisService), @@ -132,6 +134,8 @@ describe('PostingsMarkdownEditor', () => { new QuoteAction(), new CodeAction(), new CodeBlockAction(), + new UrlAction(), + new AttachmentAction(), new UserMentionAction(courseManagementService, metisService), new ChannelReferenceAction(metisService, channelService), new ExerciseReferenceAction(metisService), diff --git a/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor-communication-action.integration.spec.ts b/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor-communication-action.integration.spec.ts index 3d95a49b9cc0..9b1c935ded18 100644 --- a/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor-communication-action.integration.spec.ts +++ b/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor-communication-action.integration.spec.ts @@ -263,7 +263,7 @@ describe('MonacoEditorCommunicationActionIntegration', () => { }); }); - describe('FaqReferenceAction (', () => { + describe('FaqReferenceAction', () => { it('should initialize with empty values if faqs are not available', () => { jest.spyOn(metisService, 'getCourse').mockReturnValue({ faqs: undefined } as any); From 7b0876dbc1a6b2d3287d5a53ff048a668f6d4852 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Mon, 28 Oct 2024 09:18:43 +0100 Subject: [PATCH 23/30] fixed error with ( the title --- .../shared/metis/posting-content/posting-content.components.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts b/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts index f2ca51b0c8c6..a2cfe70478b0 100644 --- a/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts +++ b/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts @@ -118,7 +118,7 @@ export class PostingContentComponent implements OnInit, OnChanges, OnDestroy { referenceStr = this.content.substring(this.content.indexOf(']', patternMatch.startIndex)! + 1, this.content.indexOf('(', patternMatch.startIndex)!); linkToReference = [this.content.substring(this.content.indexOf('(', patternMatch.startIndex)! + 1, this.content.indexOf(')', patternMatch.startIndex))]; } else if (ReferenceType.FAQ === referenceType) { - referenceStr = this.content.substring(this.content.indexOf(']', patternMatch.startIndex)! + 1, this.content.indexOf('(', patternMatch.startIndex)!); + referenceStr = this.content.substring(this.content.indexOf(']', patternMatch.startIndex)! + 1, this.content.lastIndexOf('(')!); linkToReference = [ this.content.substring(this.content.indexOf('(/courses', patternMatch.startIndex)! + 1, this.content.indexOf('?faqId', patternMatch.startIndex)), ]; From 47c6d5db4fa6122245fd60c92228a59eb80f9e49 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Mon, 28 Oct 2024 10:37:34 +0100 Subject: [PATCH 24/30] Changed search to apply on answers and on private chats --- .../ConversationMessageRepository.java | 16 ++++++++-------- .../communication/repository/MessageSpecs.java | 7 +++---- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationMessageRepository.java b/src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationMessageRepository.java index 21b3dfac0b81..a7268c5d7a69 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationMessageRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationMessageRepository.java @@ -5,7 +5,6 @@ import static de.tum.cit.aet.artemis.communication.repository.MessageSpecs.getConversationsSpecification; import static de.tum.cit.aet.artemis.communication.repository.MessageSpecs.getCourseWideChannelsSpecification; import static de.tum.cit.aet.artemis.communication.repository.MessageSpecs.getOwnSpecification; -import static de.tum.cit.aet.artemis.communication.repository.MessageSpecs.getSearchTextSpecification; import static de.tum.cit.aet.artemis.communication.repository.MessageSpecs.getSortSpecification; import static de.tum.cit.aet.artemis.communication.repository.MessageSpecs.getUnresolvedSpecification; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; @@ -53,7 +52,6 @@ public interface ConversationMessageRepository extends ArtemisJpaRepository configureSearchSpecification(Specification specification, PostContextFilterDTO postContextFilter, long userId) { return specification // @formatter:off - .and(getSearchTextSpecification(postContextFilter.searchText())) .and(getOwnSpecification(Boolean.TRUE.equals(postContextFilter.filterToOwn()), userId)) .and(getAnsweredOrReactedSpecification(Boolean.TRUE.equals(postContextFilter.filterToAnsweredOrReacted()), userId)) .and(getUnresolvedSpecification(Boolean.TRUE.equals(postContextFilter.filterToUnresolved()))) @@ -72,8 +70,9 @@ private Specification configureSearchSpecification(Specification spe default Page findMessages(PostContextFilterDTO postContextFilter, Pageable pageable, long userId) { var specification = Specification.where(getConversationSpecification(postContextFilter.conversationId())); specification = configureSearchSpecification(specification, postContextFilter, userId); + String searchText = postContextFilter.searchText() != null ? postContextFilter.searchText() : ""; // Fetch all necessary attributes to avoid lazy loading (even though relations are defined as EAGER in the domain class, specification queries do not respect this) - return findPostsWithSpecification(pageable, specification); + return findPostsWithSpecification(pageable, specification, searchText); } /** @@ -88,17 +87,18 @@ default Page findCourseWideMessages(PostContextFilterDTO postContextFilter var specification = Specification.where(getCourseWideChannelsSpecification(postContextFilter.courseId())) .and(getConversationsSpecification(postContextFilter.courseWideChannelIds())); specification = configureSearchSpecification(specification, postContextFilter, userId); - return findPostsWithSpecification(pageable, specification); + String searchText = postContextFilter.searchText() != null ? postContextFilter.searchText() : ""; + return findPostsWithSpecification(pageable, specification, searchText); } - private PageImpl findPostsWithSpecification(Pageable pageable, Specification specification) { + private PageImpl findPostsWithSpecification(Pageable pageable, Specification specification, String searchText) { // Only fetch the postIds without any left joins to avoid that Hibernate loads all objects and creates the page in Java long start = System.nanoTime(); Page postIds = findPostIdsWithSpecification(specification, pageable); log.debug("findPostIdsWithSpecification took {}", TimeLogUtil.formatDurationFrom(start)); // Fetch all necessary attributes to avoid lazy loading (even though relations are defined as EAGER in the domain class, specification queries do not respect this) long start2 = System.nanoTime(); - List posts = findByPostIdsWithEagerRelationships(postIds.getContent()); + List posts = findByPostIdsWithEagerRelationships(postIds.getContent(), searchText); // Make sure to sort the posts in the same order as the postIds Map postMap = posts.stream().collect(Collectors.toMap(Post::getId, post -> post)); posts = postIds.stream().map(postMap::get).toList(); @@ -118,9 +118,9 @@ private PageImpl findPostsWithSpecification(Pageable pageable, Specificati LEFT JOIN FETCH a.reactions LEFT JOIN FETCH a.post LEFT JOIN FETCH a.author - WHERE p.id IN :postIds + WHERE p.id IN :postIds and (p.content like %:searchText% OR a.content like %:searchText%) """) - List findByPostIdsWithEagerRelationships(@Param("postIds") List postIds); + List findByPostIdsWithEagerRelationships(@Param("postIds") List postIds, @Param("searchText") String searchText); default Post findMessagePostByIdElseThrow(Long postId) throws EntityNotFoundException { return getValueElseThrow(findById(postId).filter(post -> post.getConversation() != null), postId); diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/repository/MessageSpecs.java b/src/main/java/de/tum/cit/aet/artemis/communication/repository/MessageSpecs.java index 938eb9d7a8d3..1ba8ddb0f15b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/repository/MessageSpecs.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/repository/MessageSpecs.java @@ -19,7 +19,6 @@ import de.tum.cit.aet.artemis.communication.domain.PostSortCriterion; import de.tum.cit.aet.artemis.communication.domain.Post_; import de.tum.cit.aet.artemis.communication.domain.Reaction_; -import de.tum.cit.aet.artemis.communication.domain.conversation.Channel; import de.tum.cit.aet.artemis.communication.domain.conversation.Channel_; import de.tum.cit.aet.artemis.communication.domain.conversation.Conversation_; import de.tum.cit.aet.artemis.core.domain.Course_; @@ -93,11 +92,11 @@ public static Specification getCourseWideChannelsSpecification(Long course return (root, query, criteriaBuilder) -> { final var conversationJoin = root.join(Post_.conversation, JoinType.LEFT); final var isInCoursePredicate = criteriaBuilder.equal(conversationJoin.get(Channel_.COURSE).get(Course_.ID), courseId); - final var isCourseWidePredicate = criteriaBuilder.isTrue(conversationJoin.get(Channel_.IS_COURSE_WIDE)); + // final var isCourseWidePredicate = criteriaBuilder.isTrue(conversationJoin.get(Channel_.IS_COURSE_WIDE)); // make sure we only fetch channels (which are sub types of conversations) // this avoids the creation of sub queries - final var isChannelPredicate = criteriaBuilder.equal(conversationJoin.type(), criteriaBuilder.literal(Channel.class)); - return criteriaBuilder.and(isInCoursePredicate, isCourseWidePredicate, isChannelPredicate); + // final var isChannelPredicate = criteriaBuilder.equal(conversationJoin.type(), criteriaBuilder.literal(Channel.class)); + return criteriaBuilder.and(isInCoursePredicate); }; } From f1562b22f6cf0453b1dbfea31fd54d7fa312d894 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Mon, 28 Oct 2024 14:32:58 +0100 Subject: [PATCH 25/30] Fixed seach to have no errors --- .../repository/ConversationMessageRepository.java | 14 ++++++++------ .../communication/repository/MessageSpecs.java | 5 ++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationMessageRepository.java b/src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationMessageRepository.java index a7268c5d7a69..7dd771bee41d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationMessageRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationMessageRepository.java @@ -5,6 +5,7 @@ import static de.tum.cit.aet.artemis.communication.repository.MessageSpecs.getConversationsSpecification; import static de.tum.cit.aet.artemis.communication.repository.MessageSpecs.getCourseWideChannelsSpecification; import static de.tum.cit.aet.artemis.communication.repository.MessageSpecs.getOwnSpecification; +import static de.tum.cit.aet.artemis.communication.repository.MessageSpecs.getSearchTextSpecification; import static de.tum.cit.aet.artemis.communication.repository.MessageSpecs.getSortSpecification; import static de.tum.cit.aet.artemis.communication.repository.MessageSpecs.getUnresolvedSpecification; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; @@ -52,6 +53,7 @@ public interface ConversationMessageRepository extends ArtemisJpaRepository configureSearchSpecification(Specification specification, PostContextFilterDTO postContextFilter, long userId) { return specification // @formatter:off + .and(getSearchTextSpecification(postContextFilter.searchText())) .and(getOwnSpecification(Boolean.TRUE.equals(postContextFilter.filterToOwn()), userId)) .and(getAnsweredOrReactedSpecification(Boolean.TRUE.equals(postContextFilter.filterToAnsweredOrReacted()), userId)) .and(getUnresolvedSpecification(Boolean.TRUE.equals(postContextFilter.filterToUnresolved()))) @@ -72,7 +74,7 @@ default Page findMessages(PostContextFilterDTO postContextFilter, Pageable specification = configureSearchSpecification(specification, postContextFilter, userId); String searchText = postContextFilter.searchText() != null ? postContextFilter.searchText() : ""; // Fetch all necessary attributes to avoid lazy loading (even though relations are defined as EAGER in the domain class, specification queries do not respect this) - return findPostsWithSpecification(pageable, specification, searchText); + return findPostsWithSpecification(pageable, specification); } /** @@ -88,17 +90,17 @@ default Page findCourseWideMessages(PostContextFilterDTO postContextFilter .and(getConversationsSpecification(postContextFilter.courseWideChannelIds())); specification = configureSearchSpecification(specification, postContextFilter, userId); String searchText = postContextFilter.searchText() != null ? postContextFilter.searchText() : ""; - return findPostsWithSpecification(pageable, specification, searchText); + return findPostsWithSpecification(pageable, specification); } - private PageImpl findPostsWithSpecification(Pageable pageable, Specification specification, String searchText) { + private PageImpl findPostsWithSpecification(Pageable pageable, Specification specification) { // Only fetch the postIds without any left joins to avoid that Hibernate loads all objects and creates the page in Java long start = System.nanoTime(); Page postIds = findPostIdsWithSpecification(specification, pageable); log.debug("findPostIdsWithSpecification took {}", TimeLogUtil.formatDurationFrom(start)); // Fetch all necessary attributes to avoid lazy loading (even though relations are defined as EAGER in the domain class, specification queries do not respect this) long start2 = System.nanoTime(); - List posts = findByPostIdsWithEagerRelationships(postIds.getContent(), searchText); + List posts = findByPostIdsWithEagerRelationships(postIds.getContent()); // Make sure to sort the posts in the same order as the postIds Map postMap = posts.stream().collect(Collectors.toMap(Post::getId, post -> post)); posts = postIds.stream().map(postMap::get).toList(); @@ -118,9 +120,9 @@ private PageImpl findPostsWithSpecification(Pageable pageable, Specificati LEFT JOIN FETCH a.reactions LEFT JOIN FETCH a.post LEFT JOIN FETCH a.author - WHERE p.id IN :postIds and (p.content like %:searchText% OR a.content like %:searchText%) + WHERE p.id IN :postIds """) - List findByPostIdsWithEagerRelationships(@Param("postIds") List postIds, @Param("searchText") String searchText); + List findByPostIdsWithEagerRelationships(@Param("postIds") List postIds); default Post findMessagePostByIdElseThrow(Long postId) throws EntityNotFoundException { return getValueElseThrow(findById(postId).filter(post -> post.getConversation() != null), postId); diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/repository/MessageSpecs.java b/src/main/java/de/tum/cit/aet/artemis/communication/repository/MessageSpecs.java index 1ba8ddb0f15b..ec316020290b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/repository/MessageSpecs.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/repository/MessageSpecs.java @@ -60,7 +60,10 @@ else if (searchText.startsWith("#") && StringUtils.isNumeric(searchText.substrin Predicate searchInMessageContent = criteriaBuilder.like(criteriaBuilder.lower(root.get(Post_.CONTENT)), searchTextLiteral); - return criteriaBuilder.and(searchInMessageContent); + Join answersJoin = root.join(Post_.ANSWERS, JoinType.LEFT); + Predicate searchInAnswerContent = criteriaBuilder.like(criteriaBuilder.lower(answersJoin.get(AnswerPost_.CONTENT)), searchTextLiteral); + + return criteriaBuilder.or(searchInMessageContent, searchInAnswerContent); } }); } From dbe5e5f9380f467cbd763bcac29649c98a39ac1a Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Mon, 28 Oct 2024 15:45:10 +0100 Subject: [PATCH 26/30] Added references to non-channel posts --- .../ConversationMessageRepository.java | 2 -- .../repository/MessageSpecs.java | 4 ---- .../webapp/app/shared/metis/metis.service.ts | 20 ++++++++++++++++--- src/main/webapp/i18n/de/conversation.json | 2 ++ src/main/webapp/i18n/en/conversation.json | 2 ++ 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationMessageRepository.java b/src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationMessageRepository.java index 7dd771bee41d..21b3dfac0b81 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationMessageRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationMessageRepository.java @@ -72,7 +72,6 @@ private Specification configureSearchSpecification(Specification spe default Page findMessages(PostContextFilterDTO postContextFilter, Pageable pageable, long userId) { var specification = Specification.where(getConversationSpecification(postContextFilter.conversationId())); specification = configureSearchSpecification(specification, postContextFilter, userId); - String searchText = postContextFilter.searchText() != null ? postContextFilter.searchText() : ""; // Fetch all necessary attributes to avoid lazy loading (even though relations are defined as EAGER in the domain class, specification queries do not respect this) return findPostsWithSpecification(pageable, specification); } @@ -89,7 +88,6 @@ default Page findCourseWideMessages(PostContextFilterDTO postContextFilter var specification = Specification.where(getCourseWideChannelsSpecification(postContextFilter.courseId())) .and(getConversationsSpecification(postContextFilter.courseWideChannelIds())); specification = configureSearchSpecification(specification, postContextFilter, userId); - String searchText = postContextFilter.searchText() != null ? postContextFilter.searchText() : ""; return findPostsWithSpecification(pageable, specification); } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/repository/MessageSpecs.java b/src/main/java/de/tum/cit/aet/artemis/communication/repository/MessageSpecs.java index ec316020290b..63a04293f358 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/repository/MessageSpecs.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/repository/MessageSpecs.java @@ -95,10 +95,6 @@ public static Specification getCourseWideChannelsSpecification(Long course return (root, query, criteriaBuilder) -> { final var conversationJoin = root.join(Post_.conversation, JoinType.LEFT); final var isInCoursePredicate = criteriaBuilder.equal(conversationJoin.get(Channel_.COURSE).get(Course_.ID), courseId); - // final var isCourseWidePredicate = criteriaBuilder.isTrue(conversationJoin.get(Channel_.IS_COURSE_WIDE)); - // make sure we only fetch channels (which are sub types of conversations) - // this avoids the creation of sub queries - // final var isChannelPredicate = criteriaBuilder.equal(conversationJoin.type(), criteriaBuilder.literal(Channel.class)); return criteriaBuilder.and(isInCoursePredicate); }; } diff --git a/src/main/webapp/app/shared/metis/metis.service.ts b/src/main/webapp/app/shared/metis/metis.service.ts index a2badd9dba73..ae69e48f35cf 100644 --- a/src/main/webapp/app/shared/metis/metis.service.ts +++ b/src/main/webapp/app/shared/metis/metis.service.ts @@ -6,7 +6,7 @@ import { User } from 'app/core/user/user.model'; import { AccountService } from 'app/core/auth/account.service'; import { Course } from 'app/entities/course.model'; import { Posting } from 'app/entities/metis/posting.model'; -import { Injectable, OnDestroy } from '@angular/core'; +import { Injectable, OnDestroy, inject } from '@angular/core'; import { AnswerPostService } from 'app/shared/metis/answer-post.service'; import { AnswerPost } from 'app/entities/metis/answer-post.model'; import { Reaction } from 'app/entities/metis/reaction.model'; @@ -27,10 +27,11 @@ import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; import { MetisPostDTO } from 'app/entities/metis/metis-post-dto.model'; import dayjs from 'dayjs/esm'; import { PlagiarismCase } from 'app/exercises/shared/plagiarism/types/PlagiarismCase'; -import { Conversation, ConversationDTO } from 'app/entities/metis/conversation/conversation.model'; +import { Conversation, ConversationDTO, ConversationType } from 'app/entities/metis/conversation/conversation.model'; import { ChannelDTO, ChannelSubType, getAsChannelDTO } from 'app/entities/metis/conversation/channel.model'; import { ConversationService } from 'app/shared/metis/conversations/conversation.service'; import { NotificationService } from 'app/shared/notification/notification.service'; +import { TranslateService } from '@ngx-translate/core'; @Injectable() export class MetisService implements OnDestroy { @@ -49,6 +50,7 @@ export class MetisService implements OnDestroy { private subscriptionChannel?: string; private courseWideTopicSubscription: Subscription; + private translateService = inject(TranslateService); constructor( protected postService: PostService, @@ -515,14 +517,26 @@ export class MetisService implements OnDestroy { let routerLinkComponents = undefined; let queryParams = undefined; let displayName = ''; + console.log(post); if (post.conversation) { - displayName = getAsChannelDTO(post.conversation)?.name ?? ''; + displayName = this.getDisplayName(post)!; routerLinkComponents = ['/courses', this.courseId, 'communication']; queryParams = { conversationId: post.conversation.id! }; } return { routerLinkComponents, displayName, queryParams }; } + getDisplayName(post: Post) { + switch (post.conversation!.type) { + case ConversationType.CHANNEL: + return getAsChannelDTO(post.conversation)?.name ?? ''; + case ConversationType.ONE_TO_ONE: + return this.translateService.instant('artemisApp.conversationsLayout.conversationSelectionSideBar.groupChat'); + case ConversationType.GROUP_CHAT: + return this.translateService.instant('artemisApp.conversationsLayout.conversationSelectionSideBar.directMessage'); + } + } + /** * Creates (and updates) the websocket channel for receiving messages in dedicated channels; * On message reception, subsequent actions for updating the dependent components are defined based on the MetisPostAction encapsulated in the MetisPostDTO (message payload); diff --git a/src/main/webapp/i18n/de/conversation.json b/src/main/webapp/i18n/de/conversation.json index 1e35654026e4..02dbe7b3369d 100644 --- a/src/main/webapp/i18n/de/conversation.json +++ b/src/main/webapp/i18n/de/conversation.json @@ -45,7 +45,9 @@ "examChannels": "Klausuren", "createChannel": "Kanal erstellen", "browseChannels": "Kanäle durchsuchen", + "groupChat": "Gruppenchat", "groupChats": "Gruppenchats", + "directMessage": "Direktnachricht", "directMessages": "Direktnachrichten", "filterConversationPlaceholder": "Konversationen filtern", "sideBarSection": { diff --git a/src/main/webapp/i18n/en/conversation.json b/src/main/webapp/i18n/en/conversation.json index 93c350bf4c55..3cea737eb412 100644 --- a/src/main/webapp/i18n/en/conversation.json +++ b/src/main/webapp/i18n/en/conversation.json @@ -45,7 +45,9 @@ "examChannels": "Exams", "createChannel": "Create channel", "browseChannels": "Browse channels", + "groupChat": "Group Chat", "groupChats": "Group Chats", + "directMessage": "Direct Message", "directMessages": "Direct Messages", "filterConversationPlaceholder": "Filter conversations", "sideBarSection": { From c45068bc708f6b3b3c6950d3ad678c4d997df341 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Mon, 28 Oct 2024 17:51:00 +0100 Subject: [PATCH 27/30] Fixed tests, fixed string insertion --- .../posting-content.components.ts | 2 +- ...postings-markdown-editor.component.spec.ts | 78 +++++++++++-------- 2 files changed, 45 insertions(+), 35 deletions(-) diff --git a/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts b/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts index a2cfe70478b0..83ce0ba5571d 100644 --- a/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts +++ b/src/main/webapp/app/shared/metis/posting-content/posting-content.components.ts @@ -118,7 +118,7 @@ export class PostingContentComponent implements OnInit, OnChanges, OnDestroy { referenceStr = this.content.substring(this.content.indexOf(']', patternMatch.startIndex)! + 1, this.content.indexOf('(', patternMatch.startIndex)!); linkToReference = [this.content.substring(this.content.indexOf('(', patternMatch.startIndex)! + 1, this.content.indexOf(')', patternMatch.startIndex))]; } else if (ReferenceType.FAQ === referenceType) { - referenceStr = this.content.substring(this.content.indexOf(']', patternMatch.startIndex)! + 1, this.content.lastIndexOf('(')!); + referenceStr = this.content.substring(this.content.indexOf(']', patternMatch.startIndex)! + 1, this.content.indexOf('(/courses', patternMatch.startIndex)!); linkToReference = [ this.content.substring(this.content.indexOf('(/courses', patternMatch.startIndex)! + 1, this.content.indexOf('?faqId', patternMatch.startIndex)), ]; diff --git a/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts b/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts index 8112b8a1ef0a..5b8797887293 100644 --- a/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts @@ -8,7 +8,7 @@ import { MetisService } from 'app/shared/metis/metis.service'; import { MockMetisService } from '../../../../helpers/mocks/service/mock-metis-service.service'; import { metisAnswerPostUser2, metisPostExerciseUser1 } from '../../../../helpers/sample/metis-sample-data'; import { LectureService } from 'app/lecture/lecture.service'; -import { Subject } from 'rxjs'; +import { Subject, of } from 'rxjs'; import { CourseManagementService } from 'app/course/manage/course-management.service'; import { ChannelService } from 'app/shared/metis/conversations/channel.service'; import * as CourseModel from 'app/entities/course.model'; @@ -24,12 +24,13 @@ import { CodeBlockAction } from 'app/shared/monaco-editor/model/actions/code-blo import { ExerciseReferenceAction } from 'app/shared/monaco-editor/model/actions/communication/exercise-reference.action'; import { LectureAttachmentReferenceAction } from 'app/shared/monaco-editor/model/actions/communication/lecture-attachment-reference.action'; import { FaqReferenceAction } from 'app/shared/monaco-editor/model/actions/communication/faq-reference.action'; -import { UrlAction } from '../../../../../../../main/webapp/app/shared/monaco-editor/model/actions/url.action'; -import { AttachmentAction } from '../../../../../../../main/webapp/app/shared/monaco-editor/model/actions/attachment.action'; +import { UrlAction } from 'app/shared/monaco-editor/model/actions/url.action'; +import { AttachmentAction } from 'app/shared/monaco-editor/model/actions/attachment.action'; import { EmojiAction } from 'app/shared/monaco-editor/model/actions/emoji.action'; import { Overlay, OverlayPositionBuilder } from '@angular/cdk/overlay'; import { TextEditor } from 'app/shared/monaco-editor/model/actions/adapter/text-editor.interface'; import { ComponentPortal } from '@angular/cdk/portal'; +import { HttpResponse } from '@angular/common/http'; describe('PostingsMarkdownEditor', () => { let component: PostingMarkdownEditorComponent; @@ -38,6 +39,7 @@ describe('PostingsMarkdownEditor', () => { let mockMarkdownEditorComponent: MarkdownEditorMonacoComponent; let metisService: MetisService; let lectureService: LectureService; + let findLectureWithDetailsSpy: jest.SpyInstance; const backdropClickSubject = new Subject(); const mockOverlayRef = { @@ -118,6 +120,10 @@ describe('PostingsMarkdownEditor', () => { debugElement = fixture.debugElement; metisService = TestBed.inject(MetisService); lectureService = TestBed.inject(LectureService); + + findLectureWithDetailsSpy = jest.spyOn(lectureService, 'findAllByCourseIdWithSlides'); + const returnValue = of(new HttpResponse({ body: [], status: 200 })); + findLectureWithDetailsSpy.mockReturnValue(returnValue); fixture.autoDetectChanges(); mockMarkdownEditorComponent = fixture.debugElement.query(By.directive(MarkdownEditorMonacoComponent)).componentInstance; component.ngOnInit(); @@ -172,45 +178,49 @@ describe('PostingsMarkdownEditor', () => { expect(component.lectureAttachmentReferenceAction).toEqual(new LectureAttachmentReferenceAction(metisService, lectureService)); }); - it('should have set the correct default commands on init if faq is disabled', () => { - jest.spyOn(CourseModel, 'isFaqEnabled').mockReturnValueOnce(false); + it('should have set the correct default commands on init if faq is enabled', () => { + jest.spyOn(CourseModel, 'isFaqEnabled').mockReturnValueOnce(true); component.ngOnInit(); - expect(component.defaultActions).toEqual([ - new BoldAction(), - new ItalicAction(), - new UnderlineAction(), - new QuoteAction(), - new CodeAction(), - new CodeBlockAction(), - new UrlAction(), - new AttachmentAction(), - new UserMentionAction(courseManagementService, metisService), - new ChannelReferenceAction(metisService, channelService), - new ExerciseReferenceAction(metisService), - ]); + expect(component.defaultActions).toEqual( + expect.arrayContaining([ + expect.any(BoldAction), + expect.any(ItalicAction), + expect.any(UnderlineAction), + expect.any(QuoteAction), + expect.any(CodeAction), + expect.any(CodeBlockAction), + expect.any(EmojiAction), + expect.any(UrlAction), + expect.any(AttachmentAction), + expect.any(UserMentionAction), + expect.any(ChannelReferenceAction), + expect.any(ExerciseReferenceAction), + expect.any(FaqReferenceAction), + ]), + ); expect(component.lectureAttachmentReferenceAction).toEqual(new LectureAttachmentReferenceAction(metisService, lectureService)); }); - it('should have set the correct default commands on init if faq is enabled', () => { - jest.spyOn(CourseModel, 'isFaqEnabled').mockReturnValueOnce(true); + it('should have set the correct default commands on init if faq is disabled', () => { + jest.spyOn(CourseModel, 'isFaqEnabled').mockReturnValueOnce(false); component.ngOnInit(); - expect(component.defaultActions).toEqual([ - new BoldAction(), - new ItalicAction(), - new UnderlineAction(), - new QuoteAction(), - new CodeAction(), - new CodeBlockAction(), - new UrlAction(), - new AttachmentAction(), - new UserMentionAction(courseManagementService, metisService), - new ChannelReferenceAction(metisService, channelService), - new ExerciseReferenceAction(metisService), - new FaqReferenceAction(metisService), - ]); + expect(component.defaultActions).toEqual( + expect.arrayContaining([ + expect.any(BoldAction), + expect.any(ItalicAction), + expect.any(UnderlineAction), + expect.any(QuoteAction), + expect.any(CodeAction), + expect.any(CodeBlockAction), + expect.any(EmojiAction), + expect.any(UrlAction), + expect.any(AttachmentAction), + expect.any(ExerciseReferenceAction), + ]), + ); expect(component.lectureAttachmentReferenceAction).toEqual(new LectureAttachmentReferenceAction(metisService, lectureService)); }); From ffbac97ed46809756652a871d48da8aa092f47d2 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Mon, 28 Oct 2024 22:01:12 +0100 Subject: [PATCH 28/30] remove logging --- src/main/webapp/app/shared/metis/metis.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/webapp/app/shared/metis/metis.service.ts b/src/main/webapp/app/shared/metis/metis.service.ts index ae69e48f35cf..c7b04b7fe3f0 100644 --- a/src/main/webapp/app/shared/metis/metis.service.ts +++ b/src/main/webapp/app/shared/metis/metis.service.ts @@ -517,7 +517,6 @@ export class MetisService implements OnDestroy { let routerLinkComponents = undefined; let queryParams = undefined; let displayName = ''; - console.log(post); if (post.conversation) { displayName = this.getDisplayName(post)!; routerLinkComponents = ['/courses', this.courseId, 'communication']; From 676d211c8832a9085af54478d254253023c473c5 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 29 Oct 2024 08:00:59 +0100 Subject: [PATCH 29/30] Fixed search to only display course wide posts. Also did fix own filter --- .../repository/MessageSpecs.java | 17 ++++++++++++----- .../webapp/app/shared/metis/metis.service.ts | 19 +++---------------- src/main/webapp/i18n/de/conversation.json | 2 -- src/main/webapp/i18n/en/conversation.json | 2 -- 4 files changed, 15 insertions(+), 25 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/repository/MessageSpecs.java b/src/main/java/de/tum/cit/aet/artemis/communication/repository/MessageSpecs.java index 63a04293f358..87fba6211dab 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/repository/MessageSpecs.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/repository/MessageSpecs.java @@ -19,6 +19,7 @@ import de.tum.cit.aet.artemis.communication.domain.PostSortCriterion; import de.tum.cit.aet.artemis.communication.domain.Post_; import de.tum.cit.aet.artemis.communication.domain.Reaction_; +import de.tum.cit.aet.artemis.communication.domain.conversation.Channel; import de.tum.cit.aet.artemis.communication.domain.conversation.Channel_; import de.tum.cit.aet.artemis.communication.domain.conversation.Conversation_; import de.tum.cit.aet.artemis.core.domain.Course_; @@ -38,8 +39,8 @@ public static Specification getConversationSpecification(Long conversation } /** - * Specification which filters Messages according to a search string in a match-all-manner - * message is only kept if the search string (which is not a #id pattern) is included in the message content (all strings lowercased) + * Specification which filters Messages and answer posts according to a search string in a match-all-manner + * Message and answer post are only kept if the search string (which is not a #id pattern) is included in the message content (all strings lowercased) * * @param searchText Text to be searched within messages * @return specification used to chain DB operations @@ -95,12 +96,16 @@ public static Specification getCourseWideChannelsSpecification(Long course return (root, query, criteriaBuilder) -> { final var conversationJoin = root.join(Post_.conversation, JoinType.LEFT); final var isInCoursePredicate = criteriaBuilder.equal(conversationJoin.get(Channel_.COURSE).get(Course_.ID), courseId); - return criteriaBuilder.and(isInCoursePredicate); + final var isCourseWidePredicate = criteriaBuilder.isTrue(conversationJoin.get(Channel_.IS_COURSE_WIDE)); + // make sure we only fetch channels (which are sub types of conversations) + // this avoids the creation of sub queries + final var isChannelPredicate = criteriaBuilder.equal(conversationJoin.type(), criteriaBuilder.literal(Channel.class)); + return criteriaBuilder.and(isInCoursePredicate, isCourseWidePredicate, isChannelPredicate); }; } /** - * Specification to fetch Posts of the calling user + * Specification to fetch Posts and answer posts of the calling user * * @param filterToOwn whether only calling users own Posts should be fetched or not * @param userId id of the calling user @@ -112,7 +117,9 @@ public static Specification getOwnSpecification(boolean filterToOwn, Long return null; } else { - return criteriaBuilder.equal(root.get(Post_.AUTHOR).get(User_.ID), userId); + Join answersJoin = root.join(Post_.ANSWERS, JoinType.LEFT); + Predicate searchInAnswerContent = criteriaBuilder.equal(answersJoin.get(AnswerPost_.AUTHOR).get(User_.ID), userId); + return criteriaBuilder.or(criteriaBuilder.equal(root.get(Post_.AUTHOR).get(User_.ID), userId), searchInAnswerContent); } }); } diff --git a/src/main/webapp/app/shared/metis/metis.service.ts b/src/main/webapp/app/shared/metis/metis.service.ts index c7b04b7fe3f0..a2badd9dba73 100644 --- a/src/main/webapp/app/shared/metis/metis.service.ts +++ b/src/main/webapp/app/shared/metis/metis.service.ts @@ -6,7 +6,7 @@ import { User } from 'app/core/user/user.model'; import { AccountService } from 'app/core/auth/account.service'; import { Course } from 'app/entities/course.model'; import { Posting } from 'app/entities/metis/posting.model'; -import { Injectable, OnDestroy, inject } from '@angular/core'; +import { Injectable, OnDestroy } from '@angular/core'; import { AnswerPostService } from 'app/shared/metis/answer-post.service'; import { AnswerPost } from 'app/entities/metis/answer-post.model'; import { Reaction } from 'app/entities/metis/reaction.model'; @@ -27,11 +27,10 @@ import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; import { MetisPostDTO } from 'app/entities/metis/metis-post-dto.model'; import dayjs from 'dayjs/esm'; import { PlagiarismCase } from 'app/exercises/shared/plagiarism/types/PlagiarismCase'; -import { Conversation, ConversationDTO, ConversationType } from 'app/entities/metis/conversation/conversation.model'; +import { Conversation, ConversationDTO } from 'app/entities/metis/conversation/conversation.model'; import { ChannelDTO, ChannelSubType, getAsChannelDTO } from 'app/entities/metis/conversation/channel.model'; import { ConversationService } from 'app/shared/metis/conversations/conversation.service'; import { NotificationService } from 'app/shared/notification/notification.service'; -import { TranslateService } from '@ngx-translate/core'; @Injectable() export class MetisService implements OnDestroy { @@ -50,7 +49,6 @@ export class MetisService implements OnDestroy { private subscriptionChannel?: string; private courseWideTopicSubscription: Subscription; - private translateService = inject(TranslateService); constructor( protected postService: PostService, @@ -518,24 +516,13 @@ export class MetisService implements OnDestroy { let queryParams = undefined; let displayName = ''; if (post.conversation) { - displayName = this.getDisplayName(post)!; + displayName = getAsChannelDTO(post.conversation)?.name ?? ''; routerLinkComponents = ['/courses', this.courseId, 'communication']; queryParams = { conversationId: post.conversation.id! }; } return { routerLinkComponents, displayName, queryParams }; } - getDisplayName(post: Post) { - switch (post.conversation!.type) { - case ConversationType.CHANNEL: - return getAsChannelDTO(post.conversation)?.name ?? ''; - case ConversationType.ONE_TO_ONE: - return this.translateService.instant('artemisApp.conversationsLayout.conversationSelectionSideBar.groupChat'); - case ConversationType.GROUP_CHAT: - return this.translateService.instant('artemisApp.conversationsLayout.conversationSelectionSideBar.directMessage'); - } - } - /** * Creates (and updates) the websocket channel for receiving messages in dedicated channels; * On message reception, subsequent actions for updating the dependent components are defined based on the MetisPostAction encapsulated in the MetisPostDTO (message payload); diff --git a/src/main/webapp/i18n/de/conversation.json b/src/main/webapp/i18n/de/conversation.json index 02dbe7b3369d..1e35654026e4 100644 --- a/src/main/webapp/i18n/de/conversation.json +++ b/src/main/webapp/i18n/de/conversation.json @@ -45,9 +45,7 @@ "examChannels": "Klausuren", "createChannel": "Kanal erstellen", "browseChannels": "Kanäle durchsuchen", - "groupChat": "Gruppenchat", "groupChats": "Gruppenchats", - "directMessage": "Direktnachricht", "directMessages": "Direktnachrichten", "filterConversationPlaceholder": "Konversationen filtern", "sideBarSection": { diff --git a/src/main/webapp/i18n/en/conversation.json b/src/main/webapp/i18n/en/conversation.json index 3cea737eb412..93c350bf4c55 100644 --- a/src/main/webapp/i18n/en/conversation.json +++ b/src/main/webapp/i18n/en/conversation.json @@ -45,9 +45,7 @@ "examChannels": "Exams", "createChannel": "Create channel", "browseChannels": "Browse channels", - "groupChat": "Group Chat", "groupChats": "Group Chats", - "directMessage": "Direct Message", "directMessages": "Direct Messages", "filterConversationPlaceholder": "Filter conversations", "sideBarSection": { From 6234eabc5d1fba5d7d8e5d1190ebd26a2caa8154 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Tue, 29 Oct 2024 17:14:39 +0100 Subject: [PATCH 30/30] Changes from johannes --- src/main/webapp/app/entities/course.model.ts | 3 --- .../app/overview/course-faq/course-faq.component.ts | 8 ++++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/webapp/app/entities/course.model.ts b/src/main/webapp/app/entities/course.model.ts index d31f0792a6fe..f3084853431a 100644 --- a/src/main/webapp/app/entities/course.model.ts +++ b/src/main/webapp/app/entities/course.model.ts @@ -29,9 +29,6 @@ export function isCommunicationEnabled(course: Course | undefined) { return config === CourseInformationSharingConfiguration.COMMUNICATION_AND_MESSAGING || config === CourseInformationSharingConfiguration.COMMUNICATION_ONLY; } -/** - * Note: Keep in sync with method in CourseRepository.java - */ export function isFaqEnabled(course: Course | undefined) { return course?.faqEnabled; } diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.ts b/src/main/webapp/app/overview/course-faq/course-faq.component.ts index b96cf8a62742..c228372e9a84 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq.component.ts +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.ts @@ -34,7 +34,7 @@ export class CourseFaqComponent implements OnInit, OnDestroy { private parentParamSubscription: Subscription; courseId: number; - faqId: number; + referencedFaqId: number; faqs: Faq[]; filteredFaqs: Faq[]; @@ -60,8 +60,8 @@ export class CourseFaqComponent implements OnInit, OnDestroy { constructor() { effect(() => { - if (this.faqId) { - this.scrollToFaq(this.faqId); + if (this.referencedFaqId) { + this.scrollToFaq(this.referencedFaqId); } }); } @@ -74,7 +74,7 @@ export class CourseFaqComponent implements OnInit, OnDestroy { }); this.route.queryParams.pipe(takeUntil(this.ngUnsubscribe)).subscribe((params) => { - this.faqId = params['faqId']; + this.referencedFaqId = params['faqId']; }); this.searchInput.pipe(debounceTime(300)).subscribe((searchTerm: string) => {