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 search for answer post in course wide channels #9616

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 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
47c6d5d
Changed search to apply on answers and on private chats
cremertim Oct 28, 2024
45befc9
Merge branch 'develop' into feature/communication/reference-faq-in-me…
cremertim Oct 28, 2024
c9f2dbd
Merge branch 'develop' into feature/communication/add-answers-to-post…
cremertim Oct 28, 2024
f1562b2
Fixed seach to have no errors
cremertim Oct 28, 2024
dbe5e5f
Added references to non-channel posts
cremertim Oct 28, 2024
c45068b
Fixed tests, fixed string insertion
cremertim Oct 28, 2024
ffbac97
remove logging
cremertim Oct 28, 2024
676d211
Fixed search to only display course wide posts. Also did fix own filter
cremertim Oct 29, 2024
6234eab
Changes from johannes
cremertim Oct 29, 2024
86594a3
Merge branch 'develop' into feature/communication/reference-faq-in-me…
cremertim Oct 29, 2024
17755b8
Merge remote-tracking branch 'origin/feature/communication/reference-…
cremertim Oct 29, 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
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ public static Specification<Post> 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
Expand All @@ -61,7 +61,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<Post, AnswerPost> answersJoin = root.join(Post_.ANSWERS, JoinType.LEFT);
Predicate searchInAnswerContent = criteriaBuilder.like(criteriaBuilder.lower(answersJoin.get(AnswerPost_.CONTENT)), searchTextLiteral);

return criteriaBuilder.or(searchInMessageContent, searchInAnswerContent);
}
});
}
Expand Down Expand Up @@ -102,7 +105,7 @@ public static Specification<Post> getCourseWideChannelsSpecification(Long course
}

/**
* 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
Expand All @@ -114,7 +117,9 @@ public static Specification<Post> getOwnSpecification(boolean filterToOwn, Long
return null;
}
else {
return criteriaBuilder.equal(root.get(Post_.AUTHOR).get(User_.ID), userId);
Join<Post, AnswerPost> 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);
}
});
}
Expand Down
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';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Correct the casing of ViewChildren in the import statement

The import statement incorrectly uses viewChildren instead of ViewChildren. In Angular, decorators and class names should follow PascalCase naming convention.

Apply this diff to fix the import:

-import { Component, ElementRef, OnDestroy, OnInit, ViewEncapsulation, effect, inject, viewChildren } from '@angular/core';
+import { Component, ElementRef, OnDestroy, OnInit, ViewEncapsulation, effect, inject, ViewChildren } from '@angular/core';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { Component, ElementRef, OnDestroy, OnInit, ViewEncapsulation, effect, inject, viewChildren } from '@angular/core';
import { Component, ElementRef, OnDestroy, OnInit, ViewEncapsulation, effect, inject, ViewChildren } from '@angular/core';

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>();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use the @ViewChildren decorator for faqElements

Currently, faqElements is assigned using viewChildren, but it should be declared using the @ViewChildren decorator to properly query child elements.

Apply this diff to correct the property declaration:

-faqElements = viewChildren<ElementRef>('faqElement');
+@ViewChildren('faqElement') faqElements: QueryList<ElementRef>;

Also, ensure you import QueryList from @angular/core:

+import { ..., QueryList } from '@angular/core';

Committable suggestion was skipped due to low confidence.

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);

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) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Access faqElements correctly and remove parentheses

The faqElements is a property, not a method. Remove the parentheses when accessing it. Additionally, use toArray() to convert the QueryList to an array before using find.

Apply this diff to fix the usage:

-const faqElement = this.faqElements().find((faq) => faq.nativeElement.id === 'faq-' + String(faqId));
+const faqElement = this.faqElements.toArray().find((faq) => faq.nativeElement.id === 'faq-' + String(faqId));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const faqElement = this.faqElements().find((faq) => faq.nativeElement.id === 'faq-' + String(faqId));
const faqElement = this.faqElements.toArray().find((faq) => faq.nativeElement.id === 'faq-' + String(faqId));

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;
},
});
}
}
Comment on lines +762 to +777
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling and subscription management.

The setFaqs method has several issues that should be addressed:

  1. Missing error handling for failed HTTP requests
  2. Potential memory leak due to unmanaged subscription
  3. No loading state management
  4. Incorrect JSDoc comment (mentions metis service)

Here's the suggested implementation:

 /**
- * set course property before using metis service
+ * Fetches and sets the accepted FAQs for the given course
  * @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),
+                takeUntil(this.ngUnsubscribe)
             )
             .subscribe({
                 next: (res: Faq[]) => {
                     course.faqs = res;
                 },
+                error: (error) => {
+                    this.alertService.addAlert({
+                        type: AlertType.DANGER,
+                        message: 'artemisApp.faq.error.load',
+                    });
+                }
             });
     }
 }

Committable suggestion was skipped due to low confidence.


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',
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;
Comment on lines +104 to +105
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider extracting icon mappings to improve maintainability.

While the FAQ case is correctly implemented, the referenceIcon method is growing with many cases. Consider extracting the icon mappings to a constant object for better maintainability.

Here's a suggested refactoring:

private static readonly REFERENCE_TYPE_ICONS: Record<ReferenceType, IconProp> = {
    [ReferenceType.POST]: faMessage,
    [ReferenceType.LECTURE]: faChalkboardUser,
    [ReferenceType.PROGRAMMING]: faKeyboard,
    [ReferenceType.MODELING]: faProjectDiagram,
    [ReferenceType.QUIZ]: faCheckDouble,
    [ReferenceType.TEXT]: faFont,
    [ReferenceType.FILE_UPLOAD]: faFileUpload,
    [ReferenceType.SLIDE]: faFile,
    [ReferenceType.FAQ]: faQuestion
};

referenceIcon(reference: ReferenceType): IconProp {
    return PostingContentPartComponent.REFERENCE_TYPE_ICONS[reference] ?? faPaperclip;
}

This approach:

  • Reduces method complexity
  • Makes it easier to add new mappings
  • Improves type safety
  • Follows Angular's style guidelines

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;
Comment on lines +120 to +125
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add null checks and improve string parsing robustness.

The FAQ reference parsing logic could be more resilient to malformed input. Consider these improvements:

 } 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;
+    const closingBracketIndex = this.content.indexOf(']', patternMatch.startIndex);
+    const coursePathIndex = this.content.indexOf('(/courses', patternMatch.startIndex);
+    const faqIdIndex = this.content.indexOf('?faqId', patternMatch.startIndex);
+    const equalsIndex = this.content.indexOf('=', faqIdIndex);
+    const closingParenIndex = this.content.indexOf(')', equalsIndex);
+    
+    if (closingBracketIndex === -1 || coursePathIndex === -1 || faqIdIndex === -1 || 
+        equalsIndex === -1 || closingParenIndex === -1) {
+        console.warn('Malformed FAQ reference detected');
+        return;
+    }
+    
+    referenceStr = this.content.substring(closingBracketIndex + 1, coursePathIndex);
+    linkToReference = [this.content.substring(coursePathIndex + 1, faqIdIndex)];
+    queryParams = { faqId: this.content.substring(equalsIndex + 1, closingParenIndex) } as Params;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} 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.FAQ === referenceType) {
const closingBracketIndex = this.content.indexOf(']', patternMatch.startIndex);
const coursePathIndex = this.content.indexOf('(/courses', patternMatch.startIndex);
const faqIdIndex = this.content.indexOf('?faqId', patternMatch.startIndex);
const equalsIndex = this.content.indexOf('=', faqIdIndex);
const closingParenIndex = this.content.indexOf(')', equalsIndex);
if (closingBracketIndex === -1 || coursePathIndex === -1 || faqIdIndex === -1 ||
equalsIndex === -1 || closingParenIndex === -1) {
console.warn('Malformed FAQ reference detected');
return;
}
referenceStr = this.content.substring(closingBracketIndex + 1, coursePathIndex);
linkToReference = [this.content.substring(coursePathIndex + 1, faqIdIndex)];
queryParams = { faqId: this.content.substring(equalsIndex + 1, closingParenIndex) } 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider improving regex maintainability and validation.

The regex pattern could benefit from these improvements:

  1. Split the long regex into named constants for better maintainability:
private static readonly PATTERNS = {
    POST: '#\\d+',
    FAQ: '\\[faq].*?\\[\\/faq]',
    // ... other patterns
};

private static readonly COMBINED_PATTERN = new RegExp(
    Object.entries(PATTERNS)
        .map(([key, pattern]) => `(?<${key}>${pattern})`)
        .join('|'),
    'g'
);
  1. Make the FAQ pattern more specific to validate the expected format:
FAQ: '\\[faq]\\s*([^\\[\\]]+?)\\s*\\(\\s*\\/courses\\/[^\\s]+\\?faqId=\\d+\\s*\\)\\s*\\[\\/faq]'

Also applies to: 218-218

// 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;

// 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
Loading
Loading