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 editors to propose FAQ #9457

Closed
wants to merge 28 commits into from
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0544837
First draft implementation: Propose FAQ as editor
cremertim Oct 9, 2024
280e3f4
First draft implementation: Added test cases
cremertim Oct 9, 2024
a7dbc99
Merge branch 'develop' into feature/communication/propose-faq
cremertim Oct 9, 2024
2eb7316
fixed tests
cremertim Oct 9, 2024
2be8182
Merge branch 'develop' into feature/communication/propose-faq
cremertim Oct 11, 2024
a1c3562
Merge branch 'refs/heads/develop' into feature/communication/propose-faq
cremertim Oct 12, 2024
0ab7a91
Added proper alert messages
cremertim Oct 12, 2024
47e51cc
merged develop
cremertim Oct 12, 2024
edf208e
`Integrated code lifecycle`: Simplify user interface for ssh keys (#9…
SimonEntholzer Oct 12, 2024
491e368
Exam mode: Add summary to exam deletion dialog (#9185)
ole-ve Oct 12, 2024
8045a7f
Development: Fix architecture tests for exam deletion summary (#9458)
ole-ve Oct 12, 2024
29add81
Tutorial groups: Redesign overview page (#9445)
PaRangger Oct 12, 2024
ebc28b2
Iris: Add chat in text exercises (#9362)
MichaelOwenDyer Oct 12, 2024
a893024
Plagiarism checks: Use file extensions for plagiarism view (#9350)
magaupp Oct 12, 2024
3154064
Integrated code lifecycle: Configure checkout path and timeout for pr…
BBesrour Oct 12, 2024
87d73ec
Development: Migrate the git diff report module to standalone compone…
pzdr7 Oct 12, 2024
db90e89
General: Fix an issue when trying to delete too many users at once (#…
chrisknedl Oct 12, 2024
d8c99a2
Modeling exercises: Fix submission error and redundant tooltip for AI…
LeonWehrhahn Oct 12, 2024
2259241
Integrated code lifecycle: Disable access to personal VCS access toke…
SimonEntholzer Oct 12, 2024
0139625
Development: Adapt client test coverage
krusche Oct 12, 2024
6deb17c
Exam mode: Generate student exam on demand if student is registered f…
coolchock Oct 12, 2024
d801ef8
Development: Fix architecture tests in exam module
krusche Oct 12, 2024
330a677
General: Cache course icons and profile pictures to improve performan…
krusche Oct 12, 2024
1f6cc87
merge changes
cremertim Oct 9, 2024
8720762
First draft implementation: Added test cases
cremertim Oct 9, 2024
7beeea6
fixed tests
cremertim Oct 9, 2024
ba80f4e
Added proper alert messages
cremertim Oct 12, 2024
059a0b5
Merge remote-tracking branch 'origin/feature/communication/propose-fa…
cremertim Oct 13, 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 @@ -12,6 +12,7 @@
import org.springframework.transaction.annotation.Transactional;

import de.tum.cit.aet.artemis.communication.domain.Faq;
import de.tum.cit.aet.artemis.communication.domain.FaqState;
import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository;

/**
Expand All @@ -30,6 +31,8 @@ public interface FaqRepository extends ArtemisJpaRepository<Faq, Long> {
""")
Set<String> findAllCategoriesByCourseId(@Param("courseId") Long courseId);

Set<Faq> findAllByCourseIdAndFaqState(Long courseId, FaqState faqState);

@Transactional
@Modifying
void deleteAllByCourseId(Long courseId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@
import org.springframework.web.bind.annotation.RestController;

import de.tum.cit.aet.artemis.communication.domain.Faq;
import de.tum.cit.aet.artemis.communication.domain.FaqState;
import de.tum.cit.aet.artemis.communication.dto.FaqDTO;
import de.tum.cit.aet.artemis.communication.repository.FaqRepository;
import de.tum.cit.aet.artemis.core.domain.Course;
import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException;
import de.tum.cit.aet.artemis.core.repository.CourseRepository;
import de.tum.cit.aet.artemis.core.security.Role;
import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastEditor;
import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastInstructor;
import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent;
import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService;
Expand Down Expand Up @@ -72,7 +74,7 @@ public FaqResource(CourseRepository courseRepository, AuthorizationCheckService
* @throws URISyntaxException if the Location URI syntax is incorrect
*/
@PostMapping("courses/{courseId}/faqs")
@EnforceAtLeastInstructor
@EnforceAtLeastEditor
public ResponseEntity<FaqDTO> createFaq(@RequestBody Faq faq, @PathVariable Long courseId) throws URISyntaxException {
log.debug("REST request to save Faq : {}", faq);
if (faq.getId() != null) {
Expand All @@ -82,7 +84,7 @@ public ResponseEntity<FaqDTO> createFaq(@RequestBody Faq faq, @PathVariable Long
if (faq.getCourse() == null || !faq.getCourse().getId().equals(courseId)) {
throw new BadRequestAlertException("Course ID in path and FAQ do not match", ENTITY_NAME, "courseIdMismatch");
}
authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null);
authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, faq.getCourse(), null);

Faq savedFaq = faqRepository.save(faq);
FaqDTO dto = new FaqDTO(savedFaq);
Expand All @@ -99,13 +101,14 @@ public ResponseEntity<FaqDTO> createFaq(@RequestBody Faq faq, @PathVariable Long
* if the faq is not valid or if the faq course id does not match with the path variable
*/
@PutMapping("courses/{courseId}/faqs/{faqId}")
@EnforceAtLeastInstructor
@EnforceAtLeastEditor
public ResponseEntity<FaqDTO> updateFaq(@RequestBody Faq faq, @PathVariable Long faqId, @PathVariable Long courseId) {
log.debug("REST request to update Faq : {}", faq);
if (faqId == null || !faqId.equals(faq.getId())) {
throw new BadRequestAlertException("Id of FAQ and path must match", ENTITY_NAME, "idNull");
}
authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null);
Course course = courseRepository.findByIdElseThrow(courseId);
authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null);
cremertim marked this conversation as resolved.
Show resolved Hide resolved
cremertim marked this conversation as resolved.
Show resolved Hide resolved
Faq existingFaq = faqRepository.findByIdElseThrow(faqId);
if (!Objects.equals(existingFaq.getCourse().getId(), courseId)) {
throw new BadRequestAlertException("Course ID of the FAQ provided courseID must match", ENTITY_NAME, "idNull");
Expand Down Expand Up @@ -174,6 +177,34 @@ public ResponseEntity<Set<FaqDTO>> getFaqForCourse(@PathVariable Long courseId)
return ResponseEntity.ok().body(faqDTOS);
}

/**
* GET /courses/:courseId/faq-status/:faqState : get all the faqs of a course in the specified status
*
* @param courseId the courseId of the course for which all faqs should be returned
* @param faqState the state of all returned FAQs
* @return the ResponseEntity with status 200 (OK) and the list of faqs in body
*/
@GetMapping("courses/{courseId}/faq-state/{faqState}")
@EnforceAtLeastStudent
public ResponseEntity<Set<FaqDTO>> getAllFaqForCourseByStatus(@PathVariable Long courseId, @PathVariable String faqState) {
log.debug("REST request to get all Faqs for the course with id : {}", courseId);
FaqState retrivedState = defineState(faqState);
Course course = courseRepository.findByIdElseThrow(courseId);
authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null);
Set<Faq> faqs = faqRepository.findAllByCourseIdAndFaqState(courseId, retrivedState);
Set<FaqDTO> faqDTOS = faqs.stream().map(FaqDTO::new).collect(Collectors.toSet());
return ResponseEntity.ok().body(faqDTOS);
}

private FaqState defineState(String faqState) {
return switch (faqState) {
case "ACCEPTED" -> FaqState.ACCEPTED;
case "REJECTED" -> FaqState.REJECTED;
case "PROPOSED" -> FaqState.PROPOSED;
default -> throw new IllegalArgumentException("Unknown state: " + faqState);
};
cremertim marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* GET /courses/:courseId/faq-categories : get all the faq categories of a course
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
<span class="tab-link-text" jhiTranslate="entity.action.scores"></span>
</a>
}
@if (course.isAtLeastInstructor && course.faqEnabled) {
@if (course.isAtLeastEditor && course.faqEnabled) {
<a class="tab-link" [routerLink]="['/course-management', course.id, 'faqs']" routerLinkActive="active">
<fa-icon [icon]="faQuestion" />
<span class="tab-link-text" jhiTranslate="entity.action.faq"></span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ <h4 class="text-center no-exercises mt-3 fw-medium" jhiTranslate="artemisApp.cou
</a>
}

@if (course.isAtLeastInstructor && course.faqEnabled) {
@if (course.isAtLeastEditor && course.faqEnabled) {
<a
[routerLink]="['/course-management', course.id, 'faqs']"
class="btn btn-info me-1 mb-1"
Expand Down
6 changes: 3 additions & 3 deletions src/main/webapp/app/entities/faq.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { Course } from 'app/entities/course.model';
import { FaqCategory } from './faq-category.model';

export enum FaqState {
ACCEPTED,
REJECTED,
PROPOSED,
ACCEPTED = 'ACCEPTED',
REJECTED = 'REJECTED',
PROPOSED = 'PROPOSED',
cremertim marked this conversation as resolved.
Show resolved Hide resolved
}

export class Faq implements BaseEntity {
Expand Down
18 changes: 15 additions & 3 deletions src/main/webapp/app/faq/faq-update.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown
import { ArtemisCategorySelectorModule } from 'app/shared/category-selector/category-selector.module';
import { ArtemisSharedModule } from 'app/shared/shared.module';
import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module';
import { AccountService } from 'app/core/auth/account.service';

@Component({
selector: 'jhi-faq-update',
Expand All @@ -31,6 +32,7 @@ export class FaqUpdateComponent implements OnInit {
existingCategories: FaqCategory[];
faqCategories: FaqCategory[];
courseId: number;
isAtLeastInstructor: boolean = false;
domainActionsDescription = [new FormulaAction()];

// Icons
Expand All @@ -44,6 +46,7 @@ export class FaqUpdateComponent implements OnInit {
private navigationUtilService = inject(ArtemisNavigationUtilService);
private router = inject(Router);
private translateService = inject(TranslateService);
private accountService = inject(AccountService);

ngOnInit() {
this.isSaving = false;
Expand All @@ -56,6 +59,7 @@ export class FaqUpdateComponent implements OnInit {
if (course) {
this.faq.course = course;
this.loadCourseFaqCategories(course.id);
this.isAtLeastInstructor = this.accountService.isAtLeastInstructorInCourse(course);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Ensure the subscription is properly unsubscribed to prevent memory leaks

The subscribe method used on this.activatedRoute.parent?.data in ngOnInit may lead to a memory leak if the component is destroyed before the observable completes. To prevent this, consider using the takeUntil operator with an ngOnDestroy lifecycle hook to unsubscribe when the component is destroyed.

Apply this refactor to manage the subscription:

 import { Component, OnInit, OnDestroy, inject } from '@angular/core';
 import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';

 export class FaqUpdateComponent implements OnInit, OnDestroy {
     private ngUnsubscribe = new Subject<void>();

     ngOnInit() {
         // ...
-        this.activatedRoute.parent?.data.subscribe((data) => {
+        this.activatedRoute.parent?.data.pipe(takeUntil(this.ngUnsubscribe)).subscribe((data) => {
             // existing code
         });
     }

+    ngOnDestroy() {
+        this.ngUnsubscribe.next();
+        this.ngUnsubscribe.complete();
+    }
 }

Committable suggestion was skipped due to low confidence.

}
this.faqCategories = faq?.categories ? faq.categories : [];
});
Expand All @@ -77,10 +81,10 @@ export class FaqUpdateComponent implements OnInit {
*/
save() {
this.isSaving = true;
this.faq.faqState = this.isAtLeastInstructor ? FaqState.ACCEPTED : FaqState.PROPOSED;
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Consider handling potential null values for this.faq

While assigning faqState, ensure that this.faq is not null or undefined to prevent potential runtime errors.

Apply this minor adjustment:

 this.isSaving = true;
+if (this.faq) {
     this.faq.faqState = this.isAtLeastInstructor ? FaqState.ACCEPTED : FaqState.PROPOSED;
+}
📝 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
this.faq.faqState = this.isAtLeastInstructor ? FaqState.ACCEPTED : FaqState.PROPOSED;
this.isSaving = true;
if (this.faq) {
this.faq.faqState = this.isAtLeastInstructor ? FaqState.ACCEPTED : FaqState.PROPOSED;
}

if (this.faq.id !== undefined) {
this.subscribeToSaveResponse(this.faqService.update(this.courseId, this.faq));
} else {
this.faq.faqState = FaqState.ACCEPTED;
this.subscribeToSaveResponse(this.faqService.create(this.courseId, this.faq));
}
}
Expand All @@ -107,13 +111,21 @@ export class FaqUpdateComponent implements OnInit {
if (faqBody) {
this.faq = faqBody;
}
this.alertService.success(this.translateService.instant('artemisApp.faq.created', { id: faq.id }));
if (this.isAtLeastInstructor) {
this.alertService.success(this.translateService.instant('artemisApp.faq.created', { id: faq.id }));
} else {
this.alertService.success(this.translateService.instant('artemisApp.faq.proposed', { id: faq.id }));
}
Comment on lines +114 to +118
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Refactor duplicated success message code to improve maintainability

The blocks of code displaying success messages in both the creation and update scenarios are similar, with differences only in the translation keys and messages. To reduce duplication and enhance maintainability, consider refactoring the success message logic into a separate method or variable.

Apply this refactor to consolidate the success message handling:

 protected onSaveSuccess(faq: Faq) {
     this.isSaving = false;
     const isNewFaq = !this.faq.id;
     const actionKey = isNewFaq ? (this.isAtLeastInstructor ? 'created' : 'proposed') : (this.isAtLeastInstructor ? 'updated' : 'proposedChange');
     const translationKey = `artemisApp.faq.${actionKey}`;

+    this.alertService.success(this.translateService.instant(translationKey, { id: faq.id }));
     this.router.navigate(['course-management', this.courseId, 'faqs']);
 }

Then, you can remove the duplicated code blocks from both if-else branches.

Also applies to: 124-128

this.router.navigate(['course-management', this.courseId, 'faqs']);
},
});
} else {
this.isSaving = false;
this.alertService.success(this.translateService.instant('artemisApp.faq.updated', { id: faq.id }));
if (this.isAtLeastInstructor) {
this.alertService.success(this.translateService.instant('artemisApp.faq.updated', { id: faq.id }));
} else {
this.alertService.success(this.translateService.instant('artemisApp.faq.proposedChange', { id: faq.id }));
}
this.router.navigate(['course-management', this.courseId, 'faqs']);
}
}
Expand Down
62 changes: 44 additions & 18 deletions src/main/webapp/app/faq/faq.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,17 @@ <h2 id="page-heading">
}
</div>
<div class="d-flex-end justify-content-end">
<a id="create-faq" class="btn btn-primary mb-1" [routerLink]="['new']">
<fa-icon [icon]="faPlus" />
<span jhiTranslate="artemisApp.faq.home.createLabel"></span>
</a>
@if (isAtleastInstrucor) {
<a id="create-faq" class="btn btn-primary mb-1" [routerLink]="['new']">
<fa-icon [icon]="faPlus" />
<span jhiTranslate="artemisApp.faq.home.createLabel"></span>
</a>
} @else {
<a id="propose-faq" class="btn btn-primary mb-1" [routerLink]="['new']">
<fa-icon [icon]="faPlus" />
<span jhiTranslate="artemisApp.faq.home.proposeLabel"></span>
</a>
}
cremertim marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>
</div>
Expand All @@ -64,6 +71,10 @@ <h2 id="page-heading">
<span jhiTranslate="artemisApp.faq.table.questionAnswer"></span>
<fa-icon [icon]="faSort" />
</th>
<th jhiSortBy="faqState">
<span jhiTranslate="artemisApp.faq.table.state"></span>
<fa-icon [icon]="faSort" />
</th>
<th jhiSortBy="categories">
<span jhiTranslate="artemisApp.faq.table.categories"></span>
<fa-icon [icon]="faSort" />
Expand All @@ -83,34 +94,49 @@ <h2 id="page-heading">
<td>
<p class="markdown-preview" [innerHTML]="faq.questionAnswer | htmlForMarkdown"></p>
</td>
<td>
<p class="markdown-preview" [innerHTML]="faq.faqState | htmlForMarkdown"></p>
</td>
Comment on lines +100 to +102
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

⚠️ Potential issue

Display user-friendly labels for FAQ states

Currently, the FAQ state is displayed using faq.faqState, which may show the raw enum value (e.g., PROPOSED, APPROVED). To improve user experience, consider displaying a user-friendly label or translating the state.

You can apply this change to use the Angular translate pipe with a translation key:

-        <p class="markdown-preview" [innerHTML]="faq.faqState | htmlForMarkdown"></p>
+        <p>{{ 'artemisApp.faq.state.' + faq.faqState | translate }}</p>

Ensure that you have the corresponding entries in your translation files, such as:

"artemisApp": {
  "faq": {
    "state": {
      "PROPOSED": "Proposed",
      "APPROVED": "Approved",
      "REJECTED": "Rejected"
    }
  }
}

Let me know if you need assistance in implementing this change or updating the translation files.

<td>
<div class="d-flex">
@for (category of faq.categories; track category) {
<jhi-custom-exercise-category-badge [category]="category" />
}
</div>
</td>

<td class="text-end">
<div class="btn-group flex-btn-group-container">
@if (faq.faqState == FaqState.PROPOSED && isAtleastInstrucor) {
<div class="btn-group-vertical me-1 mb-1">
<button class="btn btn-success btn-sm" (click)="acceptProposedFaq(courseId, faq)">
<fa-icon [icon]="faCheck" />
<span class="d-none d-md-inline" jhiTranslate="artemisApp.faq.home.accept"></span>
</button>
<button type="button" class="mt-1 btn btn-warning" id="reject-faq-{{ faq.id }}" (click)="rejectFaq(courseId, faq)">
<fa-icon [icon]="faCancel" />
<span class="d-none d-md-inline" jhiTranslate="artemisApp.faq.home.reject"></span>
</button>
cremertim marked this conversation as resolved.
Show resolved Hide resolved
</div>
}
<div class="btn-group-vertical me-1 mb-1">
<a [routerLink]="[faq.id, 'edit']" class="btn btn-primary btn-sm">
<fa-icon [icon]="faPencilAlt" />
<span class="d-none d-md-inline" jhiTranslate="entity.action.edit"></span>
</a>

<button
class="mt-1"
jhiDeleteButton
id="delete-faq-{{ faq.id }}"
[entityTitle]="faq.questionTitle || ''"
deleteQuestion="artemisApp.faq.delete.question"
deleteConfirmationText="artemisApp.faq.delete.typeNameToConfirm"
(delete)="deleteFaq(courseId, faq.id!)"
[dialogError]="dialogError$"
>
<fa-icon [icon]="faTrash" />
</button>
@if (isAtleastInstrucor) {
<button
class="mt-1"
jhiDeleteButton
id="delete-faq-{{ faq.id }}"
[entityTitle]="faq.questionTitle || ''"
deleteQuestion="artemisApp.faq.delete.question"
deleteConfirmationText="artemisApp.faq.delete.typeNameToConfirm"
(delete)="deleteFaq(courseId, faq.id!)"
[dialogError]="dialogError$"
>
<fa-icon [icon]="faTrash" />
</button>
}
Comment on lines +129 to +142
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Consider allowing tutors to delete their own proposed FAQs

Currently, only instructors can delete FAQs. To enhance usability and allow tutors to manage their own content, consider permitting tutors to delete their own proposed FAQs before they are reviewed by an instructor.

</div>
</div>
</td>
Expand Down
47 changes: 45 additions & 2 deletions src/main/webapp/app/faq/faq.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component, OnDestroy, OnInit, inject } from '@angular/core';
import { Faq } from 'app/entities/faq.model';
import { faEdit, faFilter, faPencilAlt, faPlus, faSort, faTrash } from '@fortawesome/free-solid-svg-icons';
import { Faq, FaqState } from 'app/entities/faq.model';
import { faCancel, faCheck, faEdit, faFilter, faPencilAlt, faPlus, faSort, faTrash } from '@fortawesome/free-solid-svg-icons';
import { Subject } from 'rxjs';
import { map } from 'rxjs/operators';
import { AlertService } from 'app/core/util/alert.service';
Expand All @@ -15,6 +15,9 @@ import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-catego
import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module';
import { ArtemisSharedModule } from 'app/shared/shared.module';
import { ArtemisMarkdownModule } from 'app/shared/markdown.module';
import { AccountService } from 'app/core/auth/account.service';
import { Course } from 'app/entities/course.model';
import { TranslateService } from '@ngx-translate/core';

@Component({
selector: 'jhi-faq',
Expand All @@ -24,11 +27,14 @@ import { ArtemisMarkdownModule } from 'app/shared/markdown.module';
imports: [ArtemisSharedModule, CustomExerciseCategoryBadgeComponent, ArtemisSharedComponentModule, ArtemisMarkdownModule],
})
export class FaqComponent implements OnInit, OnDestroy {
protected readonly FaqState = FaqState;
faqs: Faq[];
course: Course;
filteredFaqs: Faq[];
existingCategories: FaqCategory[];
courseId: number;
hasCategories: boolean = false;
cremertim marked this conversation as resolved.
Show resolved Hide resolved
isAtleastInstrucor: boolean = false;
cremertim marked this conversation as resolved.
Show resolved Hide resolved

private dialogErrorSource = new Subject<string>();
dialogError$ = this.dialogErrorSource.asObservable();
Expand All @@ -44,11 +50,15 @@ export class FaqComponent implements OnInit, OnDestroy {
faPencilAlt = faPencilAlt;
faFilter = faFilter;
faSort = faSort;
faCancel = faCancel;
faCheck = faCheck;

private faqService = inject(FaqService);
private route = inject(ActivatedRoute);
private alertService = inject(AlertService);
private sortService = inject(SortService);
private accountService = inject(AccountService);
private translateService = inject(TranslateService);

constructor() {
this.predicate = 'id';
Expand All @@ -59,6 +69,13 @@ export class FaqComponent implements OnInit, OnDestroy {
this.courseId = Number(this.route.snapshot.paramMap.get('courseId'));
this.loadAll();
this.loadCourseFaqCategories(this.courseId);
this.route.data.subscribe((data) => {
const course = data['course'];
if (course) {
this.course = course;
this.isAtleastInstrucor = this.accountService.isAtLeastInstructorInCourse(course);
}
});
cremertim marked this conversation as resolved.
Show resolved Hide resolved
}

ngOnDestroy(): void {
Expand Down Expand Up @@ -121,4 +138,30 @@ export class FaqComponent implements OnInit, OnDestroy {
});
this.applyFilters();
}

rejectFaq(courseId: number, faq: Faq) {
const previousState = faq.faqState;
faq.faqState = FaqState.REJECTED;
faq.course = this.course;
this.faqService.update(courseId, faq).subscribe({
next: () => this.alertService.success(this.translateService.instant('artemisApp.faq.rejected', { id: faq.id })),
error: (error: HttpErrorResponse) => {
this.dialogErrorSource.next(error.message);
faq.faqState = previousState;
},
});
}

acceptProposedFaq(courseId: number, faq: Faq) {
const previousState = faq.faqState;
faq.faqState = FaqState.ACCEPTED;
faq.course = this.course;
this.faqService.update(courseId, faq).subscribe({
next: () => this.alertService.success(this.translateService.instant('artemisApp.faq.accepted', { id: faq.id })),
error: (error: HttpErrorResponse) => {
this.dialogErrorSource.next(error.message);
faq.faqState = previousState;
},
});
}
Comment on lines +163 to +187
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Refactor rejectFaq and acceptProposedFaq to reduce code duplication

The methods rejectFaq and acceptProposedFaq have similar logic. To adhere to the DRY (Don't Repeat Yourself) principle, consider refactoring them into a single reusable method.

You can create a generic method to handle FAQ state updates:

private updateFaqState(courseId: number, faq: Faq, newState: FaqState, translationKey: string) {
    const previousState = faq.faqState;
    faq.faqState = newState;
    faq.course = this.course;
    this.faqService.update(courseId, faq).subscribe({
        next: () => this.alertService.success(this.translateService.instant(translationKey, { id: faq.id })),
        error: (error: HttpErrorResponse) => {
            this.dialogErrorSource.next(error.message);
            faq.faqState = previousState;
        },
    });
}

Then, refactor the original methods to utilize this new function:

rejectFaq(courseId: number, faq: Faq) {
    this.updateFaqState(courseId, faq, FaqState.REJECTED, 'artemisApp.faq.rejected');
}

acceptProposedFaq(courseId: number, faq: Faq) {
    this.updateFaqState(courseId, faq, FaqState.ACCEPTED, 'artemisApp.faq.accepted');
}

This refactoring enhances maintainability and simplifies future updates.

}
Loading
Loading