Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Communication: Allow users to reference FAQs in post #9566

Open
wants to merge 33 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
cb29b7b
Enable references for FAQ's
cremertim Oct 22, 2024
0660599
sort faqs by id
cremertim Oct 22, 2024
f91168d
Scroll to FAQ on reference
cremertim Oct 23, 2024
61ea920
Fixed tests
cremertim Oct 23, 2024
dc8c74d
Fixed tests
cremertim Oct 23, 2024
6518cf5
Added FAQ's to initial CourseOverviewComponents
cremertim Oct 23, 2024
a3f8ba3
Add test for postingContent
cremertim Oct 23, 2024
22706e3
Merge branch 'develop' into feature/communication/reference-faq-in-me…
cremertim Oct 23, 2024
c617c83
Add tests
cremertim Oct 23, 2024
f7683a0
Adressed coderabit
cremertim Oct 23, 2024
54fc115
Added testcases
cremertim Oct 24, 2024
86c9383
Merge branch 'develop' into feature/communication/reference-faq-in-me…
cremertim Oct 24, 2024
f1f99f9
You should only be able to reference accepted FAQs
cremertim Oct 24, 2024
5860f85
Removed uneccessary attribute
cremertim Oct 24, 2024
fe73a9a
Resolved failing test
cremertim Oct 24, 2024
b999232
Margin to make ui cleaner
cremertim Oct 24, 2024
81857de
Remove console error
cremertim Oct 24, 2024
436aa5a
fixed test
cremertim Oct 24, 2024
2c00600
Changes from patrik
cremertim Oct 24, 2024
0f42275
render selected faq
cremertim Oct 24, 2024
6885814
fixed query issues with ?
cremertim Oct 24, 2024
ea0d825
Merge branch 'develop' into feature/communication/reference-faq-in-me…
cremertim Oct 25, 2024
7566bc6
Merge branch 'develop' into feature/communication/reference-faq-in-me…
cremertim Oct 25, 2024
509eb8b
Merge branch 'develop' into feature/communication/reference-faq-in-me…
cremertim Oct 26, 2024
23d9633
resolved merge conflict
cremertim Oct 26, 2024
6f53284
fixed style
cremertim Oct 26, 2024
89fe6a2
fixed failing tests
cremertim Oct 27, 2024
7b0876d
fixed error with ( the title
cremertim Oct 28, 2024
45befc9
Merge branch 'develop' into feature/communication/reference-faq-in-me…
cremertim Oct 28, 2024
c45068b
Fixed tests, fixed string insertion
cremertim Oct 28, 2024
6234eab
Changes from johannes
cremertim Oct 29, 2024
86594a3
Merge branch 'develop' into feature/communication/reference-faq-in-me…
cremertim Oct 29, 2024
7e0f0df
Merge branch 'develop' into feature/communication/reference-faq-in-me…
cremertim Oct 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/main/webapp/app/entities/course.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -28,6 +29,10 @@ export function isCommunicationEnabled(course: Course | undefined) {
return config === CourseInformationSharingConfiguration.COMMUNICATION_AND_MESSAGING || config === CourseInformationSharingConfiguration.COMMUNICATION_ONLY;
}

export function isFaqEnabled(course: Course | undefined) {
return course?.faqEnabled;
}

/**
* Note: Keep in sync with method in CourseRepository.java
*/
Expand Down Expand Up @@ -98,6 +103,7 @@ export class Course implements BaseEntity {

public exercises?: Exercise[];
public lectures?: Lecture[];
public faqs?: Faq[];
public competencies?: Competency[];
public prerequisites?: Prerequisite[];
public learningPathsEnabled?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@
}
</div>
</div>
<hr class="mb-1 mt-1" />
<hr />
@if (faqs?.length === 0) {
<h2 class="markdown-preview" jhiTranslate="artemisApp.faq.noExisting"></h2>
}
<div>
@for (faq of this.filteredFaqs; track faq) {
<jhi-course-faq-accordion [faq]="faq"></jhi-course-faq-accordion>
<div #faqElement id="faq-{{ faq.id }}">
<jhi-course-faq-accordion [faq]="faq"></jhi-course-faq-accordion>
</div>
}
</div>
@if (filteredFaqs?.length === 0 && faqs.length > 0) {
Expand Down
35 changes: 33 additions & 2 deletions src/main/webapp/app/overview/course-faq/course-faq.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component, OnDestroy, OnInit, ViewEncapsulation, inject } from '@angular/core';
import { Component, ElementRef, OnDestroy, OnInit, ViewEncapsulation, effect, inject, viewChildren } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
cremertim marked this conversation as resolved.
Show resolved Hide resolved
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';
Expand All @@ -17,6 +17,8 @@ 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';
import { Renderer2 } from '@angular/core';

@Component({
selector: 'jhi-course-faq',
Expand All @@ -27,10 +29,12 @@ import { ArtemisMarkdownModule } from 'app/shared/markdown.module';
imports: [ArtemisSharedComponentModule, ArtemisSharedModule, CourseFaqAccordionComponent, CustomExerciseCategoryBadgeComponent, SearchFilterComponent, ArtemisMarkdownModule],
})
export class CourseFaqComponent implements OnInit, OnDestroy {
faqElements = viewChildren<ElementRef>('faqElement');
private ngUnsubscribe = new Subject<void>();
private parentParamSubscription: Subscription;

courseId: number;
referencedFaqId: number;
faqs: Faq[];

filteredFaqs: Faq[];
Expand All @@ -51,13 +55,28 @@ export class CourseFaqComponent implements OnInit, OnDestroy {

private faqService = inject(FaqService);
private alertService = inject(AlertService);
private sortService = inject(SortService);
private renderer = inject(Renderer2);
cremertim marked this conversation as resolved.
Show resolved Hide resolved

constructor() {
effect(() => {
if (this.referencedFaqId) {
this.scrollToFaq(this.referencedFaqId);
}
});
}

ngOnInit(): void {
this.parentParamSubscription = this.route.parent!.params.subscribe((params) => {
this.courseId = Number(params.courseId);
this.loadFaqs();
this.loadCourseExerciseCategories(this.courseId);
});

this.route.queryParams.pipe(takeUntil(this.ngUnsubscribe)).subscribe((params) => {
this.referencedFaqId = params['faqId'];
});

this.searchInput.pipe(debounceTime(300)).subscribe((searchTerm: string) => {
this.refreshFaqList(searchTerm);
});
Expand All @@ -78,6 +97,7 @@ export class CourseFaqComponent implements OnInit, OnDestroy {
next: (res: Faq[]) => {
this.faqs = res;
this.applyFilters();
this.sortFaqs();
},
error: (res: HttpErrorResponse) => onError(this.alertService, res),
});
Expand Down Expand Up @@ -113,4 +133,15 @@ export class CourseFaqComponent implements OnInit, OnDestroy {
this.applyFilters();
this.applySearch(searchTerm);
}

sortFaqs() {
this.sortService.sortByProperty(this.filteredFaqs, 'id', true);
}

scrollToFaq(faqId: number): void {
const faqElement = this.faqElements().find((faq) => faq.nativeElement.id === 'faq-' + String(faqId));
if (faqElement) {
this.renderer.selectRootElement(faqElement.nativeElement, true).scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
}
21 changes: 21 additions & 0 deletions src/main/webapp/app/overview/course-overview.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,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, FaqState } from 'app/entities/faq.model';
import { FaqService } from 'app/faq/faq.service';
import { CourseSidebarService } from 'app/overview/course-sidebar.service';

interface CourseActionItem {
Expand Down Expand Up @@ -191,6 +193,7 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit
readonly isMessagingEnabled = isMessagingEnabled;
readonly isCommunicationEnabled = isCommunicationEnabled;

private faqService = inject(FaqService);
private courseSidebarService: CourseSidebarService = inject(CourseSidebarService);

constructor(
Expand Down Expand Up @@ -717,6 +720,7 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit
map((res: HttpResponse<Course>) => {
if (res.body) {
this.course = res.body;
this.setFaqs(this.course);
}

if (refresh) {
Expand Down Expand Up @@ -755,6 +759,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
.findAllByCourseIdAndState(this.courseId, FaqState.ACCEPTED)
.pipe(map((res: HttpResponse<Faq[]>) => res.body))
.subscribe({
next: (res: Faq[]) => {
course.faqs = res;
},
});
cremertim marked this conversation as resolved.
Show resolved Hide resolved
}
}
cremertim marked this conversation as resolved.
Show resolved Hide resolved

ngOnDestroy() {
if (this.teamAssignmentUpdateListener) {
this.teamAssignmentUpdateListener.unsubscribe();
Expand Down
8 changes: 8 additions & 0 deletions src/main/webapp/app/shared/metis/metis.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,14 @@ export class MetisService implements OnDestroy {
}
}

/**
* returns the router link required for navigating to the exercise referenced within a faq
* @return {string} router link of the faq
*/
getLinkForFaq(): string {
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
Expand Down
1 change: 1 addition & 0 deletions src/main/webapp/app/shared/metis/metis.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export enum ReferenceType {
FILE_UPLOAD = 'file-upload',
USER = 'USER',
CHANNEL = 'CHANNEL',
FAQ = 'FAQ',
cremertim marked this conversation as resolved.
Show resolved Hide resolved
IMAGE = 'IMAGE',
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -99,6 +101,8 @@ export class PostingContentPartComponent {
return faFileUpload;
case ReferenceType.SLIDE:
return faFile;
case ReferenceType.FAQ:
return faQuestion;
default:
return faPaperclip;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ 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('(/courses', 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
// attachmentToReference: location of attachment to be opened on reference click
Expand Down Expand Up @@ -206,9 +212,10 @@ 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
// Group 11: reference pattern for FAQ
// globally searched for, i.e. no return after first match
const pattern =
/(?<POST>#\d+)|(?<PROGRAMMING>\[programming].*?\[\/programming])|(?<MODELING>\[modeling].*?\[\/modeling])|(?<QUIZ>\[quiz].*?\[\/quiz])|(?<TEXT>\[text].*?\[\/text])|(?<FILE_UPLOAD>\[file-upload].*?\[\/file-upload])|(?<LECTURE>\[lecture].*?\[\/lecture])|(?<ATTACHMENT>\[attachment].*?\[\/attachment])|(?<ATTACHMENT_UNITS>\[lecture-unit].*?\[\/lecture-unit])|(?<SLIDE>\[slide].*?\[\/slide])|(?<USER>\[user].*?\[\/user])|(?<CHANNEL>\[channel].*?\[\/channel])|(?<IMAGE>!\[.*?]\(.*?\))/g;
/(?<POST>#\d+)|(?<PROGRAMMING>\[programming].*?\[\/programming])|(?<MODELING>\[modeling].*?\[\/modeling])|(?<QUIZ>\[quiz].*?\[\/quiz])|(?<TEXT>\[text].*?\[\/text])|(?<FILE_UPLOAD>\[file-upload].*?\[\/file-upload])|(?<LECTURE>\[lecture].*?\[\/lecture])|(?<ATTACHMENT>\[attachment].*?\[\/attachment])|(?<ATTACHMENT_UNITS>\[lecture-unit].*?\[\/lecture-unit])|(?<SLIDE>\[slide].*?\[\/slide])|(?<USER>\[user].*?\[\/user])|(?<CHANNEL>\[channel].*?\[\/channel])|(?<IMAGE>!\[.*?]\(.*?\))|(?<FAQ>\[faq].*?\[\/faq])/g;
cremertim marked this conversation as resolved.
Show resolved Hide resolved

// array with PatternMatch objects per reference found in the posting content
const patternMatches: PatternMatch[] = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,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';
Expand All @@ -34,6 +34,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';
import { UrlAction } from 'app/shared/monaco-editor/model/actions/url.action';
import { AttachmentAction } from 'app/shared/monaco-editor/model/actions/attachment.action';
import { ConversationDTO } from 'app/entities/metis/conversation/conversation.model';
Expand Down Expand Up @@ -93,6 +94,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(),
Expand All @@ -105,6 +108,7 @@ export class PostingMarkdownEditorComponent implements OnInit, ControlValueAcces
new AttachmentAction(),
...messagingOnlyActions,
new ExerciseReferenceAction(this.metisService),
...faqAction,
];

this.lectureAttachmentReferenceAction = new LectureAttachmentReferenceAction(this.metisService, this.lectureService);
Expand Down
cremertim marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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 ?? [];
this.setValues(
faqs.map((faq) => ({
id: faq.id!.toString(),
value: faq.questionTitle!,
type: 'faq',
})),
);
cremertim marked this conversation as resolved.
Show resolved Hide resolved

this.disposableCompletionProvider = this.registerCompletionProviderForCurrentModel<ValueItem>(
editor,
() => Promise.resolve(this.getValues()),
(item: ValueItem, range: TextEditorRange) =>
new TextEditorCompletionItem(
`/faq ${item.value}`,
item.type,
`[${item.type}]${item.value}(${this.metisService.getLinkForFaq()}?faqId=${item.id})[/${item.type}]`,
TextEditorCompletionItemKind.Default,
cremertim marked this conversation as resolved.
Show resolved Hide resolved
range,
),
'/',
);
}
cremertim marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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 {
this.replaceTextAtCurrentSelection(editor, FaqReferenceAction.DEFAULT_INSERT_TEXT);
cremertim marked this conversation as resolved.
Show resolved Hide resolved
editor.triggerCompletion();
editor.focus();
}

dispose(): void {
super.dispose();
this.disposableCompletionProvider?.dispose();
}

getOpeningIdentifier(): string {
return '[faq]';
}
}
3 changes: 2 additions & 1 deletion src/main/webapp/i18n/de/metis.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
"exercise": "Aufgabe",
"lecture": "Vortrag",
"channel": "Kanal",
"user": "Benutzer"
"user": "Benutzer",
"faq": "FAQ"
},
"channel": {
"noChannel": "Es ist kein Kanal verfügbar.",
Expand Down
3 changes: 2 additions & 1 deletion src/main/webapp/i18n/en/metis.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
"exercise": "Exercise",
"lecture": "Lecture",
"channel": "Channel",
"user": "User"
"user": "User",
"faq": "FAQ"
},
"channel": {
"noChannel": "There is no channel available.",
Expand Down
Loading
Loading